Merged in feat/SW-431-payment-flow (pull request #635)

Feat/SW-431 payment flow

* feat(SW-431): Update mock hotel data

* feat(SW-431): Added route handler and trpc routes

* feat(SW-431): List payment methods and handle booking status and redirection

* feat(SW-431): Updated booking page to poll for booking status

* feat(SW-431): Updated create booking contract

* feat(SW-431): small fix

* fix(SW-431): Added intl string and sorted dictionaries

* fix(SW-431): Changes from PR

* fix(SW-431): fixes from PR

* fix(SW-431): add todo comments

* fix(SW-431): update schema prop


Approved-by: Simon.Emanuelsson
This commit is contained in:
Tobias Johansson
2024-10-04 09:37:09 +00:00
parent 105f721dc9
commit 4103e3fb37
26 changed files with 711 additions and 287 deletions

View File

@@ -1,62 +1,161 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum,
} from "@/constants/booking"
import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner"
import Button from "@/components/TempDesignSystem/Button"
import { toast } from "@/components/TempDesignSystem/Toasts"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import useLang from "@/hooks/useLang"
import styles from "./payment.module.css"
import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section"
const maxRetries = 40
const retryInterval = 2000
export default function Payment({ hotel }: PaymentProps) {
const router = useRouter()
const lang = useLang()
const intl = useIntl()
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>("")
export default function Payment() {
const initiateBooking = trpc.booking.booking.create.useMutation({
onSuccess: (result) => {
// TODO: Handle success, poll for payment link and redirect the user to the payment
console.log("Res", result)
if (result?.confirmationNumber) {
// Planet doesn't support query params so we have to store values in session storage
sessionStorage.setItem(
BOOKING_CONFIRMATION_NUMBER,
result.confirmationNumber
)
setConfirmationNumber(result.confirmationNumber)
} else {
// TODO: add proper error message
toast.error("Failed to create booking")
}
},
onError: () => {
// TODO: Handle error
console.log("Error")
onError: (error) => {
console.error("Error", error)
// TODO: add proper error message
toast.error("Failed to create booking")
},
})
return (
<Button
onClick={() =>
// TODO: Use real values
initiateBooking.mutate({
hotelId: "811",
checkInDate: "2024-12-10",
checkOutDate: "2024-12-11",
rooms: [
{
adults: 1,
children: 0,
rateCode: "SAVEEU",
roomTypeCode: "QC",
guest: {
title: "Mr",
firstName: "Test",
lastName: "User",
email: "test.user@scandichotels.com",
phoneCountryCodePrefix: "string",
phoneNumber: "string",
countryCode: "string",
},
smsConfirmationRequested: true,
},
],
payment: {
cardHolder: {
Email: "test.user@scandichotels.com",
Name: "Test User",
PhoneCountryCode: "",
PhoneSubscriber: "",
},
success: "success/handle",
error: "error/handle",
cancel: "cancel/handle",
const bookingStatus = useHandleBookingStatus(
confirmationNumber,
BookingStatusEnum.PaymentRegistered,
maxRetries,
retryInterval
)
useEffect(() => {
if (bookingStatus?.data?.paymentUrl) {
router.push(bookingStatus.data.paymentUrl)
}
}, [bookingStatus, router])
function handleSubmit() {
initiateBooking.mutate({
hotelId: hotel.operaId,
checkInDate: "2024-12-10",
checkOutDate: "2024-12-11",
rooms: [
{
adults: 1,
childrenAges: [],
rateCode: "SAVEEU",
roomTypeCode: "QC",
guest: {
title: "Mr",
firstName: "Test",
lastName: "User",
email: "test.user@scandichotels.com",
phoneCountryCodePrefix: "string",
phoneNumber: "string",
countryCode: "string",
},
})
}
>
Create booking
</Button>
packages: {
breakfast: true,
allergyFriendly: true,
petFriendly: true,
accessibility: true,
},
smsConfirmationRequested: true,
},
],
payment: {
paymentMethod: selectedPaymentMethod,
cardHolder: {
email: "test.user@scandichotels.com",
name: "Test User",
phoneCountryCode: "",
phoneSubscriber: "",
},
success: `api/web/payment-callback/${lang}/success`,
error: `api/web/payment-callback/${lang}/error`,
cancel: `api/web/payment-callback/${lang}/cancel`,
},
})
}
if (
initiateBooking.isPending ||
(confirmationNumber && !bookingStatus.data?.paymentUrl)
) {
return <LoadingSpinner />
}
return (
<div>
<div>
<div className={styles.paymentItemContainer}>
<button
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod("card")}
>
<input
type="radio"
name="payment-method"
id="card"
value="card"
checked={selectedPaymentMethod === "card"}
/>
<label htmlFor="card">card</label>
</button>
{hotel.merchantInformationData.alternatePaymentOptions.map(
(paymentOption) => (
<button
key={paymentOption}
className={styles.paymentItem}
onClick={() => setSelectedPaymentMethod(paymentOption)}
>
<input
type="radio"
name="payment-method"
id={paymentOption}
value={paymentOption}
checked={selectedPaymentMethod === paymentOption}
/>
<label htmlFor={paymentOption}>{paymentOption}</label>
</button>
)
)}
</div>
</div>
<Button disabled={!selectedPaymentMethod} onClick={handleSubmit}>
{intl.formatMessage({ id: "Complete booking & go to payment" })}
</Button>
</div>
)
}

View File

@@ -0,0 +1,18 @@
.paymentItemContainer {
max-width: 480px;
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
padding-bottom: var(--Spacing-x4);
}
.paymentItem {
background-color: var(--Base-Background-Normal);
padding: var(--Spacing-x3);
border: 1px solid var(--Base-Border-Normal);
border-radius: var(--Corner-radius-Medium);
display: flex;
align-items: center;
gap: var(--Spacing-x2);
cursor: pointer;
}