hedgedoc/frontend/src/components/login-page/new-user/new-user-card.tsx
Erik Michelson 024a8ca0d8 fix(login): prevent default for new user confirmation form event
The new user form which is shown when a new user logs in via an external
auth provider did not include the event.preventDefault() statement.
This meant that on submitting the form there was a race-condition
between the JS code sending the data to the API and the browser
reloading the page. Chromium-based browsers seem to handle events before
handling page navigation, and because the form event initiates a page
navigation on a successful API response it worked. Firefox seems to call
the event handler and the page navigation in parallel, resulting in a
loop on the new user form.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2025-02-19 19:15:21 +01:00

129 lines
4.5 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FormEvent } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Card, Form } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { useAsync } from 'react-use'
import { cancelPendingUser, confirmPendingUser, getPendingUserInfo } from '../../../api/auth/pending-user'
import { useRouter } from 'next/navigation'
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
import { UsernameLabelField } from '../../common/fields/username-label-field'
import { DisplayNameField } from '../../common/fields/display-name-field'
import { ProfilePictureChoice, ProfilePictureSelectField } from '../../common/fields/profile-picture-select-field'
import { useOnInputChange } from '../../../hooks/common/use-on-input-change'
import { fetchAndSetUser } from '../utils/fetch-and-set-user'
/**
* The card where a new user can enter their user information.
*/
export const NewUserCard: React.FC = () => {
const router = useRouter()
const { showErrorNotification } = useUiNotifications()
const { value, error, loading } = useAsync(getPendingUserInfo, [])
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
const [pictureChoice, setPictureChoice] = useState(ProfilePictureChoice.FALLBACK)
const [isUsernameSubmittable, setIsUsernameSubmittable] = useState(false)
const [isDisplayNameSubmittable, setIsDisplayNameSubmittable] = useState(false)
const isSubmittable = useMemo(() => {
return isUsernameSubmittable && isDisplayNameSubmittable
}, [isUsernameSubmittable, isDisplayNameSubmittable])
const onChangeUsername = useOnInputChange(setUsername)
const onChangeDisplayName = useOnInputChange(setDisplayName)
const submitUserdata = useCallback(
(event: FormEvent) => {
event.preventDefault()
confirmPendingUser({
username,
displayName,
profilePicture: pictureChoice === ProfilePictureChoice.PROVIDER ? value?.photoUrl : undefined
})
.then(() => fetchAndSetUser())
.then(() => {
router.push('/')
})
.catch(showErrorNotification('login.welcome.error'))
},
[username, displayName, pictureChoice, router, showErrorNotification, value?.photoUrl]
)
const cancelUserCreation = useCallback(() => {
cancelPendingUser()
.catch(showErrorNotification('login.welcome.cancelError'))
.finally(() => {
router.push('/login')
})
}, [router, showErrorNotification])
useEffect(() => {
if (error) {
showErrorNotification('login.welcome.error')(error)
router.push('/login')
}
}, [error, router, showErrorNotification])
useEffect(() => {
if (!value) {
return
}
setUsername(value.username ?? '')
setDisplayName(value.displayName ?? '')
if (value.photoUrl) {
setPictureChoice(ProfilePictureChoice.PROVIDER)
}
}, [value])
if (!value && !loading) {
return null
}
return (
<Card>
<Card.Body>
{loading && <p>Loading...</p>}
<Card.Title>
{displayName !== '' ? (
<Trans i18nKey={'login.welcome.title'} values={{ name: displayName }} />
) : (
<Trans i18nKey={'login.welcome.titleFallback'} />
)}
</Card.Title>
<Trans i18nKey={'login.welcome.description'} />
<hr />
<Form onSubmit={submitUserdata} className={'d-flex flex-column gap-3'}>
<DisplayNameField
onChange={onChangeDisplayName}
value={displayName}
onValidityChange={setIsDisplayNameSubmittable}
/>
<UsernameLabelField
onChange={onChangeUsername}
value={username}
onValidityChange={setIsUsernameSubmittable}
/>
<ProfilePictureSelectField
onChange={setPictureChoice}
value={pictureChoice}
pictureUrl={value?.photoUrl}
username={username}
/>
<div className={'d-flex gap-3'}>
<Button variant={'secondary'} type={'button'} className={'w-50'} onClick={cancelUserCreation}>
<Trans i18nKey={'common.cancel'} />
</Button>
<Button variant={'success'} type={'submit'} className={'w-50'} disabled={!isSubmittable}>
<Trans i18nKey={'common.continue'} />
</Button>
</div>
</Form>
</Card.Body>
</Card>
)
}