fix: allow any type of phone number
This commit is contained in:
committed by
Michael Zetterberg
parent
79e669020a
commit
9580281421
@@ -119,6 +119,7 @@ export default async function DetailsPage(
|
|||||||
<EnterDetailsProvider
|
<EnterDetailsProvider
|
||||||
booking={booking}
|
booking={booking}
|
||||||
breakfastPackages={breakfastPackages}
|
breakfastPackages={breakfastPackages}
|
||||||
|
lang={lang}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
searchParamsStr={selectRoomParams.toString()}
|
searchParamsStr={selectRoomParams.toString()}
|
||||||
user={user}
|
user={user}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { getLocalizedLanguageOptions } from "@/constants/languages"
|
|||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
|
import DeprecatedPhone from "@/components/TempDesignSystem/Form/DeprecatedPhone"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
|
||||||
import Select from "@/components/TempDesignSystem/Form/Select"
|
import Select from "@/components/TempDesignSystem/Form/Select"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
@@ -87,7 +87,11 @@ export default function FormContent() {
|
|||||||
type="email"
|
type="email"
|
||||||
data-hj-suppress
|
data-hj-suppress
|
||||||
/>
|
/>
|
||||||
<Phone label={phoneNumber} name="phoneNumber" data-hj-suppress />
|
<DeprecatedPhone
|
||||||
|
label={phoneNumber}
|
||||||
|
name="phoneNumber"
|
||||||
|
data-hj-suppress
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
items={languageOptions}
|
items={languageOptions}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import Button from "@/components/TempDesignSystem/Button"
|
|||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||||
|
import DeprecatedPhone from "@/components/TempDesignSystem/Form/DeprecatedPhone"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
@@ -169,7 +169,7 @@ export default function SignupForm({ title }: SignUpFormProps) {
|
|||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
type="email"
|
type="email"
|
||||||
/>
|
/>
|
||||||
<Phone label={phoneNumber} name="phoneNumber" />
|
<DeprecatedPhone label={phoneNumber} name="phoneNumber" />
|
||||||
</section>
|
</section>
|
||||||
<section className={styles.password}>
|
<section className={styles.password}>
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||||
import { useCallback, useEffect, useMemo } from "react"
|
import { useCallback, useEffect, useMemo } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -52,6 +53,14 @@ export default function Details() {
|
|||||||
[idx, rooms]
|
[idx, rooms]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const initialPhoneNumber = initialData.phoneNumber
|
||||||
|
const parsedInitialPhoneNumber = initialPhoneNumber
|
||||||
|
? parsePhoneNumberFromString(initialPhoneNumber)
|
||||||
|
: undefined
|
||||||
|
let initialPhoneNumberCC = initialData.phoneNumberCC
|
||||||
|
if (parsedInitialPhoneNumber && !initialPhoneNumberCC) {
|
||||||
|
initialPhoneNumberCC = parsedInitialPhoneNumber.country ?? ""
|
||||||
|
}
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
countryCode: initialData.countryCode,
|
countryCode: initialData.countryCode,
|
||||||
@@ -60,7 +69,10 @@ export default function Details() {
|
|||||||
join: initialData.join,
|
join: initialData.join,
|
||||||
lastName: initialData.lastName,
|
lastName: initialData.lastName,
|
||||||
membershipNo: initialData.membershipNo,
|
membershipNo: initialData.membershipNo,
|
||||||
phoneNumber: initialData.phoneNumber,
|
phoneNumber: parsedInitialPhoneNumber?.isValid()
|
||||||
|
? parsedInitialPhoneNumber?.number
|
||||||
|
: initialPhoneNumber,
|
||||||
|
phoneNumberCC: initialPhoneNumberCC,
|
||||||
specialRequest: {
|
specialRequest: {
|
||||||
comment: room.specialRequest.comment,
|
comment: room.specialRequest.comment,
|
||||||
},
|
},
|
||||||
@@ -89,7 +101,7 @@ export default function Details() {
|
|||||||
}
|
}
|
||||||
}, [handleSubmit, isValid, setIncomplete, updateDetails])
|
}, [handleSubmit, isValid, setIncomplete, updateDetails])
|
||||||
|
|
||||||
useEffect(updateDetailsStore, [methods.formState.isValid, updateDetailsStore])
|
useEffect(updateDetailsStore, [updateDetailsStore])
|
||||||
|
|
||||||
// Trigger validation of the room manually when another room changes its data.
|
// Trigger validation of the room manually when another room changes its data.
|
||||||
// Only do it if the field has a value, to avoid error states before the user
|
// Only do it if the field has a value, to avoid error states before the user
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function getMultiroomDetailsSchema(
|
|||||||
multiroomErrors.PHONE_REQUIRED,
|
multiroomErrors.PHONE_REQUIRED,
|
||||||
multiroomErrors.PHONE_REQUESTED
|
multiroomErrors.PHONE_REQUESTED
|
||||||
),
|
),
|
||||||
|
phoneNumberCC: z.string(),
|
||||||
membershipNo: z
|
membershipNo: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||||
import { useCallback, useEffect } from "react"
|
import { useCallback, useEffect } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
@@ -10,8 +11,6 @@ import SpecialRequests from "@/components/HotelReservation/EnterDetails/Details/
|
|||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
||||||
import PhoneCountryCode from "@/components/TempDesignSystem/Form/Phone/CountryCode"
|
|
||||||
import PhoneNumber from "@/components/TempDesignSystem/Form/Phone/Number"
|
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import { useRoomContext } from "@/contexts/Details/Room"
|
import { useRoomContext } from "@/contexts/Details/Room"
|
||||||
|
|
||||||
@@ -45,6 +44,14 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
|
|
||||||
const memberRate = "member" in room.roomRate ? room.roomRate.member : null
|
const memberRate = "member" in room.roomRate ? room.roomRate.member : null
|
||||||
|
|
||||||
|
const initialPhoneNumber = user?.phoneNumber || initialData.phoneNumber
|
||||||
|
const parsedInitialPhoneNumber = initialPhoneNumber
|
||||||
|
? parsePhoneNumberFromString(initialPhoneNumber)
|
||||||
|
: undefined
|
||||||
|
let initialPhoneNumberCC = initialData.phoneNumberCC
|
||||||
|
if (parsedInitialPhoneNumber && !initialPhoneNumberCC) {
|
||||||
|
initialPhoneNumberCC = parsedInitialPhoneNumber.country ?? ""
|
||||||
|
}
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
countryCode: user?.address?.countryCode || initialData.countryCode,
|
countryCode: user?.address?.countryCode || initialData.countryCode,
|
||||||
@@ -55,7 +62,10 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
join: initialData.join,
|
join: initialData.join,
|
||||||
lastName: user?.lastName || initialData.lastName,
|
lastName: user?.lastName || initialData.lastName,
|
||||||
membershipNo: initialData.membershipNo,
|
membershipNo: initialData.membershipNo,
|
||||||
phoneNumber: user?.phoneNumber || initialData.phoneNumber,
|
phoneNumber: parsedInitialPhoneNumber?.isValid()
|
||||||
|
? parsedInitialPhoneNumber?.number
|
||||||
|
: initialPhoneNumber,
|
||||||
|
phoneNumberCC: initialPhoneNumberCC,
|
||||||
zipCode: "zipCode" in initialData ? initialData.zipCode : undefined,
|
zipCode: "zipCode" in initialData ? initialData.zipCode : undefined,
|
||||||
specialRequest: {
|
specialRequest: {
|
||||||
comment: room.specialRequest.comment,
|
comment: room.specialRequest.comment,
|
||||||
@@ -92,7 +102,7 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
}
|
}
|
||||||
}, [handleSubmit, isValid, onSubmit, setIncomplete])
|
}, [handleSubmit, isValid, onSubmit, setIncomplete])
|
||||||
|
|
||||||
useEffect(updateDetailsStore, [methods.formState.isValid, updateDetailsStore])
|
useEffect(updateDetailsStore, [updateDetailsStore])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
@@ -161,10 +171,6 @@ export default function Details({ user }: DetailsProps) {
|
|||||||
readOnly={!!user}
|
readOnly={!!user}
|
||||||
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
registerOptions={{ required: true, onBlur: updateDetailsStore }}
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<PhoneCountryCode />
|
|
||||||
<PhoneNumber />
|
|
||||||
</div>
|
|
||||||
{user ? null : (
|
{user ? null : (
|
||||||
<div className={styles.fullWidth}>
|
<div className={styles.fullWidth}>
|
||||||
<Signup
|
<Signup
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const baseDetailsSchema = z.object({
|
|||||||
roomOneErrors.PHONE_REQUIRED,
|
roomOneErrors.PHONE_REQUIRED,
|
||||||
roomOneErrors.PHONE_REQUESTED
|
roomOneErrors.PHONE_REQUESTED
|
||||||
),
|
),
|
||||||
|
phoneNumberCC: z.string(),
|
||||||
specialRequest: specialRequestSchema,
|
specialRequest: specialRequestSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ export const signedInDetailsSchema = z.object({
|
|||||||
lastName: z.string().default(""),
|
lastName: z.string().default(""),
|
||||||
membershipNo: z.string().default(""),
|
membershipNo: z.string().default(""),
|
||||||
phoneNumber: z.string().default(""),
|
phoneNumber: z.string().default(""),
|
||||||
|
phoneNumberCC: z.string().default(""),
|
||||||
join: z
|
join: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { cx } from "class-variance-authority"
|
import { cx } from "class-variance-authority"
|
||||||
|
import { type CountryCode, parsePhoneNumberFromString } from "libphonenumber-js"
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { Label } from "react-aria-components"
|
import { Label } from "react-aria-components"
|
||||||
@@ -389,6 +390,23 @@ export default function PaymentClient({
|
|||||||
} else {
|
} else {
|
||||||
rateCode = booking.rooms[idx].rateCode
|
rateCode = booking.rooms[idx].rateCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let phoneNumber = room.guest.phoneNumber
|
||||||
|
const phoneNumberCC =
|
||||||
|
room.guest.phoneNumberCC.toUpperCase() as CountryCode
|
||||||
|
let parsedPhonenumber
|
||||||
|
if (phoneNumberCC) {
|
||||||
|
parsedPhonenumber = parsePhoneNumberFromString(
|
||||||
|
phoneNumber,
|
||||||
|
phoneNumberCC
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parsedPhonenumber = parsePhoneNumberFromString(phoneNumber)
|
||||||
|
}
|
||||||
|
if (parsedPhonenumber?.isValid()) {
|
||||||
|
phoneNumber = parsedPhonenumber.number
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
adults: room.adults,
|
adults: room.adults,
|
||||||
bookingCode: room.roomRate.bookingCode,
|
bookingCode: room.roomRate.bookingCode,
|
||||||
@@ -403,7 +421,7 @@ export default function PaymentClient({
|
|||||||
firstName: room.guest.firstName,
|
firstName: room.guest.firstName,
|
||||||
lastName: room.guest.lastName,
|
lastName: room.guest.lastName,
|
||||||
membershipNumber: room.guest.membershipNo,
|
membershipNumber: room.guest.membershipNo,
|
||||||
phoneNumber: room.guest.phoneNumber,
|
phoneNumber,
|
||||||
// Only allowed for room one
|
// Only allowed for room one
|
||||||
...(idx === 0 && {
|
...(idx === 0 && {
|
||||||
dateOfBirth:
|
dateOfBirth:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useFormContext } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||||
|
import DeprecatedPhone from "@/components/TempDesignSystem/Form/DeprecatedPhone"
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||||
import Phone from "@/components/TempDesignSystem/Form/Phone"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
import styles from "./modifyContact.module.css"
|
import styles from "./modifyContact.module.css"
|
||||||
@@ -73,7 +73,7 @@ export default function ModifyContact({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<Phone
|
<DeprecatedPhone
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
defaultMessage: "Phone number",
|
defaultMessage: "Phone number",
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"use client"
|
||||||
|
import "react-international-phone/style.css"
|
||||||
|
|
||||||
|
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"
|
||||||
|
import { TextField } from "react-aria-components"
|
||||||
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
||||||
|
import {
|
||||||
|
CountrySelector,
|
||||||
|
DialCodePreview,
|
||||||
|
type ParsedCountry,
|
||||||
|
usePhoneInput,
|
||||||
|
} from "react-international-phone"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
import { getDefaultCountryFromLang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
|
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||||
|
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import styles from "./phone.module.css"
|
||||||
|
|
||||||
|
import type { ChangeEvent } from "react"
|
||||||
|
|
||||||
|
import type { PhoneProps } from "@/types/components/form/phone"
|
||||||
|
|
||||||
|
export default function Phone({
|
||||||
|
ariaLabel = "Phone number input",
|
||||||
|
className = "",
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
name = "phoneNumber",
|
||||||
|
placeholder = "",
|
||||||
|
readOnly = false,
|
||||||
|
registerOptions = {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}: PhoneProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const { control, setValue, trigger } = useFormContext()
|
||||||
|
const phone = useWatch({ name })
|
||||||
|
|
||||||
|
const { field, fieldState, formState } = useController({
|
||||||
|
control,
|
||||||
|
disabled,
|
||||||
|
name,
|
||||||
|
rules: registerOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultPhoneNumber = formState.defaultValues?.phoneNumber
|
||||||
|
|
||||||
|
// If defaultPhoneNumber exists and is valid, parse it to get the country code,
|
||||||
|
// otherwise set the default country from the lang.
|
||||||
|
const defaultCountry = isValidPhoneNumber(defaultPhoneNumber)
|
||||||
|
? parsePhoneNumber(defaultPhoneNumber).country?.toLowerCase()
|
||||||
|
: getDefaultCountryFromLang(lang)
|
||||||
|
|
||||||
|
const { country, handlePhoneValueChange, inputValue, setCountry } =
|
||||||
|
usePhoneInput({
|
||||||
|
defaultCountry,
|
||||||
|
disableDialCodeAndPrefix: true,
|
||||||
|
forceDialCode: true,
|
||||||
|
value: phone,
|
||||||
|
onChange: (value) => {
|
||||||
|
// If not checked trigger(name) forces validation on mount
|
||||||
|
// which shows error message before user even can see the form
|
||||||
|
if (value.inputValue) {
|
||||||
|
setValue(name, value.phone)
|
||||||
|
trigger(name)
|
||||||
|
} else {
|
||||||
|
setValue(name, "")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSelectCountry(value: ParsedCountry) {
|
||||||
|
setCountry(value.iso2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(evt: ChangeEvent<HTMLInputElement>) {
|
||||||
|
handlePhoneValueChange(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.phone} ${className}`}>
|
||||||
|
<CountrySelector
|
||||||
|
disabled={readOnly}
|
||||||
|
dropdownArrowClassName={styles.arrow}
|
||||||
|
flagClassName={styles.flag}
|
||||||
|
onSelect={handleSelectCountry}
|
||||||
|
preferredCountries={["de", "dk", "fi", "no", "se", "gb"]}
|
||||||
|
selectedCountry={country.iso2}
|
||||||
|
renderButtonWrapper={(props) => (
|
||||||
|
<button
|
||||||
|
{...props.rootProps}
|
||||||
|
className={styles.select}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
data-testid="country-selector"
|
||||||
|
>
|
||||||
|
<Label required={!!registerOptions.required} size="small">
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Country code",
|
||||||
|
})}
|
||||||
|
</Label>
|
||||||
|
<span className={styles.selectContainer}>
|
||||||
|
{props.children}
|
||||||
|
<Body asChild fontOnly>
|
||||||
|
<DialCodePreview
|
||||||
|
className={styles.dialCode}
|
||||||
|
dialCode={country.dialCode}
|
||||||
|
prefix="+"
|
||||||
|
/>
|
||||||
|
</Body>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="keyboard_arrow_down"
|
||||||
|
className={styles.chevron}
|
||||||
|
color="Icon/Default"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
defaultValue={field.value}
|
||||||
|
isDisabled={disabled ?? field.disabled}
|
||||||
|
isInvalid={fieldState.invalid}
|
||||||
|
isRequired={!!registerOptions?.required}
|
||||||
|
isReadOnly={readOnly}
|
||||||
|
name={field.name}
|
||||||
|
type="tel"
|
||||||
|
>
|
||||||
|
<AriaInputWithLabel
|
||||||
|
{...field}
|
||||||
|
// hack used since chrome does not respect autocomplete="off"
|
||||||
|
autoComplete="nope"
|
||||||
|
id={field.name}
|
||||||
|
label={label}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
readOnly={readOnly}
|
||||||
|
required={!!registerOptions.required}
|
||||||
|
type="tel"
|
||||||
|
value={inputValue}
|
||||||
|
/>
|
||||||
|
<ErrorMessage errors={formState.errors} name={field.name} />
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
.phone {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
|
||||||
|
--react-international-phone-background-color: var(--Main-Grey-White);
|
||||||
|
--react-international-phone-border-color: var(--Scandic-Beige-40);
|
||||||
|
--react-international-phone-dropdown-preferred-list-divider-color: var(
|
||||||
|
--Scandic-Brand-Pale-Peach
|
||||||
|
);
|
||||||
|
--react-international-phone-selected-dropdown-item-background-color: var(
|
||||||
|
--Scandic-Blue-00
|
||||||
|
);
|
||||||
|
--react-international-phone-text-color: var(--Main-Grey-100);
|
||||||
|
|
||||||
|
--react-international-phone-dropdown-preferred-list-divider-margin: 8px;
|
||||||
|
|
||||||
|
--react-international-phone-height: 60px;
|
||||||
|
--react-international-phone-dropdown-top: calc(
|
||||||
|
var(--react-international-phone-height) + var(--Spacing-x1)
|
||||||
|
);
|
||||||
|
--react-international-phone-dial-code-preview-font-size: var(
|
||||||
|
--typography-Body-Regular-fontSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 385px) {
|
||||||
|
.phone {
|
||||||
|
grid-template-columns: minmax(124px, 164px) 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone:has(.input:active, .input:focus) {
|
||||||
|
--react-international-phone-border-color: var(--Scandic-Blue-90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone :global(.react-international-phone-country-selector-dropdown) {
|
||||||
|
background: var(--Main-Grey-White);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.08);
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
outline: none;
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone
|
||||||
|
:global(.react-international-phone-country-selector-dropdown__list-item) {
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x1) var(--Spacing-x1)
|
||||||
|
var(--Spacing-x-one-and-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone
|
||||||
|
:global(.react-international-phone-country-selector-button__button-content) {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
align-content: center;
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border-color: var(--Scandic-Beige-40);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
height: 60px;
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||||
|
transition: border-color 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select[aria-expanded="true"] .chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectContainer {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border: none;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
height: 18px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
height: 18px;
|
||||||
|
margin: 0;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select .dialCode {
|
||||||
|
border: none;
|
||||||
|
color: var(--UI-Text-High-contrast);
|
||||||
|
line-height: 1;
|
||||||
|
justify-self: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { Select } from "@scandic-hotels/design-system/Select"
|
|
||||||
|
|
||||||
import { countryPhoneCodes } from "@/constants/countryPhoneCodes"
|
|
||||||
|
|
||||||
export default function PhoneCountryCode() {
|
|
||||||
const intl = useIntl()
|
|
||||||
const countries = Object.entries(countryPhoneCodes).map(
|
|
||||||
([countryName, countryCode]) => ({
|
|
||||||
label: countryName,
|
|
||||||
value: countryCode,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
console.log("length: ", countries.length)
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="phoneCountryCode"
|
|
||||||
label={intl.formatMessage({
|
|
||||||
defaultMessage: "Country code",
|
|
||||||
})}
|
|
||||||
items={countries}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
|
||||||
|
|
||||||
export default function PhoneNumber() {
|
|
||||||
const intl = useIntl()
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
label={intl.formatMessage({
|
|
||||||
defaultMessage: "Phone number",
|
|
||||||
})}
|
|
||||||
name="phoneNumber"
|
|
||||||
type="tel"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import "react-international-phone/style.css"
|
import "react-international-phone/style.css"
|
||||||
|
|
||||||
import {
|
|
||||||
isValidPhoneNumber,
|
|
||||||
parsePhoneNumberWithError,
|
|
||||||
} from "libphonenumber-js"
|
|
||||||
import { TextField } from "react-aria-components"
|
import { TextField } from "react-aria-components"
|
||||||
import { useController, useFormContext, useWatch } from "react-hook-form"
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
import {
|
import {
|
||||||
CountrySelector,
|
CountrySelector,
|
||||||
DialCodePreview,
|
DialCodePreview,
|
||||||
@@ -17,6 +13,8 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
import { getDefaultCountryFromLang } from "@/constants/languages"
|
||||||
|
|
||||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||||
import Label from "@/components/TempDesignSystem/Form/Label"
|
import Label from "@/components/TempDesignSystem/Form/Label"
|
||||||
@@ -25,17 +23,12 @@ import useLang from "@/hooks/useLang"
|
|||||||
|
|
||||||
import styles from "./phone.module.css"
|
import styles from "./phone.module.css"
|
||||||
|
|
||||||
import type { ChangeEvent } from "react"
|
import type { PhoneProps } from "@/types/components/form/phone"
|
||||||
|
|
||||||
import type {
|
|
||||||
LowerCaseCountryCode,
|
|
||||||
PhoneProps,
|
|
||||||
} from "@/types/components/form/phone"
|
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
export default function Phone({
|
export default function Phone({
|
||||||
ariaLabel = "Phone number input",
|
ariaLabel = "Phone number input",
|
||||||
className = "",
|
className = "",
|
||||||
|
countrySelectorName = "phoneNumberCC",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
label,
|
label,
|
||||||
name = "phoneNumber",
|
name = "phoneNumber",
|
||||||
@@ -47,48 +40,27 @@ export default function Phone({
|
|||||||
}: PhoneProps) {
|
}: PhoneProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const { control, setValue, trigger } = useFormContext()
|
const { formState, getFieldState, register, setValue } = useFormContext()
|
||||||
const phone = useWatch({ name })
|
const fieldState = getFieldState(name)
|
||||||
|
const [phoneNumber, phoneNumberCC] = useWatch({
|
||||||
|
name: [name, countrySelectorName],
|
||||||
|
}) as [string, string]
|
||||||
|
|
||||||
const { field, fieldState, formState } = useController({
|
const { country, setCountry } = usePhoneInput({
|
||||||
control,
|
defaultCountry: phoneNumberCC
|
||||||
disabled,
|
? phoneNumberCC
|
||||||
name,
|
: getDefaultCountryFromLang(lang),
|
||||||
rules: registerOptions,
|
|
||||||
})
|
|
||||||
|
|
||||||
const defaultPhoneNumber = formState.defaultValues?.phoneNumber ?? ""
|
|
||||||
|
|
||||||
// If defaultPhoneNumber exists and is valid, parse it to get the country code,
|
|
||||||
// otherwise set the default country from the lang.
|
|
||||||
const defaultCountry = isValidPhoneNumber(defaultPhoneNumber)
|
|
||||||
? parsePhoneNumberWithError(defaultPhoneNumber).country?.toLowerCase()
|
|
||||||
: getDefaultCountryFromLang(lang)
|
|
||||||
|
|
||||||
const { country, handlePhoneValueChange, inputValue, setCountry } =
|
|
||||||
usePhoneInput({
|
|
||||||
defaultCountry,
|
|
||||||
disableDialCodeAndPrefix: true,
|
|
||||||
forceDialCode: true,
|
|
||||||
value: phone,
|
|
||||||
onChange: (value) => {
|
|
||||||
// If not checked trigger(name) forces validation on mount
|
|
||||||
// which shows error message before user even can see the form
|
|
||||||
if (value.inputValue) {
|
|
||||||
setValue(name, value.phone)
|
|
||||||
trigger(name)
|
|
||||||
} else {
|
|
||||||
setValue(name, "")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSelectCountry(value: ParsedCountry) {
|
function handleSelectCountry(value: ParsedCountry) {
|
||||||
setCountry(value.iso2)
|
setCountry(value.iso2)
|
||||||
|
setValue(countrySelectorName, value.iso2, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
if (registerOptions.onBlur) {
|
||||||
|
registerOptions.onBlur(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(evt: ChangeEvent<HTMLInputElement>) {
|
|
||||||
handlePhoneValueChange(evt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,41 +106,24 @@ export default function Phone({
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
defaultValue={field.value}
|
isDisabled={disabled || registerOptions.disabled}
|
||||||
isDisabled={disabled ?? field.disabled}
|
|
||||||
isInvalid={fieldState.invalid}
|
isInvalid={fieldState.invalid}
|
||||||
isRequired={!!registerOptions?.required}
|
isRequired={!!registerOptions?.required}
|
||||||
isReadOnly={readOnly}
|
isReadOnly={readOnly}
|
||||||
name={field.name}
|
name={name}
|
||||||
type="tel"
|
type="tel"
|
||||||
|
value={phoneNumber}
|
||||||
>
|
>
|
||||||
<AriaInputWithLabel
|
<AriaInputWithLabel
|
||||||
{...field}
|
{...register(name, registerOptions)}
|
||||||
// hack used since chrome does not respect autocomplete="off"
|
autoComplete="tel-national"
|
||||||
autoComplete="nope"
|
|
||||||
id={field.name}
|
|
||||||
label={label}
|
label={label}
|
||||||
onChange={handleChange}
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
required={!!registerOptions.required}
|
|
||||||
type="tel"
|
type="tel"
|
||||||
value={inputValue}
|
|
||||||
/>
|
/>
|
||||||
<ErrorMessage errors={formState.errors} name={field.name} />
|
<ErrorMessage errors={formState.errors} name={name} />
|
||||||
</TextField>
|
</TextField>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultCountryFromLang(lang: Lang): LowerCaseCountryCode {
|
|
||||||
const countryMap: Record<Lang, LowerCaseCountryCode> = {
|
|
||||||
sv: "se",
|
|
||||||
da: "dk",
|
|
||||||
fi: "fi",
|
|
||||||
no: "no",
|
|
||||||
de: "de",
|
|
||||||
en: "se", // Default to Sweden for English
|
|
||||||
}
|
|
||||||
return countryMap[lang] || "se"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
export const countryPhoneCodes = {
|
|
||||||
Sweden: "+46",
|
|
||||||
Norway: "+47",
|
|
||||||
Denmark: "+45",
|
|
||||||
Finland: "+358",
|
|
||||||
Germany: "+49",
|
|
||||||
Afghanistan: "+93",
|
|
||||||
Albania: "+355",
|
|
||||||
Algeria: "+213",
|
|
||||||
"American Samoa": "+1684",
|
|
||||||
Andorra: "+376",
|
|
||||||
Angola: "+244",
|
|
||||||
Anguilla: "+1264",
|
|
||||||
Antarctica: "+672",
|
|
||||||
"Antigua and Barbuda": "+1268",
|
|
||||||
Argentina: "+54",
|
|
||||||
Armenia: "+374",
|
|
||||||
Aruba: "+297",
|
|
||||||
Australia: "+61",
|
|
||||||
Austria: "+43",
|
|
||||||
Azerbaijan: "+994",
|
|
||||||
Bahamas: "+1242",
|
|
||||||
Bahrain: "+973",
|
|
||||||
Bangladesh: "+880",
|
|
||||||
Barbados: "+1246",
|
|
||||||
Belarus: "+375",
|
|
||||||
Belgium: "+32",
|
|
||||||
Belize: "+501",
|
|
||||||
Benin: "+229",
|
|
||||||
Bermuda: "+1441",
|
|
||||||
Bhutan: "+975",
|
|
||||||
Bolivia: "+591",
|
|
||||||
Bonaire: "+599",
|
|
||||||
"Bosnia and Herzegovina": "+387",
|
|
||||||
Botswana: "+267",
|
|
||||||
"Bouvet Island": "+47",
|
|
||||||
Brazil: "+55",
|
|
||||||
"British Indian Ocean Territory": "+246",
|
|
||||||
"Brunei Darussalam": "+673",
|
|
||||||
Bulgaria: "+359",
|
|
||||||
"Burkina Faso": "+226",
|
|
||||||
Burundi: "+257",
|
|
||||||
Cambodia: "+855",
|
|
||||||
Cameroon: "+237",
|
|
||||||
Canada: "+1",
|
|
||||||
"Cape Verde": "+238",
|
|
||||||
"Cayman Islands": "+1345",
|
|
||||||
"Central African Republic": "+236",
|
|
||||||
Chad: "+235",
|
|
||||||
Chile: "+56",
|
|
||||||
China: "+86",
|
|
||||||
"Christmas Island": "+61",
|
|
||||||
"Cocos (Keeling) Islands": "+61",
|
|
||||||
Colombia: "+57",
|
|
||||||
Comoros: "+269",
|
|
||||||
Congo: "+242",
|
|
||||||
"Congo, The Democratic Republic of the": "+243",
|
|
||||||
"Cook Islands": "+682",
|
|
||||||
"Costa Rica": "+506",
|
|
||||||
"Côte d'Ivoire": "+225",
|
|
||||||
Croatia: "+385",
|
|
||||||
Cuba: "+53",
|
|
||||||
Curacao: "+599",
|
|
||||||
Cyprus: "+357",
|
|
||||||
"Czech Republic": "+420",
|
|
||||||
Djibouti: "+253",
|
|
||||||
Dominica: "+1767",
|
|
||||||
"Dominican Republic": "+1809",
|
|
||||||
Ecuador: "+593",
|
|
||||||
Egypt: "+20",
|
|
||||||
"El Salvador": "+503",
|
|
||||||
"Equatorial Guinea": "+240",
|
|
||||||
Eritrea: "+291",
|
|
||||||
Estonia: "+372",
|
|
||||||
Eswatini: "+268",
|
|
||||||
Ethiopia: "+251",
|
|
||||||
"Falkland Islands (Malvinas)": "+500",
|
|
||||||
"Faroe Islands": "+298",
|
|
||||||
Fiji: "+679",
|
|
||||||
France: "+33",
|
|
||||||
"French Guiana": "+594",
|
|
||||||
"French Polynesia": "+689",
|
|
||||||
"French Southern Territories": "+262",
|
|
||||||
Gabon: "+241",
|
|
||||||
Gambia: "+220",
|
|
||||||
Georgia: "+995",
|
|
||||||
Ghana: "+233",
|
|
||||||
Gibraltar: "+350",
|
|
||||||
Greece: "+30",
|
|
||||||
Greenland: "+299",
|
|
||||||
Grenada: "+1473",
|
|
||||||
Guadeloupe: "+590",
|
|
||||||
Guam: "+1671",
|
|
||||||
Guatemala: "+502",
|
|
||||||
Guernsey: "+44",
|
|
||||||
Guinea: "+224",
|
|
||||||
"Guinea-Bissau": "+245",
|
|
||||||
Guyana: "+592",
|
|
||||||
Haiti: "+509",
|
|
||||||
"Heard Island and Mcdonald Islands": "+61",
|
|
||||||
"Holy See (Vatican City State)": "+379",
|
|
||||||
Honduras: "+504",
|
|
||||||
"Hong Kong": "+852",
|
|
||||||
Hungary: "+36",
|
|
||||||
Iceland: "+354",
|
|
||||||
India: "+91",
|
|
||||||
Indonesia: "+62",
|
|
||||||
"Iran, Islamic Republic Of": "+98",
|
|
||||||
Iraq: "+964",
|
|
||||||
Ireland: "+353",
|
|
||||||
"Isle of Man": "+44",
|
|
||||||
Israel: "+972",
|
|
||||||
Italy: "+39",
|
|
||||||
Jamaica: "+1876",
|
|
||||||
Japan: "+81",
|
|
||||||
Jersey: "+44",
|
|
||||||
Jordan: "+962",
|
|
||||||
Kazakhstan: "+7",
|
|
||||||
Kenya: "+254",
|
|
||||||
Kiribati: "+686",
|
|
||||||
'Korea, Democratic People"S Republic of': "+850",
|
|
||||||
"Korea, Republic of": "+82",
|
|
||||||
Kuwait: "+965",
|
|
||||||
Kyrgyzstan: "+996",
|
|
||||||
Laos: "+856",
|
|
||||||
Latvia: "+371",
|
|
||||||
Lebanon: "+961",
|
|
||||||
Lesotho: "+266",
|
|
||||||
Liberia: "+231",
|
|
||||||
"Libyan Arab Jamahiriya": "+218",
|
|
||||||
Liechtenstein: "+423",
|
|
||||||
Lithuania: "+370",
|
|
||||||
Luxembourg: "+352",
|
|
||||||
Macao: "+853",
|
|
||||||
"Macedonia, The Former Yugoslav Republic of": "+389",
|
|
||||||
Madagascar: "+261",
|
|
||||||
Malawi: "+265",
|
|
||||||
Malaysia: "+60",
|
|
||||||
Maldives: "+960",
|
|
||||||
Mali: "+223",
|
|
||||||
Malta: "+356",
|
|
||||||
"Marshall Islands": "+692",
|
|
||||||
Martinique: "+596",
|
|
||||||
Mauritania: "+222",
|
|
||||||
Mauritius: "+230",
|
|
||||||
Mayotte: "+262",
|
|
||||||
Mexico: "+52",
|
|
||||||
"Micronesia, Federated States of": "+691",
|
|
||||||
"Moldova, Republic of": "+373",
|
|
||||||
Monaco: "+377",
|
|
||||||
Mongolia: "+976",
|
|
||||||
Montenegro: "+382",
|
|
||||||
Montserrat: "+1664",
|
|
||||||
Morocco: "+212",
|
|
||||||
Mozambique: "+258",
|
|
||||||
Myanmar: "+95",
|
|
||||||
Namibia: "+264",
|
|
||||||
Nauru: "+674",
|
|
||||||
Nepal: "+977",
|
|
||||||
Netherlands: "+31",
|
|
||||||
"Netherlands Antilles": "+599",
|
|
||||||
"New Caledonia": "+687",
|
|
||||||
"New Zealand": "+64",
|
|
||||||
Nicaragua: "+505",
|
|
||||||
Niger: "+227",
|
|
||||||
Nigeria: "+234",
|
|
||||||
Niue: "+683",
|
|
||||||
"Norfolk Island": "+672",
|
|
||||||
"Northern Mariana Islands": "+1670",
|
|
||||||
Oman: "+968",
|
|
||||||
Pakistan: "+92",
|
|
||||||
Palau: "+680",
|
|
||||||
Palestine: "+970",
|
|
||||||
Panama: "+507",
|
|
||||||
"Papua New Guinea": "+675",
|
|
||||||
Paraguay: "+595",
|
|
||||||
Peru: "+51",
|
|
||||||
Philippines: "+63",
|
|
||||||
Pitcairn: "+64",
|
|
||||||
Poland: "+48",
|
|
||||||
Portugal: "+351",
|
|
||||||
"Puerto Rico": "+1787",
|
|
||||||
Qatar: "+974",
|
|
||||||
RWANDA: "+250",
|
|
||||||
Reunion: "+262",
|
|
||||||
Romania: "+40",
|
|
||||||
"Russian Federation": "+7",
|
|
||||||
"Saint Barthelemy": "+590",
|
|
||||||
"Saint Helena": "+290",
|
|
||||||
"Saint Kitts and Nevis": "+1869",
|
|
||||||
"Saint Lucia": "+1758",
|
|
||||||
"Saint Martin": "+590",
|
|
||||||
"Saint Pierre and Miquelon": "+508",
|
|
||||||
"Saint Vincent and the Grenadines": "+1784",
|
|
||||||
Samoa: "+685",
|
|
||||||
"San Marino": "+378",
|
|
||||||
"Sao Tome and Principe": "+239",
|
|
||||||
"Saudi Arabia": "+966",
|
|
||||||
Senegal: "+221",
|
|
||||||
Serbia: "+381",
|
|
||||||
Seychelles: "+248",
|
|
||||||
"Sierra Leone": "+232",
|
|
||||||
Singapore: "+65",
|
|
||||||
"Sint Maarten": "+1721",
|
|
||||||
Slovakia: "+421",
|
|
||||||
Slovenia: "+386",
|
|
||||||
"Solomon Islands": "+677",
|
|
||||||
Somalia: "+252",
|
|
||||||
"South Africa": "+27",
|
|
||||||
"South Georgia and the South Sandwich Islands": "+500",
|
|
||||||
"South Sudan": "+211",
|
|
||||||
Spain: "+34",
|
|
||||||
"Sri Lanka": "+94",
|
|
||||||
Sudan: "+249",
|
|
||||||
Suriname: "+597",
|
|
||||||
"Svalbard and Jan Mayen": "+47",
|
|
||||||
Switzerland: "+41",
|
|
||||||
"Syrian Arab Republic": "+963",
|
|
||||||
Taiwan: "+886",
|
|
||||||
Tajikistan: "+992",
|
|
||||||
"Tanzania, United Republic of": "+255",
|
|
||||||
Thailand: "+66",
|
|
||||||
"Timor-Leste": "+670",
|
|
||||||
Togo: "+228",
|
|
||||||
Tokelau: "+690",
|
|
||||||
Tonga: "+676",
|
|
||||||
"Trinidad and Tobago": "+1868",
|
|
||||||
Tunisia: "+216",
|
|
||||||
Turkey: "+90",
|
|
||||||
Turkmenistan: "+993",
|
|
||||||
"Turks and Caicos Islands": "+1649",
|
|
||||||
Tuvalu: "+688",
|
|
||||||
Uganda: "+256",
|
|
||||||
Ukraine: "+380",
|
|
||||||
"United Arab Emirates": "+971",
|
|
||||||
"United Kingdom": "+44",
|
|
||||||
"United States": "+1",
|
|
||||||
"United States Minor Outlying Islands": "+1",
|
|
||||||
Uruguay: "+598",
|
|
||||||
Uzbekistan: "+998",
|
|
||||||
Vanuatu: "+678",
|
|
||||||
Venezuela: "+58",
|
|
||||||
Vietnam: "+84",
|
|
||||||
"Virgin Islands, British": "+1284",
|
|
||||||
"Virgin Islands, U.S.": "+1340",
|
|
||||||
"Wallis and Futuna": "+681",
|
|
||||||
"Western Sahara": "+212",
|
|
||||||
Yemen: "+967",
|
|
||||||
Zambia: "+260",
|
|
||||||
Zimbabwe: "+263",
|
|
||||||
"Åland Islands": "+358",
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { LowerCaseCountryCode } from "@/types/components/form/phone"
|
||||||
|
|
||||||
export enum Lang {
|
export enum Lang {
|
||||||
da = "da",
|
da = "da",
|
||||||
de = "de",
|
de = "de",
|
||||||
@@ -106,3 +108,15 @@ export function getLocalizedLanguageOptions(currentLang: Lang) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDefaultCountryFromLang(lang: Lang): LowerCaseCountryCode {
|
||||||
|
const countryMap: Record<Lang, LowerCaseCountryCode> = {
|
||||||
|
sv: "se",
|
||||||
|
da: "dk",
|
||||||
|
fi: "fi",
|
||||||
|
no: "no",
|
||||||
|
de: "de",
|
||||||
|
en: "se", // Default to Sweden for English
|
||||||
|
}
|
||||||
|
return countryMap[lang] || "se"
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function EnterDetailsProvider({
|
|||||||
booking,
|
booking,
|
||||||
breakfastPackages,
|
breakfastPackages,
|
||||||
children,
|
children,
|
||||||
|
lang,
|
||||||
rooms,
|
rooms,
|
||||||
searchParamsStr,
|
searchParamsStr,
|
||||||
user,
|
user,
|
||||||
@@ -73,7 +74,8 @@ export default function EnterDetailsProvider({
|
|||||||
initialData,
|
initialData,
|
||||||
searchParamsStr,
|
searchParamsStr,
|
||||||
user,
|
user,
|
||||||
breakfastPackages
|
breakfastPackages,
|
||||||
|
lang
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import isEqual from "fast-deep-equal"
|
import isEqual from "fast-deep-equal"
|
||||||
|
import { parsePhoneNumberFromString } from "libphonenumber-js"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sumPackages,
|
sumPackages,
|
||||||
@@ -16,6 +17,13 @@ import type { PersistedState, RoomState } from "@/types/stores/enter-details"
|
|||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
||||||
|
let phoneNumberCC = ""
|
||||||
|
if (user.phoneNumber) {
|
||||||
|
const parsedPhoneNumber = parsePhoneNumberFromString(user.phoneNumber)
|
||||||
|
if (parsedPhoneNumber?.country) {
|
||||||
|
phoneNumberCC = parsedPhoneNumber.country.toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
countryCode: user.address.countryCode?.toString(),
|
countryCode: user.address.countryCode?.toString(),
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@@ -24,6 +32,7 @@ export function extractGuestFromUser(user: NonNullable<SafeUser>) {
|
|||||||
join: false,
|
join: false,
|
||||||
membershipNo: user.membership?.membershipNumber,
|
membershipNo: user.membership?.membershipNumber,
|
||||||
phoneNumber: user.phoneNumber ?? "",
|
phoneNumber: user.phoneNumber ?? "",
|
||||||
|
phoneNumberCC,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useContext } from "react"
|
|||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
import { REDEMPTION } from "@/constants/booking"
|
||||||
|
import { getDefaultCountryFromLang } from "@/constants/languages"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +35,7 @@ import type {
|
|||||||
RoomState,
|
RoomState,
|
||||||
} from "@/types/stores/enter-details"
|
} from "@/types/stores/enter-details"
|
||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
import type { Lang } from "@/constants/routes/hotelReservation"
|
||||||
|
|
||||||
const defaultGuestState = {
|
const defaultGuestState = {
|
||||||
countryCode: "",
|
countryCode: "",
|
||||||
@@ -44,6 +46,7 @@ const defaultGuestState = {
|
|||||||
lastName: "",
|
lastName: "",
|
||||||
membershipNo: "",
|
membershipNo: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
phoneNumberCC: "",
|
||||||
zipCode: "",
|
zipCode: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +56,8 @@ export function createDetailsStore(
|
|||||||
initialState: InitialState,
|
initialState: InitialState,
|
||||||
searchParams: string,
|
searchParams: string,
|
||||||
user: SafeUser,
|
user: SafeUser,
|
||||||
breakfastPackages: BreakfastPackages
|
breakfastPackages: BreakfastPackages,
|
||||||
|
lang: Lang
|
||||||
) {
|
) {
|
||||||
const isMember = !!user
|
const isMember = !!user
|
||||||
const isRedemption =
|
const isRedemption =
|
||||||
@@ -328,6 +332,8 @@ export function createDetailsStore(
|
|||||||
currentRoom.guest.firstName = data.firstName
|
currentRoom.guest.firstName = data.firstName
|
||||||
currentRoom.guest.join = data.join
|
currentRoom.guest.join = data.join
|
||||||
currentRoom.guest.lastName = data.lastName
|
currentRoom.guest.lastName = data.lastName
|
||||||
|
currentRoom.guest.phoneNumber = data.phoneNumber
|
||||||
|
currentRoom.guest.phoneNumberCC = data.phoneNumberCC
|
||||||
|
|
||||||
if (data.specialRequest?.comment) {
|
if (data.specialRequest?.comment) {
|
||||||
currentRoom.specialRequest.comment =
|
currentRoom.specialRequest.comment =
|
||||||
@@ -339,7 +345,6 @@ export function createDetailsStore(
|
|||||||
} else {
|
} else {
|
||||||
currentRoom.guest.membershipNo = data.membershipNo
|
currentRoom.guest.membershipNo = data.membershipNo
|
||||||
}
|
}
|
||||||
currentRoom.guest.phoneNumber = data.phoneNumber
|
|
||||||
|
|
||||||
// Only valid for room 1
|
// Only valid for room 1
|
||||||
if (idx === 0 && data.join && !isMember) {
|
if (idx === 0 && data.join && !isMember) {
|
||||||
@@ -397,7 +402,10 @@ export function createDetailsStore(
|
|||||||
guest:
|
guest:
|
||||||
isMember && idx === 0
|
isMember && idx === 0
|
||||||
? deepmerge(defaultGuestState, extractGuestFromUser(user))
|
? deepmerge(defaultGuestState, extractGuestFromUser(user))
|
||||||
: defaultGuestState,
|
: {
|
||||||
|
...defaultGuestState,
|
||||||
|
phoneNumberCC: getDefaultCountryFromLang(lang),
|
||||||
|
},
|
||||||
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
|
roomPrice: getRoomPrice(room.roomRate, isMember && idx === 0),
|
||||||
specialRequest: {
|
specialRequest: {
|
||||||
comment: "",
|
comment: "",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type LowerCaseCountryCode = Lowercase<CountryCode>
|
|||||||
export interface PhoneProps {
|
export interface PhoneProps {
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
className?: string
|
className?: string
|
||||||
|
countrySelectorName?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
label: string
|
label: string
|
||||||
name?: string
|
name?: string
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { Room } from "@/types/providers/details/room"
|
import type { Room } from "@/types/providers/details/room"
|
||||||
import type { SafeUser } from "@/types/user"
|
import type { SafeUser } from "@/types/user"
|
||||||
|
import type { Lang } from "@/constants/routes/hotelReservation"
|
||||||
import type { BreakfastPackages } from "../components/hotelReservation/breakfast"
|
import type { BreakfastPackages } from "../components/hotelReservation/breakfast"
|
||||||
import type { DetailsBooking } from "../components/hotelReservation/enterDetails/details"
|
import type { DetailsBooking } from "../components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
export interface DetailsProviderProps extends React.PropsWithChildren {
|
export interface DetailsProviderProps extends React.PropsWithChildren {
|
||||||
booking: DetailsBooking
|
booking: DetailsBooking
|
||||||
breakfastPackages: BreakfastPackages
|
breakfastPackages: BreakfastPackages
|
||||||
|
lang: Lang
|
||||||
rooms: Room[]
|
rooms: Room[]
|
||||||
searchParamsStr: string
|
searchParamsStr: string
|
||||||
user: SafeUser
|
user: SafeUser
|
||||||
|
|||||||
@@ -1,87 +1,21 @@
|
|||||||
import {
|
|
||||||
isPossiblePhoneNumber,
|
|
||||||
ParseError,
|
|
||||||
parsePhoneNumberWithError,
|
|
||||||
validatePhoneNumberLength,
|
|
||||||
} from "libphonenumber-js"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
const enum ParseErrorMessage {
|
|
||||||
INVALID_COUNTRY = "INVALID_COUNTRY",
|
|
||||||
INVALID_LENGTH = "INVALID_LENGTH",
|
|
||||||
NOT_A_NUMBER = "NOT_A_NUMBER",
|
|
||||||
TOO_LONG = "TOO_LONG",
|
|
||||||
TOO_SHORT = "TOO_SHORT",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function phoneValidator(
|
export function phoneValidator(
|
||||||
msg = "Required field",
|
msg = "Required field",
|
||||||
invalidMsg = "Invalid type"
|
invalidMsg = "Invalid type"
|
||||||
) {
|
) {
|
||||||
return z
|
return z
|
||||||
.string({ invalid_type_error: invalidMsg, required_error: msg })
|
.string({ invalid_type_error: invalidMsg, required_error: msg })
|
||||||
.min(1, { message: msg })
|
.min(5, { message: "The number you have entered is too short" })
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
const containsAlphabeticChars = /[a-z]/gi.test(value)
|
||||||
const phoneNumber = parsePhoneNumberWithError(value)
|
if (containsAlphabeticChars) {
|
||||||
if (phoneNumber) {
|
|
||||||
if (isPossiblePhoneNumber(value, phoneNumber.country)) {
|
|
||||||
return validatePhoneNumberLength(value, phoneNumber.country)
|
|
||||||
} else {
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "Please enter a valid phone number",
|
message: "Please enter a valid phone number",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ParseError) {
|
|
||||||
/**
|
|
||||||
* Only setup for when we need proper validation,
|
|
||||||
* should probably move to .superRefine to be able
|
|
||||||
* to return different messages depending on error.
|
|
||||||
*/
|
|
||||||
switch (error.message) {
|
|
||||||
case ParseErrorMessage.INVALID_COUNTRY:
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message:
|
|
||||||
"The country selected and country code doesn't match",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case ParseErrorMessage.INVALID_LENGTH:
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Please enter a valid phone number",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case ParseErrorMessage.NOT_A_NUMBER:
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Please enter a number",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case ParseErrorMessage.TOO_LONG:
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "The number you have entered is too long",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case ParseErrorMessage.TOO_SHORT:
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "The number you have entered is too short",
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "The number you have entered is not valid",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user