feat(SW-3659): Use new input component * Use new input component * Update error formatter * Merged master into feat/use-new-input-component * Merged master into feat/use-new-input-component * Merge branch 'master' into feat/use-new-input-component * Merged master into feat/use-new-input-component * Update Input stories * Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component * Update Storybook logo * Add some new demo icon input story * Fix the clear content button position * Fix broken password input icon * Merged master into feat/use-new-input-component * Merged master into feat/use-new-input-component * Add aria-hidden to required asterisk * Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component * Merge branch 'master' into feat/use-new-input-component Approved-by: Bianca Widstam Approved-by: Matilda Landström
430 lines
15 KiB
TypeScript
430 lines
15 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 { 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 { 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 PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
|
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"
|
|
import {
|
|
privacy,
|
|
scandicFriends,
|
|
} from "@scandic-hotels/common/constants/routes/customerService"
|
|
|
|
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"
|
|
registerOptions={{ required: true }}
|
|
type="email"
|
|
/>
|
|
<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
|
|
/>
|
|
</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: false,
|
|
}}
|
|
>
|
|
{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>
|
|
|
|
{/*
|
|
This is a manual validation trigger workaround:
|
|
- The Controller component (which Input uses) doesn't re-render on submit,
|
|
which prevents automatic error display.
|
|
- Future fix requires Input component refactoring (out of scope for now).
|
|
*/}
|
|
{!methods.formState.isValid ? (
|
|
<Button
|
|
className={styles.signUpButton}
|
|
type="submit"
|
|
variant="Primary"
|
|
onPress={() => methods.trigger()}
|
|
typography="Body/Paragraph/mdBold"
|
|
data-testid="trigger-validation"
|
|
>
|
|
{signupButtonText}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
className={styles.signUpButton}
|
|
type="submit"
|
|
variant="Primary"
|
|
isDisabled={methods.formState.isSubmitting || signup.isPending}
|
|
typography="Body/Paragraph/mdBold"
|
|
data-testid="submit"
|
|
>
|
|
{signupButtonText}
|
|
</Button>
|
|
)}
|
|
</form>
|
|
</FormProvider>
|
|
</div>
|
|
)
|
|
}
|