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:
@@ -0,0 +1,3 @@
|
|||||||
|
.icon {
|
||||||
|
padding-right: var(--Space-x05);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button as ButtonRAC } from "react-aria-components"
|
|
||||||
import { useIntl } from "react-intl"
|
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 { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
import styles from "./button.module.css"
|
import ActionsButton from "../ActionsButton"
|
||||||
|
|
||||||
export default function AddToCalendarButton({
|
export default function AddToCalendarButton({
|
||||||
disabled,
|
disabled,
|
||||||
@@ -25,20 +21,14 @@ export default function AddToCalendarButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonRAC
|
<ActionsButton
|
||||||
className={styles.button}
|
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
onPress={handleAddToCalendar}
|
icon="calendar_add_on"
|
||||||
>
|
text={intl.formatMessage({
|
||||||
<MaterialIcon color="Icon/Interactive/Default" icon="calendar_add_on" />
|
|
||||||
<Typography variant="Body/Paragraph/mdBold">
|
|
||||||
<span className={styles.text}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "common.addToCalendar",
|
id: "common.addToCalendar",
|
||||||
defaultMessage: "Add to calendar",
|
defaultMessage: "Add to calendar",
|
||||||
})}
|
})}
|
||||||
</span>
|
onPress={handleAddToCalendar}
|
||||||
</Typography>
|
/>
|
||||||
</ButtonRAC>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
|
|||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import ActionsButton from "../ActionsButton"
|
||||||
import Alerts from "./Alerts"
|
import Alerts from "./Alerts"
|
||||||
import Steps from "./Steps"
|
import Steps from "./Steps"
|
||||||
|
|
||||||
@@ -19,12 +20,14 @@ export default function CancelStay() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Modal.Button icon="cancel" onClick={trackCancelStay}>
|
<ActionsButton
|
||||||
{intl.formatMessage({
|
icon="cancel"
|
||||||
|
onPress={trackCancelStay}
|
||||||
|
text={intl.formatMessage({
|
||||||
id: "booking.cancelStay",
|
id: "booking.cancelStay",
|
||||||
defaultMessage: "Cancel stay",
|
defaultMessage: "Cancel stay",
|
||||||
})}
|
})}
|
||||||
</Modal.Button>
|
/>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog className={styles.dialog}>
|
<Dialog className={styles.dialog}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useMyStayStore } from "@/stores/my-stay"
|
|||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import ActionsButton from "../ActionsButton"
|
||||||
import { dateHasPassed } from "../utils"
|
import { dateHasPassed } from "../utils"
|
||||||
import Alerts from "./Alerts"
|
import Alerts from "./Alerts"
|
||||||
import Steps from "./Steps"
|
import Steps from "./Steps"
|
||||||
@@ -40,13 +41,12 @@ export default function ChangeDates() {
|
|||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Modal.Button
|
<ActionsButton
|
||||||
icon="edit_calendar"
|
icon="edit_calendar"
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
onClick={trackChangeDates}
|
onPress={trackChangeDates}
|
||||||
>
|
text={text}
|
||||||
{text}
|
/>
|
||||||
</Modal.Button>
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
import { DialogTrigger } from "react-aria-components"
|
import { DialogTrigger } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
|
||||||
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import ActionsButton from "../ActionsButton"
|
||||||
|
|
||||||
export default function CustomerSupport() {
|
export default function CustomerSupport() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
@@ -15,12 +16,14 @@ export default function CustomerSupport() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Modal.Button icon="support_agent" onClick={trackCustomerSupport}>
|
<ActionsButton
|
||||||
{intl.formatMessage({
|
onPress={trackCustomerSupport}
|
||||||
|
icon="support_agent"
|
||||||
|
text={intl.formatMessage({
|
||||||
id: "common.customerSupport",
|
id: "common.customerSupport",
|
||||||
defaultMessage: "Customer support",
|
defaultMessage: "Customer support",
|
||||||
})}
|
})}
|
||||||
</Modal.Button>
|
/>
|
||||||
<CustomerSupportModal />
|
<CustomerSupportModal />
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useMyStayStore } from "@/stores/my-stay"
|
|||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
|
import ActionsButton from "../ActionsButton"
|
||||||
import { dateHasPassed } from "../utils"
|
import { dateHasPassed } from "../utils"
|
||||||
import Form from "./Form"
|
import Form from "./Form"
|
||||||
|
|
||||||
@@ -54,9 +55,11 @@ export default function GuaranteeLateArrival() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Modal.Button icon="check" onClick={trackGuaranteeLateArrival}>
|
<ActionsButton
|
||||||
{text}
|
onPress={trackGuaranteeLateArrival}
|
||||||
</Modal.Button>
|
text={text}
|
||||||
|
icon="check"
|
||||||
|
/>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog className={styles.dialog}>
|
<Dialog className={styles.dialog}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
color: var(--Text-Interactive-Default);
|
color: var(--Text-Interactive-Default);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
padding: var(--Space-x1) 0;
|
padding: var(--Space-x05) 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
.list {
|
.list {
|
||||||
list-style: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
gap: var(--Space-x15);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,22 @@ import CancelStay from "./CancelStay"
|
|||||||
import ChangeDates from "./ChangeDates"
|
import ChangeDates from "./ChangeDates"
|
||||||
import CustomerSupport from "./CustomerSupport"
|
import CustomerSupport from "./CustomerSupport"
|
||||||
import GuaranteeLateArrival from "./GuaranteeLateArrival"
|
import GuaranteeLateArrival from "./GuaranteeLateArrival"
|
||||||
|
import ResendConfirmationEmail from "./ResendConfirmationEmail"
|
||||||
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
|
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
|
||||||
|
|
||||||
import styles from "./actions.module.css"
|
import styles from "./actions.module.css"
|
||||||
|
|
||||||
export default function Actions() {
|
type ActionsProps = {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Actions({ onClose }: ActionsProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
<ChangeDates />
|
<ChangeDates />
|
||||||
<GuaranteeLateArrival />
|
<GuaranteeLateArrival />
|
||||||
<AddToCalendar />
|
<AddToCalendar />
|
||||||
|
<ResendConfirmationEmail onClose={onClose} />
|
||||||
<ViewAndPrintReceipt />
|
<ViewAndPrintReceipt />
|
||||||
<CustomerSupport />
|
<CustomerSupport />
|
||||||
<CancelStay />
|
<CancelStay />
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function ManageStay() {
|
|||||||
</ButtonRAC>
|
</ButtonRAC>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Actions />
|
<Actions onClose={close} />
|
||||||
<Info />
|
<Info />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ export namespace endpoints {
|
|||||||
export function guarantee(confirmationNumber: string) {
|
export function guarantee(confirmationNumber: string) {
|
||||||
return `${bookings}/${confirmationNumber}/guarantee`
|
return `${bookings}/${confirmationNumber}/guarantee`
|
||||||
}
|
}
|
||||||
|
export function confirmNotification(confirmationNumber: string) {
|
||||||
|
return `${bookings}/${confirmationNumber}/confirmNotification`
|
||||||
|
}
|
||||||
|
|
||||||
export const enum Stays {
|
export const enum Stays {
|
||||||
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
|
future = `${base.path.booking}/${version}/${base.enitity.Stays}/future`,
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export const removePackageInput = z.object({
|
|||||||
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
|
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({
|
export const cancelBookingsInput = z.object({
|
||||||
language: z.nativeEnum(Lang),
|
language: z.nativeEnum(Lang),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
cancelBookingsInput,
|
cancelBookingsInput,
|
||||||
guaranteeBookingInput,
|
guaranteeBookingInput,
|
||||||
removePackageInput,
|
removePackageInput,
|
||||||
|
resendConfirmationInput,
|
||||||
updateBookingInput,
|
updateBookingInput,
|
||||||
} from "../input"
|
} from "../input"
|
||||||
import { bookingConfirmationSchema } from "../output"
|
import { bookingConfirmationSchema } from "../output"
|
||||||
@@ -318,6 +319,53 @@ export const bookingMutationRouter = router({
|
|||||||
|
|
||||||
metricsRemovePackage.success()
|
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
|
return true
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user