PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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/LaunchPage.jsx +263 -111 3.1.13.2.1 View file →
@@ -1,23 +1,40 @@
1 -import { Launch } from '@auto-launch/components/Launch';
1 +import { CreatingSite } from '@auto-launch/components/CreatingSite';
2 +import { DescriptionGathering } from '@auto-launch/components/DescriptionGathering';
3 +import { LaunchUpdate } from '@auto-launch/components/LaunchUpdate';
2 4 import { Logo } from '@auto-launch/components/Logo';
3 -import { MovingGradient } from '@auto-launch/components/MovingGradients';
4 -import { NeedsTheme } from '@auto-launch/components/NeedsTheme';
5 -import { RestartLaunchModal } from '@auto-launch/components/RestartLaunchModal';
6 -import { ViewportPulse } from '@auto-launch/components/ViewportPulse';
5 +import {
6 + ModalHeading,
7 + ModalLink,
8 + ModalText,
9 +} from '@auto-launch/components/Modal';
10 +import { ShaderBackground } from '@auto-launch/components/ShaderBackground';
11 +import { activeLogo } from '@auto-launch/functions/design';
7 12 import { getAbTest } from '@auto-launch/functions/getAbTest';
8 13 import { preLaunchFunctions } from '@auto-launch/functions/setup';
9 14 import { updateOption } from '@auto-launch/functions/wp';
15 +import { useExtendifyCodeConnector } from '@auto-launch/hooks/useExtendifyCodeConnector';
16 +import { useInstallRequiredPlugins } from '@auto-launch/hooks/useInstallRequiredPlugins';
17 +import { useMigrateChoice } from '@auto-launch/hooks/useMigrateChoice';
18 +import { useNeedsTheme } from '@auto-launch/hooks/useNeedsTheme';
19 +import { useRestartLaunch } from '@auto-launch/hooks/useRestartLaunch';
10 20 import { useLaunchDataStore } from '@auto-launch/state/launch-data';
21 +import { launchStrings } from '@auto-launch/strings';
22 +import {
23 + activeChoiceTemplate,
24 + activeDescriptionTemplate,
25 + activeModalTemplate,
26 +} from '@auto-launch/templates';
27 +import { ActionButton, SecondaryButton } from '@auto-launch/templates/button';
28 +import { Heading, PageLink } from '@auto-launch/templates/heading';
29 +import { digest } from '@shared/api/digest';
11 30 import { registerCoreBlocks } from '@wordpress/block-library';
12 31 import { getBlockTypes } from '@wordpress/blocks';
13 32 import { useSelect } from '@wordpress/data';
14 -import { useEffect, useRef } from '@wordpress/element';
15 -import { __ } from '@wordpress/i18n';
16 -import { chevronLeft, Icon } from '@wordpress/icons';
33 +import { useEffect, useState } from '@wordpress/element';
17 34 import classNames from 'classnames';
18 35 import { AnimatePresence, motion } from 'framer-motion';
19 -import { checkIn } from './functions/insights';
36 +import { checkIn, reportRestApiStatus } from './functions/insights';
20 37
21 38 export const LaunchPage = () => {
22 39 const theme = useSelect((select) => select('core').getCurrentTheme());
23 40 // Checking `theme` here makes sure the data is populated
@@ -25,23 +42,68 @@
25 42
26 43 const oldPages = window.extLaunchData.resetSiteInformation.pagesIds ?? [];
27 44 const needsToReset = oldPages.length > 0;
28 45
29 - const { title, descriptionRaw, go, urlParams, designBuild } =
30 - useLaunchDataStore();
46 + const pluginUpdateNeeded = Boolean(window.extLaunchData?.pluginUpdateNeeded);
47 + const themeUpdateNeeded = Boolean(window.extLaunchData?.themeUpdateNeeded);
48 + const pluginUpdateViaRest = Boolean(
49 + window.extLaunchData?.pluginUpdateViaRest,
50 + );
51 + const launchUpdateNeeded = pluginUpdateNeeded || themeUpdateNeeded;
52 + const launchUpdateAttempt = window.extLaunchData?.launchUpdateAttempt ?? 0;
53 + const launchUpdateStale = window.extLaunchData?.launchUpdateStale ?? {};
54 + const launchUpdateGaveUp = Object.keys(launchUpdateStale).length > 0;
55 +
56 + const {
57 + title,
58 + descriptionRaw,
59 + go,
60 + urlParams,
61 + designBuild,
62 + setData,
63 + showExtendifyCodeScreen,
64 + } = useLaunchDataStore();
31 65 const skipDescription =
32 66 Boolean(urlParams?.['build-id']) ||
33 67 designBuild ||
34 68 ((title || descriptionRaw) && go);
69 + const showConnector = !skipDescription && showExtendifyCodeScreen;
35 70 const showExitLink =
36 71 !skipDescription && !window.extLaunchData?.hideAutoLaunchExitLink;
37 - const showTitle = getAbTest('AutoLaunch.ShowTitle').variant === 'B';
72 + const inMigrateVariant =
73 + getAbTest('AutoLaunch.MigrateScreen').variant === 'B';
74 + const [choosingMigration, setChoosingMigration] = useState(inMigrateVariant);
75 + const showMigrateChoice = !skipDescription && choosingMigration;
38 76
39 - const containerRef = useRef(null);
77 + // Deleting the old pages first leaves the user on a theme Launch cannot build on.
78 + const blocker = launchUpdateNeeded
79 + ? 'update'
80 + : needsTheme
81 + ? 'theme'
82 + : needsToReset
83 + ? 'reset'
84 + : null;
40 85
86 + // Only the describe screen commits to a build; every other screen can still leave.
87 + useInstallRequiredPlugins({
88 + enabled:
89 + !blocker && !skipDescription && !showConnector && !showMigrateChoice,
90 + });
91 +
41 92 useEffect(() => {
93 + if (launchUpdateNeeded) return;
94 + if (launchUpdateGaveUp) {
95 + digest({
96 + error: new Error('Launch went ahead without the pending update'),
97 + details: {
98 + source: 'auto-launch',
99 + caller: 'launch-update',
100 + stale: launchUpdateStale,
101 + },
102 + });
103 + }
42 104 // translators: Launch is a noun.
43 - document.title = __('Launch - AI-Powered Web Creation', 'extendify-local');
105 + document.title = launchStrings().documentTitle;
44 106 updateOption('extendify_launch_loaded', new Date().toISOString());
45 107 // We load core blocks so we can parse them
46 108 if (getBlockTypes().length === 0) registerCoreBlocks();
47 109
@@ -46,123 +108,213 @@
46 108 if (getBlockTypes().length === 0) registerCoreBlocks();
47 109
48 110 preLaunchFunctions();
49 111 checkIn({ stage: 'launch_page' });
50 - }, []);
112 + reportRestApiStatus();
113 + }, [launchUpdateNeeded, launchUpdateGaveUp]);
51 114
52 - if (needsTheme) {
53 - return (
54 - <Wrapper>
55 - <div className="bg-white w-full max-w-3xl rounded-lg border border-design-main/60 relative z-10">
56 - <NeedsTheme />
57 - </div>
58 - </Wrapper>
59 - );
60 - }
115 + const modal =
116 + blocker === 'update' ? (
117 + <UpdateScreen
118 + pluginUpdateNeeded={pluginUpdateNeeded}
119 + themeUpdateNeeded={themeUpdateNeeded}
120 + pluginUpdateViaRest={pluginUpdateViaRest}
121 + pluginSlug={window.extLaunchData?.pluginSlug}
122 + attempt={launchUpdateAttempt}
123 + />
124 + ) : blocker === 'theme' ? (
125 + <NeedsThemeScreen />
126 + ) : blocker === 'reset' ? (
127 + <RestartScreen pages={oldPages} />
128 + ) : null;
129 + // Site creation must not start behind a modal the user has not cleared.
130 + const screen = modal
131 + ? 'description'
132 + : showConnector
133 + ? 'connector'
134 + : showMigrateChoice
135 + ? 'migrate'
136 + : skipDescription
137 + ? 'creating'
138 + : 'description';
61 139
62 - if (needsToReset) {
63 - return (
64 - <Wrapper>
65 - <div className="w-full max-w-2xl rounded-3xl border bg-gray-100/80 backdrop-blur-2xl shadow-md relative z-10 border-gray-300">
66 - <RestartLaunchModal pages={oldPages} />
67 - </div>
68 - </Wrapper>
69 - );
70 - }
71 -
72 140 return (
73 - <Wrapper>
74 - <AnimatePresence mode="wait" initial={false}>
75 - <TheTitle skipDescription={skipDescription} />
76 - </AnimatePresence>
77 - <div ref={containerRef} className="w-full max-w-2xl relative z-10">
78 - <AnimatePresence mode="wait">
79 - <Launch
80 - key={skipDescription ? 'description-launch' : 'creating-launch'}
81 - skipDescription={skipDescription}
82 - lastHeight={containerRef.current?.offsetHeight}
83 - />
84 - </AnimatePresence>
85 - </div>
86 - {showExitLink && (
87 - <div
88 - className={classNames('flex w-full', {
89 - 'mt-6': showTitle,
90 - 'py-8 px-6 absolute bottom-0 left-0 z-10': !showTitle,
91 - })}
141 + <Viewport modal={modal} screen={screen}>
142 + {screen === 'creating' ? (
143 + <CreatingSite />
144 + ) : screen === 'connector' ? (
145 + <ConnectorScreen
146 + onProceed={() => setData('go', true)}
147 + footer={
148 + <BackLink
149 + onClick={() => setData('showExtendifyCodeScreen', false)}
150 + />
151 + }
152 + />
153 + ) : screen === 'migrate' ? (
154 + <MigrateScreen
155 + onBuildNew={() => setChoosingMigration(false)}
156 + footer={showExitLink ? <ExitLink /> : null}
157 + />
158 + ) : (
159 + <DescriptionPage
160 + heading={<TheTitle />}
161 + footer={showExitLink ? <ExitLink /> : null}
92 162 >
93 - <ExitLink />
94 - </div>
163 + {/* Matches the migrate screen's height, so choosing Build does not jump. */}
164 + <div className={classNames({ 'md:h-72.75': inMigrateVariant })}>
165 + <div className="mx-auto w-full">
166 + <DescriptionGathering autoFocus={!modal} />
167 + </div>
168 + </div>
169 + </DescriptionPage>
95 170 )}
96 - </Wrapper>
171 + </Viewport>
97 172 );
98 173 };
99 174
100 -const Wrapper = ({ children }) => {
101 - const { pulse } = useLaunchDataStore();
175 +const NeedsThemeScreen = () => {
176 + const { title, body, action } = useNeedsTheme();
177 + return (
178 + <ModalPage
179 + heading={<ModalHeading title={title} />}
180 + body={<ModalText>{body}</ModalText>}
181 + actions={<ModalLink href={action.href} label={action.label} />}
182 + />
183 + );
184 +};
102 185
186 +const UpdateScreen = (props) => <LaunchUpdate {...props} />;
187 +
188 +const RestartScreen = ({ pages }) => {
189 + const { title, body, note, exit, confirm } = useRestartLaunch({ pages });
103 190 return (
104 - <div style={{ zIndex: 99999 + 1 }} className="fixed inset-0 bg-white">
105 - <div className="relative h-dvh bg-banner-main text-banner-text text-base flex flex-col items-center justify-between">
106 - <div className="relative w-full flex flex-col items-center p-6 flex-1 min-h-0 overflow-y-auto">
107 - <div className="w-full flex flex-col items-center gap-5 md:gap-8 m-auto">
108 - <div className="mb-4">
109 - <Logo />
110 - </div>
111 - {children}
112 - </div>
113 - </div>
114 - </div>
115 - <MovingGradient />
116 - {pulse ? <ViewportPulse /> : null}
117 - </div>
191 + <ModalPage
192 + left
193 + heading={<ModalHeading title={title} />}
194 + body={
195 + <>
196 + <ModalText>{body}</ModalText>
197 + <ModalText strong>{note}</ModalText>
198 + </>
199 + }
200 + actions={
201 + <>
202 + <SecondaryButton
203 + disabled={confirm.processing}
204 + label={exit.label}
205 + onClick={exit.onClick}
206 + />
207 + <ActionButton {...confirm} />
208 + </>
209 + }
210 + />
118 211 );
119 212 };
120 213
121 -const ExitLink = () => {
214 +const ConnectorScreen = ({ onProceed, footer }) => {
215 + const { title, subtitle, options } = useExtendifyCodeConnector({ onProceed });
122 216 return (
123 - <a
124 - className="inline-flex items-center gap-0.5 text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity"
125 - href={window.extSharedData.adminUrl}
126 - onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
127 - >
128 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
129 - {__('WP Admin Dashboard', 'extendify-local')}
130 - </a>
217 + <ChoicePage
218 + heading={title && <Heading subtitle={subtitle} title={title} />}
219 + options={options}
220 + footer={footer}
221 + />
131 222 );
132 223 };
133 224
134 -const TheTitle = ({ skipDescription }) => {
135 - const useOldHeader =
136 - getAbTest('AutoLaunch.HeaderParagraphOld').variant === 'B';
137 - if (skipDescription) return null;
225 +const MigrateScreen = ({ onBuildNew, footer }) => {
226 + const { title, options } = useMigrateChoice({ onBuildNew });
227 + return (
228 + <ChoicePage
229 + heading={<Heading title={title} />}
230 + options={options}
231 + footer={footer}
232 + />
233 + );
234 +};
138 235
139 - const headingClass =
140 - 'text-xl md:text-2xl text-pretty text-banner-text font-semibold p-0 m-0 text-center';
141 - const transition = {
142 - animate: { opacity: 1 },
143 - exit: { opacity: 0 },
144 - transition: { duration: 0.4 },
236 +const Viewport = ({ children, screen, modal }) => (
237 + // Inline inset: utilities are !important here and would win over it.
238 + <div
239 + style={{ zIndex: 99999 + 1, top: 0, right: 0, bottom: 0, left: 0 }}
240 + className="group fixed bg-ui-page"
241 + >
242 + <AnimatePresence mode="wait" initial={false}>
243 + <motion.div
244 + key={screen}
245 + className="absolute inset-0"
246 + // React 18 drops a boolean inert; only a string reaches the DOM.
247 + inert={modal ? '' : undefined}
248 + initial={{ opacity: 0 }}
249 + animate={{ opacity: 1 }}
250 + exit={{ opacity: 0 }}
251 + transition={{ duration: 0.25 }}
252 + >
253 + {children}
254 + </motion.div>
255 + </AnimatePresence>
256 + {modal}
257 + <ShaderBackground />
258 + </div>
259 +);
260 +
261 +const DescriptionPage = ({ children, footer, heading }) => {
262 + const Page = activeDescriptionTemplate();
263 + const { partnerLogo, partnerName } = window.extSharedData ?? {};
264 + // A template cannot tell a heading that renders null from one that draws.
265 + const has = {
266 + heading: Boolean(heading),
267 + footer: Boolean(footer),
145 268 };
269 + const parts = {
270 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
271 + heading,
272 + content: children,
273 + footer,
274 + };
146 275
147 - if (useOldHeader) {
148 - return (
149 - <motion.div className="flex flex-col items-center gap-2" {...transition}>
150 - <h2 className={headingClass}>
151 - {__('Tell Us About Your Website', 'extendify-local')}
152 - </h2>
153 - <p className="text-sm md:text-base text-pretty text-banner-text opacity-70 p-0 m-0 text-center max-w-xl">
154 - {__(
155 - "Share your vision, and we'll craft a website that's perfectly tailored to your needs, ready to launch in no time.",
156 - 'extendify-local',
157 - )}
158 - </p>
159 - </motion.div>
160 - );
161 - }
276 + return <Page parts={parts} has={has} />;
277 +};
162 278
163 - return (
164 - <motion.h2 className={headingClass} {...transition}>
165 - {__('Describe the website you want to build', 'extendify-local')}
166 - </motion.h2>
167 - );
279 +const ModalPage = ({ heading, body, actions, left }) => {
280 + const Page = activeModalTemplate();
281 + const has = {
282 + heading: Boolean(heading),
283 + actions: Boolean(actions),
284 + card: true,
285 + left: Boolean(left),
286 + };
287 +
288 + return <Page parts={{ heading, body, actions }} has={has} />;
168 289 };
290 +
291 +const ChoicePage = ({ heading, options, footer }) => {
292 + const Page = activeChoiceTemplate();
293 + const { partnerLogo, partnerName } = window.extSharedData ?? {};
294 + const has = {
295 + heading: Boolean(heading),
296 + footer: Boolean(footer),
297 + };
298 + const parts = {
299 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
300 + heading,
301 + options,
302 + footer,
303 + };
304 +
305 + return <Page parts={parts} has={has} />;
306 +};
307 +
308 +const ExitLink = () => (
309 + <PageLink
310 + href={window.extSharedData.adminUrl}
311 + label={launchStrings().exitLink}
312 + onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
313 + />
314 +);
315 +
316 +const BackLink = ({ onClick }) => (
317 + <PageLink label={launchStrings().back} onClick={onClick} />
318 +);
319 +
320 +const TheTitle = () => <Heading title={launchStrings().heading} />;