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/LaunchPage.jsx +275 -82 3.0.4 → trunk 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,106 +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 ? null : (
80 - <div className="flex w-full p-6 md:p-8 absolute bottom-0 left-0">
81 - <a
82 - className="inline-flex items-center gap-0.5 text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity p-2"
83 - href={window.extSharedData.adminUrl}
84 - onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
85 - >
86 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
87 - {__('WP Admin Dashboard', 'extendify-local')}
88 - </a>
89 - </div>
90 - )}
91 - </Wrapper>
178 + <ModalPage
179 + heading={<ModalHeading title={title} />}
180 + body={<ModalText>{body}</ModalText>}
181 + actions={<ModalLink href={action.href} label={action.label} />}
182 + />
92 183 );
93 184 };
94 185
95 -const Wrapper = ({ children }) => {
96 - const { pulse } = useLaunchDataStore();
186 +const UpdateScreen = (props) => <LaunchUpdate {...props} />;
97 187
188 +const RestartScreen = ({ pages }) => {
189 + const { title, body, note, exit, confirm } = useRestartLaunch({ pages });
98 190 return (
99 - <div style={{ zIndex: 99999 + 1 }} className="fixed inset-0 bg-white">
100 - <div className="relative h-dvh bg-banner-main text-banner-text text-base flex flex-col items-center justify-between">
101 - <div className="relative w-full flex flex-col items-center gap-5 md:gap-8 p-6 pb-25 flex-1 justify-center">
102 - <div className="mb-4">
103 - <Logo />
104 - </div>
105 - {children}
106 - </div>
107 - </div>
108 - <MovingGradient />
109 - {pulse ? <ViewportPulse /> : null}
110 - </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 + />
111 211 );
112 212 };
113 213
114 -const TheTitle = ({ skipDescription }) => {
115 - 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 +};
116 224
225 +const MigrateScreen = ({ onBuildNew, footer }) => {
226 + const { title, options } = useMigrateChoice({ onBuildNew });
117 227 return (
118 - <motion.h2
119 - className="text-xl md:text-2xl text-pretty text-banner-text font-semibold p-0 m-0 text-center"
120 - animate={{ opacity: 1 }}
121 - exit={{ opacity: 0 }}
122 - transition={{ duration: 0.4 }}
123 - >
124 - {__('Describe the website you want to build', 'extendify-local')}
125 - </motion.h2>
228 + <ChoicePage
229 + heading={<Heading title={title} />}
230 + options={options}
231 + footer={footer}
232 + />
126 233 );
127 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} />;