Merged in fix/STAY-73-opt-in-email (pull request #3028)

Fix/STAY-73 opt in email

* fix: let user opt-in for modification email when adding ancillaries

* fix: add toast when successfully removing an ancillary


Approved-by: Erik Tiekstra
Approved-by: Elin Svedin
This commit is contained in:
Christel Westerberg
2025-10-29 12:45:18 +00:00
parent 333636c81a
commit 377c8886ad
8 changed files with 53 additions and 4 deletions

View File

@@ -20,6 +20,8 @@ import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
import { trackUpdatePaymentMethod } from "@/utils/tracking" import { trackUpdatePaymentMethod } from "@/utils/tracking"
import { ancillaryError } from "../../../schema"
import styles from "./confirmationStep.module.css" import styles from "./confirmationStep.module.css"
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries" import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
@@ -219,6 +221,12 @@ export default function ConfirmationStep({
<Checkbox <Checkbox
name="termsAndConditions" name="termsAndConditions"
registerOptions={{ required: true }} registerOptions={{ required: true }}
errorCodeMessages={{
[ancillaryError.TERMS_NOT_ACCEPTED]: intl.formatMessage({
id: "common.mustAcceptTermsError",
defaultMessage: "You must accept the terms and conditions",
}),
}}
> >
<Typography variant="Body/Supporting text (caption)/smRegular"> <Typography variant="Body/Supporting text (caption)/smRegular">
<span> <span>
@@ -229,6 +237,17 @@ export default function ConfirmationStep({
</span> </span>
</Typography> </Typography>
</Checkbox> </Checkbox>
<Checkbox name="emailOptOut">
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
id: "addAncillary.confirmationStep.optInForEmail",
defaultMessage:
"I want to receive an updated booking confirmation email reflecting this add-on",
})}
</span>
</Typography>
</Checkbox>
</div> </div>
</div> </div>
) )

View File

@@ -97,6 +97,7 @@ export default function AddAncillaryFlowModal({
deliveryTime: defaultDeliveryTime, deliveryTime: defaultDeliveryTime,
optionalText: "", optionalText: "",
termsAndConditions: false, termsAndConditions: false,
optInEmail: false,
paymentMethod: booking.guaranteeInfo paymentMethod: booking.guaranteeInfo
? PaymentMethodEnum.card ? PaymentMethodEnum.card
: savedCreditCards?.length : savedCreditCards?.length
@@ -152,6 +153,7 @@ export default function AddAncillaryFlowModal({
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
? data.deliveryTime ? data.deliveryTime
: undefined, : undefined,
emailOptOut: !data.optInEmail,
packages: packages, packages: packages,
language: lang, language: lang,
}, },

View File

@@ -7,6 +7,11 @@ const quantitySchemaWithoutRefine = z.object({
quantityWithCard: z.number().nullable(), quantityWithCard: z.number().nullable(),
}) })
export const ancillaryError = {
TERMS_NOT_ACCEPTED: "TERMS_NOT_ACCEPTED",
MIN_QUANTITY_NOT_REACHED: "MIN_QUANTITY_NOT_REACHED",
} as const
export const quantitySchema = z export const quantitySchema = z
.object({}) .object({})
.merge(quantitySchemaWithoutRefine) .merge(quantitySchemaWithoutRefine)
@@ -14,7 +19,7 @@ export const quantitySchema = z
(data) => (data) =>
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0, (data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
{ {
message: "You must select at least one quantity", message: ancillaryError.MIN_QUANTITY_NOT_REACHED,
path: ["quantityWithCard"], path: ["quantityWithCard"],
} }
) )
@@ -23,7 +28,10 @@ export const ancillaryFormSchema = z
.object({ .object({
deliveryTime: z.string(), deliveryTime: z.string(),
optionalText: z.string(), optionalText: z.string(),
termsAndConditions: z.boolean(), termsAndConditions: z
.boolean()
.refine((value) => value === true, ancillaryError.TERMS_NOT_ACCEPTED),
optInEmail: z.boolean(),
paymentMethod: nullableStringValidator, paymentMethod: nullableStringValidator,
}) })
.merge(quantitySchemaWithoutRefine) .merge(quantitySchemaWithoutRefine)
@@ -31,7 +39,7 @@ export const ancillaryFormSchema = z
(data) => (data) =>
(data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0, (data.quantityWithPoints ?? 0) > 0 || (data.quantityWithCard ?? 0) > 0,
{ {
message: "You must select at least one quantity", message: ancillaryError.MIN_QUANTITY_NOT_REACHED,
path: ["quantityWithCard"], path: ["quantityWithCard"],
} }
) )

View File

@@ -76,6 +76,13 @@ export default function RemoveButton({
if (!data) { if (!data) {
throw new Error() throw new Error()
} }
toast.success(
intl.formatMessage({
id: "myStay.removeAncillary.success",
defaultMessage:
"The product has now been removed from your booking.",
})
)
utils.booking.get.invalidate({ utils.booking.get.invalidate({
refId: refId, refId: refId,
}) })

View File

@@ -56,6 +56,7 @@ export default function GuaranteeAncillaryHandler({
ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime ancillaryDeliveryTime: selectedAncillary.requiresDeliveryTime
? formData.deliveryTime ? formData.deliveryTime
: undefined, : undefined,
emailOptOut: !formData.optInEmail,
packages, packages,
language: lang, language: lang,
}, },

View File

@@ -8,11 +8,22 @@ import { signupErrors } from "@scandic-hotels/trpc/routers/user/schemas"
import { editProfileErrors } from "@/components/Forms/Edit/Profile/schema" import { editProfileErrors } from "@/components/Forms/Edit/Profile/schema"
import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema" import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema"
import { ancillaryError } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema"
import type { IntlShape } from "react-intl" import type { IntlShape } from "react-intl"
export function getErrorMessage(intl: IntlShape, errorCode?: string) { export function getErrorMessage(intl: IntlShape, errorCode?: string) {
switch (errorCode) { switch (errorCode) {
case ancillaryError.MIN_QUANTITY_NOT_REACHED:
return intl.formatMessage({
id: "addAncillary.selectQuantityStep.minQuantityNotReached",
defaultMessage: "You must select at least one quantity",
})
case ancillaryError.TERMS_NOT_ACCEPTED:
return intl.formatMessage({
id: "addAncillary.confirmationStep.termsAndConditionsNotice",
defaultMessage: "You must accept the terms and conditions to proceed",
})
case findMyBookingErrors.BOOKING_NUMBER_INVALID: case findMyBookingErrors.BOOKING_NUMBER_INVALID:
return intl.formatMessage({ return intl.formatMessage({
id: "error.invalidBookingNumber", id: "error.invalidBookingNumber",

View File

@@ -14,6 +14,7 @@ export const addPackageInput = z.object({
comment: z.string().optional(), comment: z.string().optional(),
}) })
), ),
emailOptOut: z.boolean(),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
}) })

View File

@@ -145,7 +145,7 @@ export const bookingMutationRouter = router({
headers, headers,
body: body, body: body,
}, },
{ language } { language, emailOptOut: input.emailOptOut }
) )
if (!apiResponse.ok) { if (!apiResponse.ok) {