Merged in fix/STAY-72-resend-booking-confirmation (pull request #3067)

feat(STAY-72): add resend confirmation button and endpoint

* feat(STAY-72): add resend confirmation button and endpoint

* fix: replace modify buttons with design system button


Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Erik Tiekstra
This commit is contained in:
Christel Westerberg
2025-11-06 13:40:15 +00:00
parent ec511e2c8b
commit 20bf89d206
16 changed files with 209 additions and 56 deletions

View File

@@ -0,0 +1,33 @@
"use client"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import styles from "./actionsButton.module.css"
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
export default function ActionsButton({
icon,
text,
onPress,
isDisabled = false,
}: {
icon: MaterialSymbolProps["icon"]
text: string
onPress: () => void
isDisabled?: boolean
}) {
return (
<Button
variant="Text"
wrapping={false}
onPress={onPress}
isDisabled={isDisabled}
typography="Body/Paragraph/mdBold"
>
<MaterialIcon color="CurrentColor" icon={icon} className={styles.icon} />
<span>{text}</span>
</Button>
)
}

View File

@@ -1,14 +1,10 @@
"use client"
import { Button as ButtonRAC } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackMyStayPageLink } from "@/utils/tracking"
import styles from "./button.module.css"
import ActionsButton from "../ActionsButton"
export default function AddToCalendarButton({
disabled,
@@ -25,20 +21,14 @@ export default function AddToCalendarButton({
}
return (
<ButtonRAC
className={styles.button}
<ActionsButton
isDisabled={disabled}
icon="calendar_add_on"
text={intl.formatMessage({
id: "common.addToCalendar",
defaultMessage: "Add to calendar",
})}
onPress={handleAddToCalendar}
>
<MaterialIcon color="Icon/Interactive/Default" icon="calendar_add_on" />
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.text}>
{intl.formatMessage({
id: "common.addToCalendar",
defaultMessage: "Add to calendar",
})}
</span>
</Typography>
</ButtonRAC>
/>
)
}

View File

@@ -1,19 +0,0 @@
.button {
align-items: center;
background: none;
border: none;
cursor: pointer;
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x1) 0;
width: 100%;
&:disabled {
color: var(--Scandic-Grey-40);
}
}
.text {
color: var(--Text-Interactive-Default);
text-align: left;
}

View File

@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import Alerts from "./Alerts"
import Steps from "./Steps"
@@ -19,12 +20,14 @@ export default function CancelStay() {
return (
<DialogTrigger>
<Modal.Button icon="cancel" onClick={trackCancelStay}>
{intl.formatMessage({
<ActionsButton
icon="cancel"
onPress={trackCancelStay}
text={intl.formatMessage({
id: "booking.cancelStay",
defaultMessage: "Cancel stay",
})}
</Modal.Button>
/>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (

View File

@@ -7,6 +7,7 @@ import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import { dateHasPassed } from "../utils"
import Alerts from "./Alerts"
import Steps from "./Steps"
@@ -40,13 +41,12 @@ export default function ChangeDates() {
})
return (
<DialogTrigger>
<Modal.Button
<ActionsButton
icon="edit_calendar"
isDisabled={isDisabled}
onClick={trackChangeDates}
>
{text}
</Modal.Button>
onPress={trackChangeDates}
text={text}
/>
<Modal>
<Dialog>
{({ close }) => (

View File

@@ -2,10 +2,11 @@
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
export default function CustomerSupport() {
const intl = useIntl()
@@ -15,12 +16,14 @@ export default function CustomerSupport() {
return (
<DialogTrigger>
<Modal.Button icon="support_agent" onClick={trackCustomerSupport}>
{intl.formatMessage({
<ActionsButton
onPress={trackCustomerSupport}
icon="support_agent"
text={intl.formatMessage({
id: "common.customerSupport",
defaultMessage: "Customer support",
})}
</Modal.Button>
/>
<CustomerSupportModal />
</DialogTrigger>
)

View File

@@ -9,6 +9,7 @@ import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import { dateHasPassed } from "../utils"
import Form from "./Form"
@@ -54,9 +55,11 @@ export default function GuaranteeLateArrival() {
return (
<DialogTrigger>
<Modal.Button icon="check" onClick={trackGuaranteeLateArrival}>
{text}
</Modal.Button>
<ActionsButton
onPress={trackGuaranteeLateArrival}
text={text}
icon="check"
/>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (

View File

@@ -0,0 +1,73 @@
"use client"
import { useIntl } from "react-intl"
import { logger } from "@scandic-hotels/common/logger"
import { toast } from "@scandic-hotels/design-system/Toast"
import { trpc } from "@scandic-hotels/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import useLang from "@/hooks/useLang"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
type ResendConfirmationEmailProps = {
onClose: () => void
}
export default function ResendConfirmationEmail({
onClose,
}: ResendConfirmationEmailProps) {
const intl = useIntl()
const lang = useLang()
const refId = useMyStayStore((state) => state.refId)
const resendEmail = trpc.booking.resendConfirmation.useMutation()
function resendConfirmationEmail() {
trackMyStayPageLink("resend confirmation email")
resendEmail.mutate(
{ language: lang, refId },
{
onSuccess() {
onClose()
toast.success(
intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail.success",
defaultMessage: "Confirmation email was resent successfully",
})
)
},
onError(e) {
onClose()
toast.error(
intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail.error",
defaultMessage:
"There was an error resending the confirmation email",
})
)
logger.error("[myStay] Resend confirmation email failed", {
error: e.data,
})
},
}
)
}
const printMsg = intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail",
defaultMessage: "Resend confirmation email",
})
return (
<ActionsButton
onPress={resendConfirmationEmail}
isDisabled={resendEmail.isPending}
icon="email"
text={printMsg}
/>
)
}

View File

@@ -3,5 +3,5 @@
color: var(--Text-Interactive-Default);
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x1) 0;
padding: var(--Space-x05) 0;
}

View File

@@ -1,5 +1,8 @@
.list {
list-style: none;
display: flex;
flex-direction: column;
align-items: start;
margin: 0;
padding: 0;
gap: var(--Space-x15);
}

View File

@@ -3,16 +3,22 @@ import CancelStay from "./CancelStay"
import ChangeDates from "./ChangeDates"
import CustomerSupport from "./CustomerSupport"
import GuaranteeLateArrival from "./GuaranteeLateArrival"
import ResendConfirmationEmail from "./ResendConfirmationEmail"
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
import styles from "./actions.module.css"
export default function Actions() {
type ActionsProps = {
onClose: () => void
}
export default function Actions({ onClose }: ActionsProps) {
return (
<div className={styles.list}>
<ChangeDates />
<GuaranteeLateArrival />
<AddToCalendar />
<ResendConfirmationEmail onClose={onClose} />
<ViewAndPrintReceipt />
<CustomerSupport />
<CancelStay />

View File

@@ -54,7 +54,7 @@ export default function ManageStay() {
</ButtonRAC>
</header>
<div className={styles.content}>
<Actions />
<Actions onClose={close} />
<Info />
</div>
</>

View File

@@ -78,6 +78,9 @@ export namespace endpoints {
export function guarantee(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/guarantee`
}
export function confirmNotification(confirmationNumber: string) {
return `${bookings}/${confirmationNumber}/confirmNotification`
}
export const enum Stays {
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,

View File

@@ -22,6 +22,10 @@ export const removePackageInput = z.object({
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const resendConfirmationInput = z.object({
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const cancelBookingsInput = z.object({
language: z.nativeEnum(Lang),
})

View File

@@ -10,6 +10,7 @@ import {
cancelBookingsInput,
guaranteeBookingInput,
removePackageInput,
resendConfirmationInput,
updateBookingInput,
} from "../input"
import { bookingConfirmationSchema } from "../output"
@@ -318,6 +319,53 @@ export const bookingMutationRouter = router({
metricsRemovePackage.success()
return true
}),
resendConfirmation: safeProtectedServiceProcedure
.input(resendConfirmationInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = await ctx.getScandicUserToken()
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber } = ctx
const resendConfirmationCounter = createCounter(
"trpc.booking",
"confirmation.resend"
)
const metricsResendConfirmation = resendConfirmationCounter.init({
confirmationNumber,
})
metricsResendConfirmation.start()
const token = ctx.token ?? ctx.serviceToken
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.post(
api.endpoints.v1.Booking.confirmNotification(confirmationNumber),
{
headers,
},
{ language: input.language }
)
if (!apiResponse.ok) {
await metricsResendConfirmation.httpError(apiResponse)
return false
}
metricsResendConfirmation.success()
return true
}),
})