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