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/LaunchPage.jsx +271 -100 3.1.0 → 3.2.2 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} />;