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 +243 -151 3.1.33.2.1 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} />;