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 +243 -151 3.1.2 → trunk View file →
@@ -1,22 +1,37 @@
1 -import { ExtendifyCodeConnector } from '@auto-launch/components/ExtendifyCodeConnector';
2 -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';
3 4 import { Logo } from '@auto-launch/components/Logo';
4 -import { MigrateChoice } from '@auto-launch/components/MigrateChoice';
5 -import { MovingGradient } from '@auto-launch/components/MovingGradients';
6 -import { NeedsTheme } from '@auto-launch/components/NeedsTheme';
7 -import { RestartLaunchModal } from '@auto-launch/components/RestartLaunchModal';
8 -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';
9 12 import { getAbTest } from '@auto-launch/functions/getAbTest';
10 13 import { preLaunchFunctions } from '@auto-launch/functions/setup';
11 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';
12 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';
13 30 import { registerCoreBlocks } from '@wordpress/block-library';
14 31 import { getBlockTypes } from '@wordpress/blocks';
15 32 import { useSelect } from '@wordpress/data';
16 -import { useEffect, useRef, useState } from '@wordpress/element';
17 -import { __ } from '@wordpress/i18n';
18 -import { chevronLeft, Icon } from '@wordpress/icons';
33 +import { useEffect, useState } from '@wordpress/element';
19 34 import classNames from 'classnames';
20 35 import { AnimatePresence, motion } from 'framer-motion';
21 36 import { checkIn, reportRestApiStatus } from './functions/insights';
22 37
@@ -27,8 +42,18 @@
27 42
28 43 const oldPages = window.extLaunchData.resetSiteInformation.pagesIds ?? [];
29 44 const needsToReset = oldPages.length > 0;
30 45
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 +
31 56 const {
32 57 title,
33 58 descriptionRaw,
34 59 go,
@@ -48,13 +73,37 @@
48 73 getAbTest('AutoLaunch.MigrateScreen').variant === 'B';
49 74 const [choosingMigration, setChoosingMigration] = useState(inMigrateVariant);
50 75 const showMigrateChoice = !skipDescription && choosingMigration;
51 76
52 - 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;
53 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 +
54 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 + }
55 104 // translators: Launch is a noun.
56 - document.title = __('Launch - AI-Powered Web Creation', 'extendify-local');
105 + document.title = launchStrings().documentTitle;
57 106 updateOption('extendify_launch_loaded', new Date().toISOString());
58 107 // We load core blocks so we can parse them
59 108 if (getBlockTypes().length === 0) registerCoreBlocks();
60 109
@@ -60,169 +109,212 @@
60 109
61 110 preLaunchFunctions();
62 111 checkIn({ stage: 'launch_page' });
63 112 reportRestApiStatus();
64 - }, []);
113 + }, [launchUpdateNeeded, launchUpdateGaveUp]);
65 114
66 - if (needsTheme) {
67 - return (
68 - <Wrapper>
69 - <div className="bg-white w-full max-w-3xl rounded-lg border border-design-main/60 relative z-10">
70 - <NeedsTheme />
71 - </div>
72 - </Wrapper>
73 - );
74 - }
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';
75 139
76 - if (needsToReset) {
77 - return (
78 - <Wrapper>
79 - <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">
80 - <RestartLaunchModal pages={oldPages} />
81 - </div>
82 - </Wrapper>
83 - );
84 - }
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 +};
85 174
175 +const NeedsThemeScreen = () => {
176 + const { title, body, action } = useNeedsTheme();
86 177 return (
87 - <Wrapper
88 - footer={
89 - showConnector ? (
90 - <BackLink onClick={() => setData('showExtendifyCodeScreen', false)} />
91 - ) : showExitLink ? (
92 - <ExitLink />
93 - ) : null
94 - }
95 - >
96 - <AnimatePresence mode="wait" initial={false}>
97 - <TheTitle
98 - key={showMigrateChoice ? 'migrate' : 'description'}
99 - // The connector renders its own heading, so hide the shared one.
100 - hide={skipDescription || showConnector}
101 - migrate={showMigrateChoice}
102 - />
103 - </AnimatePresence>
104 - <div
105 - ref={containerRef}
106 - className={classNames('w-full relative z-10', {
107 - 'max-w-3xl': inMigrateVariant || showConnector,
108 - 'max-w-2xl': !inMigrateVariant && !showConnector,
109 - 'md:h-72.75': inMigrateVariant && !skipDescription,
110 - })}
111 - >
112 - <AnimatePresence mode="wait">
113 - {showConnector && (
114 - <ExtendifyCodeConnector
115 - key="extendify-code"
116 - onProceed={() => setData('go', true)}
117 - />
118 - )}
119 - {!showConnector && showMigrateChoice && (
120 - <MigrateChoice
121 - key="migrate-choice"
122 - onBuildNew={() => setChoosingMigration(false)}
123 - />
124 - )}
125 - {!showConnector && !showMigrateChoice && (
126 - <Launch
127 - key={skipDescription ? 'description-launch' : 'creating-launch'}
128 - skipDescription={skipDescription}
129 - lastHeight={containerRef.current?.offsetHeight}
130 - />
131 - )}
132 - </AnimatePresence>
133 - </div>
134 - </Wrapper>
178 + <ModalPage
179 + heading={<ModalHeading title={title} />}
180 + body={<ModalText>{body}</ModalText>}
181 + actions={<ModalLink href={action.href} label={action.label} />}
182 + />
135 183 );
136 184 };
137 185
138 -const Wrapper = ({ children, footer }) => {
139 - const { pulse } = useLaunchDataStore();
186 +const UpdateScreen = (props) => <LaunchUpdate {...props} />;
140 187
188 +const RestartScreen = ({ pages }) => {
189 + const { title, body, note, exit, confirm } = useRestartLaunch({ pages });
141 190 return (
142 - <div style={{ zIndex: 99999 + 1 }} className="fixed inset-0 bg-white">
143 - <div className="relative h-dvh bg-banner-main text-banner-text text-base overflow-y-auto">
144 - <div className="relative z-10 min-h-dvh w-full flex flex-col items-center justify-between p-6">
145 - <div className="w-full flex flex-col items-center gap-5 md:gap-8 m-auto">
146 - <div className="mb-4">
147 - <Logo />
148 - </div>
149 - {children}
150 - </div>
151 - {footer && <div className="w-full pt-8 shrink-0">{footer}</div>}
152 - </div>
153 - </div>
154 - <MovingGradient />
155 - {pulse ? <ViewportPulse /> : null}
156 - </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 + />
157 211 );
158 212 };
159 213
160 -const ExitLink = () => {
214 +const ConnectorScreen = ({ onProceed, footer }) => {
215 + const { title, subtitle, options } = useExtendifyCodeConnector({ onProceed });
161 216 return (
162 - <a
163 - className="inline-flex items-center gap-0.5 text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity"
164 - href={window.extSharedData.adminUrl}
165 - onClick={() => checkIn({ stage: 'exit_to_wp_admin' })}
166 - >
167 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
168 - {__('WP Admin Dashboard', 'extendify-local')}
169 - </a>
217 + <ChoicePage
218 + heading={title && <Heading subtitle={subtitle} title={title} />}
219 + options={options}
220 + footer={footer}
221 + />
170 222 );
171 223 };
172 224
173 -const BackLink = ({ onClick }) => {
225 +const MigrateScreen = ({ onBuildNew, footer }) => {
226 + const { title, options } = useMigrateChoice({ onBuildNew });
174 227 return (
175 - <button
176 - type="button"
177 - onClick={onClick}
178 - className="inline-flex items-center gap-0.5 border-0 bg-transparent cursor-pointer text-sm text-banner-text opacity-70 hover:opacity-100 transition-opacity"
179 - >
180 - <Icon fill="currentColor" icon={chevronLeft} size={20} />
181 - {__('Back', 'extendify-local')}
182 - </button>
228 + <ChoicePage
229 + heading={<Heading title={title} />}
230 + options={options}
231 + footer={footer}
232 + />
183 233 );
184 234 };
185 235
186 -const TheTitle = ({ hide, migrate }) => {
187 - const useOldHeader =
188 - getAbTest('AutoLaunch.HeaderParagraphOld').variant === 'B';
189 - if (hide) 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 +);
190 260
191 - const headingClass =
192 - 'text-xl md:text-2xl text-pretty text-banner-text font-semibold p-0 m-0 text-center';
193 - const transition = {
194 - animate: { opacity: 1 },
195 - exit: { opacity: 0 },
196 - 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),
197 268 };
269 + const parts = {
270 + logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />,
271 + heading,
272 + content: children,
273 + footer,
274 + };
198 275
199 - if (migrate) {
200 - return (
201 - <motion.h2 className={headingClass} {...transition}>
202 - {__('How would you like to start your website?', 'extendify-local')}
203 - </motion.h2>
204 - );
205 - }
276 + return <Page parts={parts} has={has} />;
277 +};
206 278
207 - if (useOldHeader) {
208 - return (
209 - <motion.div className="flex flex-col items-center gap-2" {...transition}>
210 - <h2 className={headingClass}>
211 - {__('Tell Us About Your Website', 'extendify-local')}
212 - </h2>
213 - <p className="text-sm md:text-base text-pretty text-banner-text opacity-70 p-0 m-0 text-center max-w-xl">
214 - {__(
215 - "Share your vision, and we'll craft a website that's perfectly tailored to your needs, ready to launch in no time.",
216 - 'extendify-local',
217 - )}
218 - </p>
219 - </motion.div>
220 - );
221 - }
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 + };
222 287
223 - return (
224 - <motion.h2 className={headingClass} {...transition}>
225 - {__('Describe the website you want to build', 'extendify-local')}
226 - </motion.h2>
227 - );
288 + return <Page parts={{ heading, body, actions }} has={has} />;
228 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} />;