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 +275 -83 3.0.63.2.1 View file →
@@ -1,20 +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';
12 +import { getAbTest } from '@auto-launch/functions/getAbTest';
13 +import { preLaunchFunctions } from '@auto-launch/functions/setup';
7 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';
8 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';
9 30 import { registerCoreBlocks } from '@wordpress/block-library';
10 31 import { getBlockTypes } from '@wordpress/blocks';
11 32 import { useSelect } from '@wordpress/data';
12 -import { useEffect, useRef } from '@wordpress/element';
13 -import { __ } from '@wordpress/i18n';
14 -import { chevronLeft, Icon } from '@wordpress/icons';
33 +import { useEffect, useState } from '@wordpress/element';
34 +import classNames from 'classnames';
15 35 import { AnimatePresence, motion } from 'framer-motion';
16 -import { checkIn } from './functions/insights';
36 +import { checkIn, reportRestApiStatus } from './functions/insights';
17 37
18 38 export const LaunchPage = () => {
19 39 const theme = useSelect((select) => select('core').getCurrentTheme());
20 40 // Checking `theme` here makes sure the data is populated
@@ -22,107 +42,279 @@
22 42
23 43 const oldPages = window.extLaunchData.resetSiteInformation.pagesIds ?? [];
24 44 const needsToReset = oldPages.length > 0;
25 45
26 - const { title, descriptionRaw, go, urlParams, designBuild } =
27 - 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();
28 65 const skipDescription =
29 66 Boolean(urlParams?.['build-id']) ||
30 67 designBuild ||
31 68 ((title || descriptionRaw) && go);
69 + const showConnector = !skipDescription && showExtendifyCodeScreen;
70 + const showExitLink =
71 + !skipDescription && !window.extLaunchData?.hideAutoLaunchExitLink;
72 + const inMigrateVariant =
73 + getAbTest('AutoLaunch.MigrateScreen').variant === 'B';
74 + const [choosingMigration, setChoosingMigration] = useState(inMigrateVariant);
75 + const showMigrateChoice = !skipDescription && choosingMigration;
32 76
33 - 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;
34 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 +
35 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 + }
36 104 // translators: Launch is a noun.
37 - document.title = __('Launch - AI-Powered Web Creation', 'extendify-local');
105 + document.title = launchStrings().documentTitle;
38 106 updateOption('extendify_launch_loaded', new Date().toISOString());
39 107 // We load core blocks so we can parse them
40 108 if (getBlockTypes().length === 0) registerCoreBlocks();
41 109
110 + preLaunchFunctions();
42 111 checkIn({ stage: 'launch_page' });
43 - }, []);
112 + reportRestApiStatus();
113 + }, [launchUpdateNeeded, launchUpdateGaveUp]);
44 114
45 - if (needsTheme) {
46 - return (
47 - <Wrapper>
48 - <div className="bg-white w-full max-w-3xl rounded-lg border border-design-main/60 relative z-10">
49 - <NeedsTheme />
50 - </div>
51 - </Wrapper>
52 - );
53 - }
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';
54 139
55 - if (needsToReset) {
56 - return (
57 - <Wrapper>
58 - <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">
59 - <RestartLaunchModal pages={oldPages} />
60 - </div>
61 - </Wrapper>
62 - );
63 - }
140 + return (
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}
162 + >
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>
170 + )}
171 + </Viewport>
172 + );
173 +};
64 174
175 +const NeedsThemeScreen = () => {
176 + const { title, body, action } = useNeedsTheme();
65 177 return (
66 - <Wrapper>
67 - <AnimatePresence mode="wait" initial={false}>
68 - <TheTitle skipDescription={skipDescription} />
69 - </AnimatePresence>
70 - <div ref={containerRef} className="w-full max-w-2xl relative z-10">
71 - <AnimatePresence mode="wait">
72 - <Launch
73 - key={skipDescription ? 'description-launch' : 'creating-launch'}
74 - skipDescription={skipDescription}
75 - lastHeight={containerRef.current?.offsetHeight}
76 - />
77 - </AnimatePresence>
78 - </div>
79 - {skipDescription ||
80 - window.extLaunchData?.hideAutoLaunchExitLink ? null : (
81 - <div className="flex w-full p-6 md:p-8 absolute bottom-0 left-0">
82 - <a
83 - className="inline-flex items-center gap-0.5 text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity p-2"
84 - href={window.extSharedData.adminUrl}
85 - onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
86 - >
87 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
88 - {__('WP Admin Dashboard', 'extendify-local')}
89 - </a>
90 - </div>
91 - )}
92 - </Wrapper>
178 + <ModalPage
179 + heading={<ModalHeading title={title} />}
180 + body={<ModalText>{body}</ModalText>}
181 + actions={<ModalLink href={action.href} label={action.label} />}
182 + />
93 183 );
94 184 };
95 185
96 -const Wrapper = ({ children }) => {
97 - const { pulse } = useLaunchDataStore();
186 +const UpdateScreen = (props) => <LaunchUpdate {...props} />;
98 187
188 +const RestartScreen = ({ pages }) => {
189 + const { title, body, note, exit, confirm } = useRestartLaunch({ pages });
99 190 return (
100 - <div style={{ zIndex: 99999 + 1 }} className="fixed inset-0 bg-white">
101 - <div className="relative h-dvh bg-banner-main text-banner-text text-base flex flex-col items-center justify-between">
102 - <div className="relative w-full flex flex-col items-center gap-5 md:gap-8 p-6 pb-25 flex-1 justify-center">
103 - <div className="mb-4">
104 - <Logo />
105 - </div>
106 - {children}
107 - </div>
108 - </div>
109 - <MovingGradient />
110 - {pulse ? <ViewportPulse /> : null}
111 - </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 + />
112 211 );
113 212 };
114 213
115 -const TheTitle = ({ skipDescription }) => {
116 - if (skipDescription) return null;
214 +const ConnectorScreen = ({ onProceed, footer }) => {
215 + const { title, subtitle, options } = useExtendifyCodeConnector({ onProceed });
216 + return (
217 + <ChoicePage
218 + heading={title && <Heading subtitle={subtitle} title={title} />}
219 + options={options}
220 + footer={footer}
221 + />
222 + );
223 +};
117 224
225 +const MigrateScreen = ({ onBuildNew, footer }) => {
226 + const { title, options } = useMigrateChoice({ onBuildNew });
118 227 return (
119 - <motion.h2
120 - className="text-xl md:text-2xl text-pretty text-banner-text font-semibold p-0 m-0 text-center"
121 - animate={{ opacity: 1 }}
122 - exit={{ opacity: 0 }}
123 - transition={{ duration: 0.4 }}
124 - >
125 - {__('Describe the website you want to build', 'extendify-local')}
126 - </motion.h2>
228 + <ChoicePage
229 + heading={<Heading title={title} />}
230 + options={options}
231 + footer={footer}
232 + />
127 233 );
128 234 };
235 +
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),
268 + };
269 + const parts = {
270 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
271 + heading,
272 + content: children,
273 + footer,
274 + };
275 +
276 + return <Page parts={parts} has={has} />;
277 +};
278 +
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} />;
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} />;