Merged in feat/sw-3514-join-card-membership-id (pull request #2967)

feat(SW-3514): Add variant of join friends card with membership id input

* Move membershipId input in enter details to join card

Add booking flow feature flag to move membershipId into join card and hide login button.
Currently only applies to first room.

* Add sas join card to multiroom


Approved-by: Hrishikesh Vaipurkar
This commit is contained in:
Anton Gunnarsson
2025-10-16 08:24:20 +00:00
parent b5817c5dcd
commit 611d81222c
11 changed files with 360 additions and 106 deletions

View File

@@ -1,26 +1,30 @@
"use client"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { membershipTermsAndConditions } from "@scandic-hotels/common/constants/routes/membershipTermsAndConditions"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Button } from "@scandic-hotels/design-system/Button"
import Footnote from "@scandic-hotels/design-system/Footnote"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import Link from "@scandic-hotels/design-system/Link"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import { useRoomContext } from "../../../../../contexts/EnterDetails/RoomContext"
import useLang from "../../../../../hooks/useLang"
import { MembershipNumberInput } from "../Signup/MembershipNumberInput"
import styles from "./partnerSASJoinScandicFriendsCard.module.css"
type Props = {
name?: string
updateDetailsStore?: () => void
}
export function PartnerSASJoinScandicFriendsCard({ name = "join" }: Props) {
export function PartnerSASJoinScandicFriendsCard({
name = "join",
updateDetailsStore,
}: Props) {
const lang = useLang()
const intl = useIntl()
const { data: euroBonusProfile } =
@@ -31,11 +35,13 @@ export function PartnerSASJoinScandicFriendsCard({ name = "join" }: Props) {
actions: { updateJoin },
} = useRoomContext()
const joinValue = useWatch({ name: "join" })
function onChange(event: { target: { value: boolean } }) {
updateJoin(event.target.value)
}
if (!euroBonusProfile || euroBonusProfile.linkStatus !== "UNLINKED") {
if (euroBonusProfile && euroBonusProfile.linkStatus !== "UNLINKED") {
return null
}
@@ -48,14 +54,9 @@ export function PartnerSASJoinScandicFriendsCard({ name = "join" }: Props) {
<Typography variant="Title/Subtitle/md">
<h2 className={styles.priceContainer}>
<span>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<>
NOT IMPLEMENTED
<br />
{intl.formatMessage({
defaultMessage: "Get the Scandic Friends room price",
})}
</>
{intl.formatMessage({
defaultMessage: "Get the member room price",
})}
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{`: `}
</span>
@@ -68,32 +69,26 @@ export function PartnerSASJoinScandicFriendsCard({ name = "join" }: Props) {
</span>
</h2>
</Typography>
<Checkbox
name={name}
className={styles.checkBox}
registerOptions={{ onChange }}
>
<Typography variant="Body/Paragraph/mdRegular">
<div>
{intl.formatMessage({
defaultMessage: "Join Scandic Friends now",
})}
</div>
</Typography>
</Checkbox>
<div className={styles.checkBox}>
<Checkbox name={name} registerOptions={{ onChange }}>
<Typography variant="Body/Paragraph/mdRegular">
<div>
{intl.formatMessage({
defaultMessage: "Join Scandic Friends now",
})}
</div>
</Typography>
</Checkbox>
<Button
variant={"Tertiary"}
size={"Small"}
typography={"Body/Paragraph/mdBold"}
color="Inverted"
className={styles.login}
onClick={() => toast.error("TODO: Not implemented")}
>
{intl.formatMessage({
defaultMessage: "Link",
})}
</Button>
<MembershipNumberInput
className={styles.membershipInput}
registerOptions={{ onBlur: updateDetailsStore }}
disabled={Boolean(joinValue)}
label={intl.formatMessage({
defaultMessage: "Already a member? Membership ID",
})}
/>
</div>
<div className={styles.terms}>
<Footnote color="uiTextPlaceholder">

View File

@@ -1,15 +1,14 @@
.cardContainer {
align-self: flex-start;
background-color: orchid;
color: white;
background-color: var(--Surface-Primary-Hover-Accent);
border-radius: var(--Corner-radius-lg);
display: grid;
gap: var(--Space-x2);
padding: var(--Space-x2);
grid-template-areas:
"price login"
"checkbox checkbox"
"terms terms";
"price"
"checkbox"
"terms";
width: min(100%, 696px);
}
@@ -22,15 +21,12 @@
color: var(--Text-Accent-Primary);
}
.login {
grid-area: login;
align-self: center;
justify-self: end;
}
.checkBox {
align-self: center;
grid-area: checkbox;
display: flex;
flex-direction: column;
gap: var(--Space-x2);
}
.terms {
@@ -39,12 +35,12 @@
@media screen and (min-width: 768px) {
.cardContainer {
grid-template-columns: 1fr auto auto;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: var(--Space-x3);
grid-template-areas:
"price checkbox login"
"terms terms terms";
"price checkbox"
"terms terms";
}
.priceContainer {

View File

@@ -0,0 +1,35 @@
import { useIntl } from "react-intl"
import BookingFlowInput from "../../../../BookingFlowInput"
import type { RegisterOptions } from "react-hook-form"
export function MembershipNumberInput({
registerOptions,
label,
className,
disabled,
}: {
registerOptions?: RegisterOptions
label?: string
className?: string
disabled?: boolean
}) {
const intl = useIntl()
return (
<BookingFlowInput
label={
label ||
intl.formatMessage({
defaultMessage: "Membership ID",
})
}
name="membershipNo"
type="tel"
registerOptions={registerOptions}
className={className}
disabled={disabled}
/>
)
}

View File

@@ -15,6 +15,7 @@ import { useBookingFlowConfig } from "../../../../../bookingFlowConfig/bookingFl
import useLang from "../../../../../hooks/useLang"
import BookingFlowInput from "../../../../BookingFlowInput"
import { getErrorMessage } from "../../../../BookingFlowInput/errors"
import { MembershipNumberInput } from "./MembershipNumberInput"
import styles from "./signup.module.css"
@@ -41,50 +42,46 @@ export default function Signup({
setIsJoinChecked(joinValue)
}, [joinValue])
return isJoinChecked ? (
<div className={styles.additionalFormData}>
<BookingFlowInput
name="zipCode"
label={intl.formatMessage({
defaultMessage: "Zip code",
})}
registerOptions={{ required: true, ...registerOptions }}
/>
<div className={styles.dateField}>
<header>
<Caption type="bold">
<span className={styles.required}>
{intl.formatMessage({
defaultMessage: "Birth date",
})}
</span>
</Caption>
</header>
<DateSelect
labels={{
day: intl.formatMessage({ defaultMessage: "Day" }),
month: intl.formatMessage({ defaultMessage: "Month" }),
year: intl.formatMessage({ defaultMessage: "Year" }),
errorMessage: getErrorMessage(
intl,
config.variant,
errors["dateOfBirth"]?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth"
if (isJoinChecked)
return (
<div className={styles.additionalFormData}>
<BookingFlowInput
name="zipCode"
label={intl.formatMessage({
defaultMessage: "Zip code",
})}
registerOptions={{ required: true, ...registerOptions }}
/>
<div className={styles.dateField}>
<header>
<Caption type="bold">
<span className={styles.required}>
{intl.formatMessage({
defaultMessage: "Birth date",
})}
</span>
</Caption>
</header>
<DateSelect
labels={{
day: intl.formatMessage({ defaultMessage: "Day" }),
month: intl.formatMessage({ defaultMessage: "Month" }),
year: intl.formatMessage({ defaultMessage: "Year" }),
errorMessage: getErrorMessage(
intl,
config.variant,
errors["dateOfBirth"]?.message?.toString()
),
}}
lang={lang}
name="dateOfBirth"
registerOptions={{ required: true, ...registerOptions }}
/>
</div>
</div>
</div>
) : (
<BookingFlowInput
label={intl.formatMessage({
defaultMessage: "Membership ID",
})}
name="membershipNo"
type="tel"
registerOptions={registerOptions}
/>
)
)
if (config.enterDetailsMembershipIdInputLocation === "join-card") return null
return <MembershipNumberInput registerOptions={registerOptions} />
}

View File

@@ -149,6 +149,9 @@ export default function Details({ user }: DetailsProps) {
setValue("phoneNumberCC", countryCode.toLowerCase())
}
}, [countryCode, setValue])
const showJoinCard = memberRate && !user
return (
<FormProvider {...methods}>
<form
@@ -156,15 +159,9 @@ export default function Details({ user }: DetailsProps) {
id={`${formID}-room-${roomNr}`}
onSubmit={methods.handleSubmit(onSubmit)}
>
{config.variant === "scandic" && (
<>{user || !memberRate ? null : <JoinScandicFriendsCard />}</>
)}
{config.variant === "partner-sas" && (
<>
{user || !memberRate ? null : <PartnerSASJoinScandicFriendsCard />}
</>
)}
{showJoinCard ? (
<JoinScandicCard updateDetailsStore={updateDetailsStore} />
) : null}
<div className={styles.container}>
<Footnote
color="uiTextHighContrast"
@@ -257,3 +254,26 @@ export default function Details({ user }: DetailsProps) {
</FormProvider>
)
}
function JoinScandicCard({
updateDetailsStore,
}: {
updateDetailsStore: () => void
}) {
const config = useBookingFlowConfig()
switch (config.enterDetailsMembershipIdInputLocation) {
case "form":
return <JoinScandicFriendsCard />
case "join-card":
return (
<PartnerSASJoinScandicFriendsCard
updateDetailsStore={updateDetailsStore}
/>
)
default:
const _exhaustiveCheck: never =
config.enterDetailsMembershipIdInputLocation
return null
}
}