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