Files
Anton Gunnarsson 66b7af877a Merged in fix/loy-514-fix-validation-tracking-in-signup-form (pull request #3445)
fix(LOY-514): Fix validation error tracking in SignupForm

* Fix issue with form submit handling

* Fix

* Remove browser validation

* Add automatic tracking of validatione rrors


Approved-by: Rasmus Langvad
Approved-by: Matilda Landström
2026-01-19 08:34:24 +00:00

407 lines
14 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { cx } from "class-variance-authority"
import { useRouter } from "next/navigation"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import {
privacy,
scandicFriends,
} from "@scandic-hotels/common/constants/routes/customerService"
import { logger } from "@scandic-hotels/common/logger"
import {
formatPhoneNumber,
getDefaultCountryFromLang,
} from "@scandic-hotels/common/utils/phone"
import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
import DateSelect from "@scandic-hotels/design-system/Form/Date"
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import Phone from "@scandic-hotels/design-system/Form/Phone"
import { PasswordInput } from "@scandic-hotels/design-system/PasswordInput"
import { TextLink } from "@scandic-hotels/design-system/TextLink"
import { TextLinkButton } from "@scandic-hotels/design-system/TextLinkButton"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useFormTracking } from "@scandic-hotels/tracking/useFormTracking"
import { trpc } from "@scandic-hotels/trpc/client"
import {
signupErrors,
type SignUpSchema,
signUpSchema,
} from "@scandic-hotels/trpc/routers/user/schemas"
import ProfilingConsentModalReadOnly from "@/components/MyPages/ProfilingConsent/Modal/ReadOnly"
import useLang from "@/hooks/useLang"
import { getFormattedCountryList } from "@/utils/countries"
import {
formatFormErrorMessage,
getErrorMessage,
} from "@/utils/getErrorMessage"
import { requestOpen } from "@/utils/profilingConsent"
import { trackLinkClick } from "@/utils/tracking/profilingConsent"
import styles from "./form.module.css"
interface SignUpFormProps {
title: string
enableProfileConsent?: boolean
}
export default function SignupForm({
title,
// Handled as a prop rather than a client env var due to limits in Netlify env var size.
enableProfileConsent = false,
}: SignUpFormProps) {
const intl = useIntl()
const router = useRouter()
const lang = useLang()
const signupButtonText = intl.formatMessage({
id: "signUp.joinNow",
defaultMessage: "Join now",
})
const signup = trpc.user.signup.useMutation({
onSuccess: (data) => {
if (data.success && data.redirectUrl) {
router.push(data.redirectUrl)
}
},
onError: (error) => {
if (error.data?.code === "CONFLICT") {
toast.error(
intl.formatMessage({
id: "signUp.accountExistsError",
defaultMessage:
"An account with this email already exists. Please try signing in instead.",
})
)
return
}
toast.error(
intl.formatMessage({
id: "errorMessage.somethingWentWrong",
defaultMessage: "Something went wrong!",
})
)
logger.error("Component Signup error:", error)
},
})
const methods = useForm<SignUpSchema>({
defaultValues: {
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
phoneNumberCC: getDefaultCountryFromLang(lang),
dateOfBirth: "",
address: {
countryCode: "",
zipCode: "",
},
password: "",
termsAccepted: false,
profilingConsent: false,
},
mode: "all",
criteriaMode: "all",
resolver: zodResolver(signUpSchema),
reValidateMode: "onChange",
shouldFocusError: true,
})
const {
control,
subscribe,
formState: { errors },
} = methods
const { trackFormSubmit } = useFormTracking("signup", subscribe, control)
async function onSubmit(data: SignUpSchema) {
const phoneNumber = formatPhoneNumber(data.phoneNumber, data.phoneNumberCC)
signup.mutate({ ...data, phoneNumber, language: lang })
trackFormSubmit()
}
function openPersonalizationModal() {
trackLinkClick({
position: "signup",
name: "read more about personalization at scandic",
})
requestOpen()
}
return (
<div className={styles.formWrapper}>
{enableProfileConsent && <ProfilingConsentModalReadOnly />}
{title ? (
<Typography variant="Title/md">
<h2>{title}</h2>
</Typography>
) : null}
<FormProvider {...methods}>
<form
className={styles.form}
id="register"
onSubmit={methods.handleSubmit(onSubmit)}
>
<section className={styles.userInfo}>
<div className={styles.container}>
<header>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
id: "signUp.contactInformation",
defaultMessage: "Contact information",
})}
</h3>
</Typography>
</header>
<div className={styles.nameInputs}>
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.firstName",
defaultMessage: "First name",
})}
name="firstName"
registerOptions={{ required: true }}
/>
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.lastName",
defaultMessage: "Last name",
})}
name="lastName"
registerOptions={{ required: true }}
/>
</div>
</div>
<div className={styles.dateField}>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "common.birthDate",
defaultMessage: "Birth date",
})}
</p>
</Typography>
<DateSelect
labels={{
day: intl.formatMessage({
id: "common.day",
defaultMessage: "Day",
}),
month: intl.formatMessage({
id: "common.month",
defaultMessage: "Month",
}),
year: intl.formatMessage({
id: "common.year",
defaultMessage: "Year",
}),
errorMessage: getErrorMessage(
intl,
errors.dateOfBirth?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth"
registerOptions={{ required: true }}
/>
</div>
<div className={cx(styles.container, styles.additional)}>
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.zipCode",
defaultMessage: "Zip code",
})}
name="address.zipCode"
registerOptions={{ required: true }}
/>
<CountrySelect
countries={getFormattedCountryList(intl)}
errorMessage={getErrorMessage(
intl,
errors.address?.countryCode?.message
)}
label={intl.formatMessage({
id: "common.country",
defaultMessage: "Country",
})}
lang={lang}
name="address.countryCode"
registerOptions={{ required: true }}
/>
<FormInput
errorFormatter={formatFormErrorMessage}
label={intl.formatMessage({
id: "common.emailAddress",
defaultMessage: "Email address",
})}
name="email"
type="email"
registerOptions={{ required: true }}
/>
<Phone
countryLabel={intl.formatMessage({
id: "common.countryCode",
defaultMessage: "Country code",
})}
countriesWithTranslatedName={getFormattedCountryList(intl)}
defaultCountryCode={getDefaultCountryFromLang(lang)}
errorMessage={getErrorMessage(
intl,
errors.phoneNumber?.message
)}
label={intl.formatMessage({
id: "common.phoneNumber",
defaultMessage: "Phone number",
})}
name="phoneNumber"
/>
</div>
</section>
<section className={styles.password}>
<header>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
id: "common.password",
defaultMessage: "Password",
})}
</h3>
</Typography>
</header>
<PasswordInput
name="password"
label={intl.formatMessage({
id: "common.password",
defaultMessage: "Password",
})}
isNewPassword
errorFormatter={formatFormErrorMessage}
/>
</section>
{enableProfileConsent && (
<section className={styles.personalization}>
<header>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
id: "signup.UnlockYourPersonalizedExperience",
defaultMessage: "Unlock your personalized experience!",
})}
</h3>
</Typography>
</header>
<Checkbox
name="profilingConsent"
registerOptions={{ required: true }}
>
{intl.formatMessage({
id: "signup.yesConsent",
defaultMessage:
"I consent to Scandic using my information to give me even more personalized travel inspiration and offers from Scandic and trusted Scandic Friends partners. This means Scandic may use information about my interactions with Scandic Friends partners, and share details of my interactions with Scandic with those partners, to make the experience even more relevant to me.",
})}
</Checkbox>
<TextLinkButton
typography="Link/sm"
color="Primary"
className={styles.personalizationButton}
onClick={openPersonalizationModal}
>
{intl.formatMessage({
id: "signup.ReadMoreAboutPersonalization",
defaultMessage: "Read more about personalization at Scandic",
})}
</TextLinkButton>
</section>
)}
<section className={styles.terms}>
<header>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
id: "signUp.termsAndConditions",
defaultMessage: "Terms and conditions",
})}
</h3>
</Typography>
</header>
<Checkbox
name="termsAccepted"
registerOptions={{ required: true }}
errorCodeMessages={{
[signupErrors.TERMS_REQUIRED]: intl.formatMessage({
id: "common.mustAcceptTermsError",
defaultMessage: "You must accept the terms and conditions",
}),
}}
>
{intl.formatMessage({
id: "signUp.iAccept",
defaultMessage: "I accept",
})}
</Checkbox>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
id: "signUp.termsAndConditionsDescription",
defaultMessage:
"By accepting the <termsAndConditionsLink>Terms and Conditions for Scandic Friends</termsAndConditionsLink> I understand that my personal data will be processed in accordance with <privacyPolicy>Scandic's Privacy Policy</privacyPolicy>.",
},
{
termsAndConditionsLink: (str) => (
<TextLink
color="Text/Interactive/Secondary"
target="_blank"
href={scandicFriends[lang]}
isInline
>
{str}
</TextLink>
),
privacyPolicy: (str) => (
<TextLink
color="Text/Interactive/Secondary"
target="_blank"
href={privacy[lang]}
isInline
>
{str}
</TextLink>
),
}
)}
</p>
</Typography>
</section>
<Button
className={styles.signUpButton}
type="submit"
variant="Primary"
isDisabled={methods.formState.isSubmitting || signup.isPending}
data-testid="submit"
>
{signupButtonText}
</Button>
</form>
</FormProvider>
</div>
)
}