| 1 |
import { CreatingSite } from '@auto-launch/components/CreatingSite'; |
| 2 |
import { DescriptionGathering } from '@auto-launch/components/DescriptionGathering'; |
| 3 |
import { LaunchUpdate } from '@auto-launch/components/LaunchUpdate'; |
| 4 |
import { Logo } from '@auto-launch/components/Logo'; |
| 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'; |
| 13 |
import { preLaunchFunctions } from '@auto-launch/functions/setup'; |
| 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'; |
| 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'; |
| 30 |
import { registerCoreBlocks } from '@wordpress/block-library'; |
| 31 |
import { getBlockTypes } from '@wordpress/blocks'; |
| 32 |
import { useSelect } from '@wordpress/data'; |
| 33 |
import { useEffect, useState } from '@wordpress/element'; |
| 34 |
import classNames from 'classnames'; |
| 35 |
import { AnimatePresence, motion } from 'framer-motion'; |
| 36 |
import { checkIn, reportRestApiStatus } from './functions/insights'; |
| 37 |
|
| 38 |
export const LaunchPage = () => { |
| 39 |
const theme = useSelect((select) => select('core').getCurrentTheme()); |
| 40 |
// Checking `theme` here makes sure the data is populated |
| 41 |
const needsTheme = theme && theme?.textdomain !== 'extendable'; |
| 42 |
|
| 43 |
const oldPages = window.extLaunchData.resetSiteInformation.pagesIds ?? []; |
| 44 |
const needsToReset = oldPages.length > 0; |
| 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 |
|
| 56 |
const { |
| 57 |
title, |
| 58 |
descriptionRaw, |
| 59 |
go, |
| 60 |
urlParams, |
| 61 |
designBuild, |
| 62 |
setData, |
| 63 |
showExtendifyCodeScreen, |
| 64 |
} = useLaunchDataStore(); |
| 65 |
const skipDescription = |
| 66 |
Boolean(urlParams?.['build-id']) || |
| 67 |
designBuild || |
| 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; |
| 76 |
|
| 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; |
| 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 |
|
| 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 |
} |
| 104 |
// translators: Launch is a noun. |
| 105 |
document.title = launchStrings().documentTitle; |
| 106 |
updateOption('extendify_launch_loaded', new Date().toISOString()); |
| 107 |
// We load core blocks so we can parse them |
| 108 |
if (getBlockTypes().length === 0) registerCoreBlocks(); |
| 109 |
|
| 110 |
preLaunchFunctions(); |
| 111 |
checkIn({ stage: 'launch_page' }); |
| 112 |
reportRestApiStatus(); |
| 113 |
}, [launchUpdateNeeded, launchUpdateGaveUp]); |
| 114 |
|
| 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'; |
| 139 |
|
| 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 |
}; |
| 174 |
|
| 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 |
}; |
| 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} |
| 206 |
/> |
| 207 |
<ActionButton {...confirm} /> |
| 208 |
</> |
| 209 |
} |
| 210 |
/> |
| 211 |
); |
| 212 |
}; |
| 213 |
|
| 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 |
}; |
| 224 |
|
| 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 |
}; |
| 235 |
|
| 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), |
| 268 |
}; |
| 269 |
const parts = { |
| 270 |
logo: <Logo name={partnerName} src={activeLogo() ?? partnerLogo} />, |
| 271 |
heading, |
| 272 |
content: children, |
| 273 |
footer, |
| 274 |
}; |
| 275 |
|
| 276 |
return <Page parts={parts} has={has} />; |
| 277 |
}; |
| 278 |
|
| 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} />; |
| 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} />; |
| 321 |
|