Merged in feat/SW-588-payment-saved-card (pull request #697)
feat(SW-588): Added saved card to payment step * feat(SW-588): Added saved card to payment step * feat(SW-588): Add proper label for saved card * feat(SW-588): fix from PR feedback * feat(SW-588): Add preloading of data * feat(SW-588): remove onChange logic for PaymentOption * feat(SW-588): moved payment files to correct folder * feat(SW-588): moved preload to layout * fix: remove unused prop Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import Image from "next/image"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
|
||||
import { PAYMENT_METHOD_ICONS, PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import { PaymentOptionProps } from "./paymentOption"
|
||||
|
||||
import styles from "./paymentOption.module.css"
|
||||
|
||||
export default function PaymentOption({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
cardNumber,
|
||||
registerOptions = {},
|
||||
}: PaymentOptionProps) {
|
||||
const { register } = useFormContext()
|
||||
|
||||
return (
|
||||
<label key={value} className={styles.paymentOption}>
|
||||
<div className={styles.titleContainer}>
|
||||
<input
|
||||
aria-hidden
|
||||
hidden
|
||||
type="radio"
|
||||
id={value}
|
||||
value={value}
|
||||
{...register(name, registerOptions)}
|
||||
/>
|
||||
<span className={styles.radio} />
|
||||
<Body>{label}</Body>
|
||||
</div>
|
||||
{cardNumber ? (
|
||||
<Caption color="uiTextMediumContrast">•••• {cardNumber}</Caption>
|
||||
) : (
|
||||
<Image
|
||||
className={styles.paymentOptionIcon}
|
||||
src={PAYMENT_METHOD_ICONS[value as PaymentMethodEnum]}
|
||||
alt={label}
|
||||
width={48}
|
||||
height={32}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RegisterOptions } from "react-hook-form"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
export interface PaymentOptionProps {
|
||||
name: string
|
||||
value: PaymentMethodEnum
|
||||
value: string
|
||||
label: string
|
||||
cardNumber?: string
|
||||
registerOptions?: RegisterOptions
|
||||
onChange?: () => void
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Checkbox from "@/components/TempDesignSystem/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
@@ -38,7 +39,15 @@ import { PaymentProps } from "@/types/components/hotelReservation/selectRate/sec
|
||||
const maxRetries = 40
|
||||
const retryInterval = 2000
|
||||
|
||||
export default function Payment({ hotel }: PaymentProps) {
|
||||
function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum)
|
||||
}
|
||||
|
||||
export default function Payment({
|
||||
hotelId,
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
}: PaymentProps) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
@@ -46,7 +55,9 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
|
||||
const methods = useForm<PaymentFormData>({
|
||||
defaultValues: {
|
||||
paymentMethod: PaymentMethodEnum.card,
|
||||
paymentMethod: savedCreditCards?.length
|
||||
? savedCreditCards[0].id
|
||||
: PaymentMethodEnum.card,
|
||||
smsConfirmation: false,
|
||||
termsAndConditions: false,
|
||||
},
|
||||
@@ -87,8 +98,17 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
}, [confirmationNumber, bookingStatus, router])
|
||||
|
||||
function handleSubmit(data: PaymentFormData) {
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
|
||||
const savedCreditCard = savedCreditCards?.find(
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
|
||||
initiateBooking.mutate({
|
||||
hotelId: hotel.operaId,
|
||||
hotelId: hotelId,
|
||||
checkInDate: "2024-12-10",
|
||||
checkOutDate: "2024-12-11",
|
||||
rooms: [
|
||||
@@ -116,7 +136,14 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
},
|
||||
],
|
||||
payment: {
|
||||
paymentMethod: data.paymentMethod,
|
||||
paymentMethod,
|
||||
card: savedCreditCard
|
||||
? {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
}
|
||||
: undefined,
|
||||
cardHolder: {
|
||||
email: "test.user@scandichotels.com",
|
||||
name: "Test User",
|
||||
@@ -143,65 +170,94 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
className={styles.paymentContainer}
|
||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
{hotel.merchantInformationData.alternatePaymentOptions.map(
|
||||
(paymentMethod) => (
|
||||
{savedCreditCards?.length ? (
|
||||
<section className={styles.section}>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "MY SAVED CARDS" })}
|
||||
</Body>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
{savedCreditCards?.map((savedCreditCard) => (
|
||||
<PaymentOption
|
||||
key={savedCreditCard.id}
|
||||
name="paymentMethod"
|
||||
value={savedCreditCard.id}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
savedCreditCard.cardType as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
cardNumber={savedCreditCard.truncatedNumber}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<section className={styles.section}>
|
||||
{savedCreditCards?.length ? (
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
||||
</Body>
|
||||
) : null}
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
{otherPaymentOptions.map((paymentMethod) => (
|
||||
<PaymentOption
|
||||
key={paymentMethod}
|
||||
name="paymentMethod"
|
||||
value={paymentMethod as PaymentMethodEnum}
|
||||
value={paymentMethod}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.section}>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
|
||||
<AriaLabel className={styles.terms}>
|
||||
<Checkbox name="termsAndConditions" />
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "booking.terms",
|
||||
},
|
||||
{
|
||||
termsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</AriaLabel>
|
||||
<AriaLabel className={styles.terms}>
|
||||
<Checkbox name="termsAndConditions" />
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "booking.terms",
|
||||
},
|
||||
{
|
||||
termsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</AriaLabel>
|
||||
</section>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.submitButton}
|
||||
@@ -1,10 +1,16 @@
|
||||
.paymentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
gap: var(--Spacing-x4);
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.paymentOptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1,9 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.nativeEnum(PaymentMethodEnum),
|
||||
paymentMethod: z.string(),
|
||||
smsConfirmation: z.boolean(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
|
||||
import { PAYMENT_METHOD_ICONS } from "@/constants/booking"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { PaymentOptionProps } from "./paymentOption"
|
||||
|
||||
import styles from "./paymentOption.module.css"
|
||||
|
||||
export default function PaymentOption({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
}: PaymentOptionProps) {
|
||||
const { register } = useFormContext()
|
||||
return (
|
||||
<label key={value} className={styles.paymentOption} htmlFor={value}>
|
||||
<div className={styles.titleContainer}>
|
||||
<input
|
||||
aria-hidden
|
||||
hidden
|
||||
type="radio"
|
||||
id={value}
|
||||
value={value}
|
||||
{...register(name)}
|
||||
/>
|
||||
<span className={styles.radio} />
|
||||
<Body asChild>
|
||||
<label htmlFor={value}>{label}</label>
|
||||
</Body>
|
||||
</div>
|
||||
<Image
|
||||
className={styles.paymentOptionIcon}
|
||||
src={PAYMENT_METHOD_ICONS[value]}
|
||||
alt={label}
|
||||
width={48}
|
||||
height={32}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user