Merged in SW-3396-move-my-saved-cards-to-design-system (pull request #2762)

SW-3396 move my saved cards to design system

* Move PaymentOption, PaymentOptionsGroup, PaymentIcons and MySavedCards (renamed SelectPaymentMethod) to design-system

* Remove unused svg payment icons

* cleanu

* cleanup

* trackUpdatePaymentMethod: remove hotelId argument that was never passed


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-09-04 13:01:36 +00:00
parent 8e00769c64
commit 6fa301f8e7
57 changed files with 1687 additions and 583 deletions

View File

@@ -13,13 +13,13 @@ import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMetho
import useSetOverflowVisibleOnRA from "@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA"
import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import MySavedCards from "@/components/HotelReservation/MySavedCards"
import PaymentOption from "@/components/HotelReservation/PaymentOption"
import PaymentOptionsGroup from "../../Payment/PaymentOptionsGroup"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
import styles from "./guarantee.module.css"
@@ -105,11 +105,19 @@ export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
</div>
</Checkbox>
{savedCreditCards?.length && guarantee ? (
<MySavedCards savedCreditCards={savedCreditCards} />
<SelectPaymentMethod
formName="paymentMethod"
onChange={(method) => trackUpdatePaymentMethod({ method })}
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.cardType as PaymentMethodEnum,
}))}
/>
) : null}
{guarantee ? (
<PaymentOptionsGroup
name="paymentMethod"
onChange={(method) => trackUpdatePaymentMethod({ method })}
label={
savedCreditCards?.length
? intl.formatMessage({

View File

@@ -8,7 +8,10 @@ import { Label } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import {
PAYMENT_METHOD_TITLES,
PaymentMethodEnum,
} from "@scandic-hotels/common/constants/paymentMethod"
import {
bookingConfirmation,
selectRate,
@@ -20,6 +23,8 @@ import { formatPhoneNumber } from "@scandic-hotels/common/utils/phone"
import Body from "@scandic-hotels/design-system/Body"
import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import { bedTypeMap } from "@scandic-hotels/trpc/constants/bedTypeMap"
@@ -27,15 +32,13 @@ import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { PAYMENT_METHOD_TITLES } from "@/constants/booking"
import { env } from "@/env/client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import PaymentOption from "@/components/HotelReservation/PaymentOption"
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import useLang from "@/hooks/useLang"
import { trackPaymentEvent } from "@/utils/tracking"
import { trackPaymentEvent, trackUpdatePaymentMethod } from "@/utils/tracking"
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
import ConfirmBooking, { ConfirmBookingRedemption } from "../Confirm"
@@ -50,7 +53,6 @@ import {
writePaymentInfoToSessionStorage,
} from "./helpers"
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
import PaymentOptionsGroup from "./PaymentOptionsGroup"
import { type PaymentFormData, paymentSchema } from "./schema"
import TermsAndConditions from "./TermsAndConditions"
@@ -551,6 +553,7 @@ export default function PaymentClient({
<PaymentOptionsGroup
name="paymentMethod"
className={styles.paymentOptionContainer}
onChange={(method) => trackUpdatePaymentMethod({ method })}
>
<Label className="sr-only">
{intl.formatMessage({
@@ -571,7 +574,7 @@ export default function PaymentClient({
{savedCreditCards.map((savedCreditCard) => (
<PaymentOption
key={savedCreditCard.id}
value={savedCreditCard.id}
value={savedCreditCard.id as PaymentMethodEnum}
label={
PAYMENT_METHOD_TITLES[
savedCreditCard.cardType as PaymentMethodEnum

View File

@@ -1,49 +0,0 @@
"use client"
import { Label, RadioGroup } from "react-aria-components"
import { useController, useFormContext } from "react-hook-form"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
import type { ReactNode } from "react"
interface PaymentOptionsGroupProps {
name: string
label?: string
children: ReactNode
className?: string
}
export default function PaymentOptionsGroup({
name,
label,
children,
className,
}: PaymentOptionsGroupProps) {
const { control } = useFormContext()
const {
field: { value, onChange },
} = useController({
name,
control,
})
const handleChange = (newValue: string) => {
onChange(newValue)
trackUpdatePaymentMethod("", newValue)
}
return (
<RadioGroup value={value} onChange={handleChange} className={className}>
{label ? (
<Typography variant="Title/Overline/sm">
<Label>{label}</Label>
</Typography>
) : null}
{children}
</RadioGroup>
)
}

View File

@@ -1,45 +0,0 @@
import { useIntl } from "react-intl"
import { PAYMENT_METHOD_TITLES } from "@/constants/booking"
import PaymentOptionsGroup from "../EnterDetails/Payment/PaymentOptionsGroup"
import PaymentOption from "../PaymentOption"
import styles from "./mySavedCards.module.css"
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface MySavedCardsProps {
savedCreditCards: CreditCard[] | null
}
export default function MySavedCards({ savedCreditCards }: MySavedCardsProps) {
const intl = useIntl()
const mySavedCardsLabel = intl.formatMessage({
defaultMessage: "MY SAVED CARDS",
})
return (
<section className={styles.section}>
<PaymentOptionsGroup
name="paymentMethod"
label={mySavedCardsLabel}
className={styles.paymentOptionContainer}
>
{savedCreditCards?.map((savedCreditCard) => (
<PaymentOption
key={savedCreditCard.id}
value={savedCreditCard.id}
label={
PAYMENT_METHOD_TITLES[
savedCreditCard.cardType as PaymentMethodEnum
]
}
cardNumber={savedCreditCard.truncatedNumber}
/>
))}
</PaymentOptionsGroup>
</section>
)
}

View File

@@ -1,11 +0,0 @@
.paymentOptionContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-one-and-half);
}
.section {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}

View File

@@ -6,6 +6,9 @@ import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMetho
import { dt } from "@scandic-hotels/common/dt"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/Link"
import { Typography } from "@scandic-hotels/design-system/Typography"
@@ -13,10 +16,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { bookingTermsAndConditions, privacyPolicy } from "@/constants/webHrefs"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
import MySavedCards from "@/components/HotelReservation/MySavedCards"
import PaymentOption from "@/components/HotelReservation/PaymentOption"
import useLang from "@/hooks/useLang"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
import styles from "./confirmationStep.module.css"
@@ -134,7 +135,16 @@ export default function ConfirmationStep({
})}
/>
{savedCreditCards?.length ? (
<MySavedCards savedCreditCards={savedCreditCards} />
<SelectPaymentMethod
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
formName={"paymentMethod"}
/>
) : null}
<PaymentOptionsGroup
name="paymentMethod"

View File

@@ -8,6 +8,9 @@ import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotel
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Divider } from "@scandic-hotels/design-system/Divider"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import Link from "@scandic-hotels/design-system/Link"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { toast } from "@scandic-hotels/design-system/Toast"
@@ -18,11 +21,9 @@ import { env } from "@/env/client"
import { useMyStayStore } from "@/stores/my-stay"
import { writeGlaToSessionStorage } from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/helpers"
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
import MySavedCards from "@/components/HotelReservation/MySavedCards"
import PaymentOption from "@/components/HotelReservation/PaymentOption"
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
import { type GuaranteeFormData, paymentSchema } from "./schema"
@@ -144,7 +145,16 @@ export default function Form() {
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
>
{savedCreditCards?.length ? (
<MySavedCards savedCreditCards={savedCreditCards} />
<SelectPaymentMethod
formName="paymentMethod"
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.type as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
/>
) : null}
<PaymentOptionsGroup
name="paymentMethod"
@@ -155,6 +165,9 @@ export default function Form() {
})
: undefined
}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
>
<PaymentOption
value={PaymentMethodEnum.card}

View File

@@ -1,55 +0,0 @@
import { cx } from "class-variance-authority"
import Image from "next/image"
import { Radio } from "react-aria-components"
import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption"
import { PAYMENT_METHOD_ICONS } from "@/constants/booking"
import styles from "./paymentOption.module.css"
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import type { PaymentOptionProps } from "./paymentOption"
export default function PaymentOption({
value,
label,
cardNumber,
}: PaymentOptionProps) {
return (
<Radio
value={value}
className={({ isFocusVisible }) =>
cx(styles.paymentOption, { [styles.focused]: isFocusVisible })
}
>
{({ isSelected }) => (
<>
<div className={styles.titleContainer}>
<span
className={cx(styles.radio, { [styles.selected]: isSelected })}
aria-hidden
/>
<Body>{label}</Body>
</div>
{cardNumber ? (
<>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Caption color="uiTextMediumContrast"> {cardNumber}</Caption>
</>
) : (
<Image
className={styles.paymentOptionIcon}
src={PAYMENT_METHOD_ICONS[value as PaymentMethodEnum]}
alt={label}
width={48}
height={32}
/>
)}
</>
)}
</Radio>
)
}

View File

@@ -1,41 +0,0 @@
.paymentOption {
position: relative;
background-color: var(--UI-Input-Controls-Surface-Normal);
padding: var(--Space-x15) var(--Space-x2);
border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-radius-md);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--Spacing-x2);
cursor: pointer;
}
.paymentOption.focused {
outline: 2px solid var(--UI-Input-Controls-Border-Focus);
outline-offset: 2px;
}
.radio {
width: 24px;
height: 24px;
border: 1px solid var(--Base-Border-Normal);
border-radius: 50%;
cursor: pointer;
}
.radio.selected {
border: 8px solid var(--Surface-UI-Fill-Active);
}
.titleContainer {
display: flex;
align-items: center;
gap: var(--Spacing-x-one-and-half);
}
.paymentOptionIcon {
position: absolute;
right: var(--Spacing-x3);
top: calc(50% - 16px);
}

View File

@@ -1,5 +0,0 @@
export interface PaymentOptionProps {
value: string
label: string
cardNumber?: string
}