Merge remote-tracking branch 'origin/develop' into feat/SW-415-select-room-card

This commit is contained in:
Pontus Dreij
2024-10-10 14:45:35 +02:00
37 changed files with 806 additions and 91 deletions

View File

@@ -0,0 +1,43 @@
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>
)
}

View File

@@ -0,0 +1,37 @@
.paymentOption {
position: relative;
background-color: var(--UI-Input-Controls-Surface-Normal);
padding: var(--Spacing-x3);
border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-radius-Medium);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--Spacing-x2);
cursor: pointer;
}
.paymentOption .radio {
width: 24px;
height: 24px;
border: 1px solid var(--Base-Border-Normal);
border-radius: 50%;
cursor: pointer;
}
.paymentOption input:checked + .radio {
border: 8px solid var(--UI-Input-Controls-Fill-Selected);
}
.titleContainer {
display: flex;
align-items: center;
gap: var(--Spacing-x-one-and-half);
pointer-events: none;
}
.paymentOptionIcon {
position: absolute;
right: var(--Spacing-x3);
top: calc(50% - 16px);
}

View File

@@ -0,0 +1,10 @@
import { RegisterOptions } from "react-hook-form"
import { PaymentMethodEnum } from "@/constants/booking"
export interface PaymentOptionProps {
name: string
value: PaymentMethodEnum
label: string
registerOptions?: RegisterOptions
}

View File

@@ -1,21 +1,36 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { Label as AriaLabel } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum,
PAYMENT_METHOD_TITLES,
PaymentMethodEnum,
} from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner"
import Button from "@/components/TempDesignSystem/Button"
import Checkbox from "@/components/TempDesignSystem/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { toast } from "@/components/TempDesignSystem/Toasts"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import useLang from "@/hooks/useLang"
import PaymentOption from "./PaymentOption"
import { PaymentFormData, paymentSchema } from "./schema"
import styles from "./payment.module.css"
import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section"
@@ -28,7 +43,17 @@ export default function Payment({ hotel }: PaymentProps) {
const lang = useLang()
const intl = useIntl()
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>("")
const methods = useForm<PaymentFormData>({
defaultValues: {
paymentMethod: PaymentMethodEnum.card,
smsConfirmation: false,
termsAndConditions: false,
},
mode: "all",
reValidateMode: "onChange",
resolver: zodResolver(paymentSchema),
})
const initiateBooking = trpc.booking.booking.create.useMutation({
onSuccess: (result) => {
@@ -38,7 +63,6 @@ export default function Payment({ hotel }: PaymentProps) {
BOOKING_CONFIRMATION_NUMBER,
result.confirmationNumber
)
setConfirmationNumber(result.confirmationNumber)
} else {
// TODO: add proper error message
@@ -60,12 +84,12 @@ export default function Payment({ hotel }: PaymentProps) {
)
useEffect(() => {
if (bookingStatus?.data?.paymentUrl) {
if (confirmationNumber && bookingStatus?.data?.paymentUrl) {
router.push(bookingStatus.data.paymentUrl)
}
}, [bookingStatus, router])
}, [confirmationNumber, bookingStatus, router])
function handleSubmit() {
function handleSubmit(data: PaymentFormData) {
initiateBooking.mutate({
hotelId: hotel.operaId,
checkInDate: "2024-12-10",
@@ -91,11 +115,11 @@ export default function Payment({ hotel }: PaymentProps) {
petFriendly: true,
accessibility: true,
},
smsConfirmationRequested: true,
smsConfirmationRequested: data.smsConfirmation,
},
],
payment: {
paymentMethod: selectedPaymentMethod,
paymentMethod: data.paymentMethod,
cardHolder: {
email: "test.user@scandichotels.com",
name: "Test User",
@@ -117,45 +141,80 @@ export default function Payment({ hotel }: PaymentProps) {
}
return (
<div>
<div>
<div className={styles.paymentItemContainer}>
<button
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod("card")}
>
<input
type="radio"
name="payment-method"
id="card"
value="card"
defaultChecked={selectedPaymentMethod === "card"}
/>
<label htmlFor="card">card</label>
</button>
<FormProvider {...methods}>
<form
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(
(paymentOption) => (
<button
key={paymentOption}
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod(paymentOption)}
>
<input
type="radio"
name="payment-method"
id={paymentOption}
value={paymentOption}
defaultChecked={selectedPaymentMethod === paymentOption}
/>
<label htmlFor={paymentOption}>{paymentOption}</label>
</button>
(paymentMethod) => (
<PaymentOption
key={paymentMethod}
name="paymentMethod"
value={paymentMethod as PaymentMethodEnum}
label={
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
}
/>
)
)}
</div>
</div>
<Button disabled={!selectedPaymentMethod} onClick={handleSubmit}>
{intl.formatMessage({ id: "Complete booking & go to payment" })}
</Button>
</div>
<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>
<Button
type="submit"
className={styles.submitButton}
disabled={
!methods.formState.isValid || methods.formState.isSubmitting
}
>
{intl.formatMessage({ id: "Complete booking & go to payment" })}
</Button>
</form>
</FormProvider>
)
}

View File

@@ -1,18 +1,27 @@
.paymentItemContainer {
max-width: 480px;
.paymentContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
padding-bottom: var(--Spacing-x4);
gap: var(--Spacing-x3);
max-width: 480px;
}
.paymentItem {
background-color: var(--Base-Background-Normal);
padding: var(--Spacing-x3);
border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-radius-Medium);
.paymentOptionContainer {
display: flex;
align-items: center;
gap: var(--Spacing-x2);
cursor: pointer;
flex-direction: column;
gap: var(--Spacing-x-one-and-half);
}
.submitButton {
align-self: flex-start;
}
.paymentContainer .link {
font-weight: 500;
font-size: var(--Typography-Caption-Regular-fontSize);
}
.terms {
display: flex;
flex-direction: row;
gap: var(--Spacing-x-one-and-half);
}

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { PaymentMethodEnum } from "@/constants/booking"
export const paymentSchema = z.object({
paymentMethod: z.nativeEnum(PaymentMethodEnum),
smsConfirmation: z.boolean(),
termsAndConditions: z.boolean().refine((value) => value === true, {
message: "You must accept the terms and conditions",
}),
})
export interface PaymentFormData extends z.output<typeof paymentSchema> {}

View File

@@ -17,6 +17,10 @@
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.wrapper:last-child .main {
border-bottom: none;
}
.main {
display: flex;
flex-direction: column;