diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/campaignActivated.module.css b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/campaignActivated.module.css new file mode 100644 index 000000000..b671d5dd4 --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/campaignActivated.module.css @@ -0,0 +1,6 @@ +.activatedText { + display: flex; + gap: var(--Space-x2); + padding-top: var(--Space-x2); + color: var(--Text-Default); +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/index.tsx new file mode 100644 index 000000000..0ceba08f9 --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/CampaignActivated/index.tsx @@ -0,0 +1,28 @@ +"use client" + +import { useIntl } from "react-intl" + +import { Divider } from "@scandic-hotels/design-system/Divider" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./campaignActivated.module.css" + +export function CampaignActivated() { + const intl = useIntl() + return ( + + + + + + + {intl.formatMessage({ + defaultMessage: "This campaign is active", + })} + + + + + ) +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/errorModal.module.css b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/errorModal.module.css new file mode 100644 index 000000000..49f084f38 --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/errorModal.module.css @@ -0,0 +1,25 @@ +.errorModal { + text-align: center; +} + +.contactDetails { + padding: 0 var(--Space-x1); + display: grid; + gap: var(--Space-x15); +} + +.address { + display: grid; + gap: var(--Space-x05); + font-style: normal; +} + +.divider { + display: grid; + grid-auto-flow: column; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: var(--Space-x2); + color: var(--UI-Text-Placeholder); + width: 100%; +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/index.tsx new file mode 100644 index 000000000..f2c9d13de --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/ErrorModal/index.tsx @@ -0,0 +1,107 @@ +import { useIntl } from "react-intl" + +import { Divider } from "@scandic-hotels/design-system/Divider" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Link from "@scandic-hotels/design-system/Link" +import Modal from "@scandic-hotels/design-system/Modal" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { supportEmail, supportPhone } from "@/constants/contactSupport" + +import useLang from "@/hooks/useLang" + +import styles from "./errorModal.module.css" + +import type { PromoCode } from "@scandic-hotels/trpc/types/promoCampaignPage" + +interface ErrorModalProps { + promoCode: PromoCode + isOpen: boolean + handleToggle: () => void +} + +export function ErrorModal({ + promoCode, + isOpen, + handleToggle, +}: ErrorModalProps) { + const intl = useIntl() + const lang = useLang() + + return ( + + + +

+ {intl.formatMessage({ + defaultMessage: "Could not activate this offer", + })} +

+
+ +

+ {intl.formatMessage({ + defaultMessage: "Read more", + })} +

+
+
+ + +

+ {intl.formatMessage({ + defaultMessage: "If this problem persists", + })} +

+
+ +
+ + +

+ {intl.formatMessage({ + defaultMessage: "Contact us", + })} +

+
+ +
+ + {supportPhone[lang]} + + + {supportEmail[lang]} + +
+
+ +

+ {intl.formatMessage( + { + defaultMessage: "Campaign reference: {promoCode}", + }, + { + promoCode, + bold: (text) => ( + + {text} + + ), + } + )} +

+
+
+
+ ) +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/activateOffer.module.css b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/activateOffer.module.css new file mode 100644 index 000000000..6ea911b40 --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/activateOffer.module.css @@ -0,0 +1,3 @@ +.activateButton { + width: 100%; +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/index.tsx new file mode 100644 index 000000000..8720e5614 --- /dev/null +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOffer/index.tsx @@ -0,0 +1,83 @@ +"use client" +import { useRouter } from "next/navigation" +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import { trackEvent } from "@scandic-hotels/tracking/base" +import { trpc } from "@scandic-hotels/trpc/client" + +import { CampaignActivated } from "./CampaignActivated" +import { ErrorModal } from "./ErrorModal" + +import styles from "./activateOffer.module.css" + +import type { PromoCode } from "@scandic-hotels/trpc/types/promoCampaignPage" + +interface ActivateOfferProps { + promoCode: PromoCode + campaignIsActive: boolean +} +export default function ActivateOffer({ + promoCode, + campaignIsActive, +}: ActivateOfferProps) { + const intl = useIntl() + const router = useRouter() + const trpcUtils = trpc.useUtils() + + async function handleActivateFlow() { + trackEvent({ + event: "ActivatePromoCampaign", + login: { + ctaName: "activate offer", + }, + }) + activateCampaign.mutate({ promotionId: promoCode }) + } + + const activateCampaign = trpc.user.promoCampaign.add.useMutation({ + onSuccess: async (data) => { + if (!data) return + trpcUtils.user.promoCampaign.invalidate() + router.refresh() + }, + }) + + function ActivateButton() { + return ( + + ) + } + + return ( + <> + {campaignIsActive || + (activateCampaign.isSuccess && activateCampaign.data) ? ( + + ) : ( + + )} + + + ) +} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOfferButton/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOfferButton/index.tsx deleted file mode 100644 index fe16c1ce8..000000000 --- a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/ActivateOfferButton/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import { Button } from "@scandic-hotels/design-system/Button" - -import styles from "../hero.module.css" - -// TODO: Trigger acivation. -export default function ActivateOfferButton() { - const intl = useIntl() - return ( - - ) -} diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/PromoLoginButton.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/PromoLoginButton.tsx index 19cbf85e8..27c4ff58d 100644 --- a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/PromoLoginButton.tsx +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/PromoLoginButton.tsx @@ -5,6 +5,7 @@ import { useIntl } from "react-intl" import { login } from "@scandic-hotels/common/constants/routes/handleAuth" import { useLazyPathname } from "@scandic-hotels/common/hooks/useLazyPathname" import ButtonLink from "@scandic-hotels/design-system/ButtonLink" +import { trackEvent } from "@scandic-hotels/tracking/base" import useLang from "@/hooks/useLang" @@ -18,6 +19,16 @@ export default function PromoLoginButton() { return ( + trackEvent({ + event: "loginStart", + login: { + position: "campaign banner login cta", + action: "login start", + ctaName: "login", + }, + }) + } href={loginHref} variant="Primary" color="Inverted" diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/index.tsx index 4694af7e4..2217619f1 100644 --- a/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/index.tsx +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/Hero/index.tsx @@ -6,13 +6,14 @@ import { Divider } from "@scandic-hotels/design-system/Divider" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import Image from "@scandic-hotels/design-system/Image" import { Typography } from "@scandic-hotels/design-system/Typography" +import { trackEvent } from "@scandic-hotels/tracking/base" import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import ActivateOfferButton from "./ActivateOfferButton" +import ActivateOffer from "./ActivateOffer" import IneligibleMessage from "./IneligibleMessage" import PromoLoginButton from "./PromoLoginButton" import { isUserEligibleForPromo } from "./utils" @@ -20,28 +21,52 @@ import { isUserEligibleForPromo } from "./utils" import styles from "./hero.module.css" import type { MembershipLevel } from "@scandic-hotels/common/constants/membershipLevels" -import type { PromoHero } from "@scandic-hotels/trpc/types/promoCampaignPage" +import type { + PromoCode, + PromoHero, +} from "@scandic-hotels/trpc/types/promoCampaignPage" interface PromoCampaignHeroProps extends React.HTMLAttributes { promoHero: PromoHero eligibleLevels: MembershipLevel[] + promoCode: PromoCode } export default async function PromoCampaignHero({ promoHero, eligibleLevels, + promoCode, className, ...props }: PromoCampaignHeroProps) { - const { image, heading, benefits } = promoHero + const intl = await getIntl() + const lang = await getLang() const profile = await getProfileSafely() + + const { image, heading, benefits } = promoHero const isLoggedIn = !!profile const userMembershipLevel = profile?.membership?.membershipLevel const isEligible = isLoggedIn ? isUserEligibleForPromo(userMembershipLevel, eligibleLevels) : false - const intl = await getIntl() - const lang = await getLang() + + const activeCampaigns = profile?.promotions + const campaignAlreadyActive = Boolean(activeCampaigns?.includes(promoCode)) + + function CampaignCTA() { + if (!isLoggedIn) return null + + if (!isEligible) { + return + } + + return ( + + ) + } return (
) : null} - - {isLoggedIn && - (isEligible ? : )} + {!isLoggedIn && ( @@ -126,6 +149,16 @@ export default async function PromoCampaignHero({ + trackEvent({ + event: "signupStart", + signUp: { + position: "campaign banner signup cta", + action: "signup start", + ctaName: "signup", + }, + }) + } href={signup[lang]} variant="Secondary" color="Inverted" diff --git a/apps/scandic-web/components/ContentType/PromoCampaignPage/index.tsx b/apps/scandic-web/components/ContentType/PromoCampaignPage/index.tsx index 34bca6746..3b575c364 100644 --- a/apps/scandic-web/components/ContentType/PromoCampaignPage/index.tsx +++ b/apps/scandic-web/components/ContentType/PromoCampaignPage/index.tsx @@ -28,8 +28,15 @@ export default async function PromoCampaignPage() { } const { promo_campaign_page, tracking } = pageData - const { heading, subheading, enddate, promo_hero, blocks, eligibleLevels } = - promo_campaign_page + const { + heading, + subheading, + enddate, + promo_hero, + blocks, + eligibleLevels, + promo_code, + } = promo_campaign_page const isCampaignExpired = enddate ? dt().isAfter(dt(enddate).endOf("day")) @@ -47,6 +54,7 @@ export default async function PromoCampaignPage() { promoHero={promo_hero} eligibleLevels={eligibleLevels} blocks={blocks} + promoCode={promo_code} /> )} @@ -61,18 +69,21 @@ function ActiveCampaignLayout({ promoHero, eligibleLevels, blocks, + promoCode, }: { heading: PromoCampaignPageData["heading"] subheading: PromoCampaignPageData["subheading"] promoHero: PromoHero eligibleLevels: PromoCampaignPageData["eligibleLevels"] blocks: PromoCampaignPageData["blocks"] + promoCode: PromoCampaignPageData["promo_code"] }) { return (
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts index 3b81ddb8a..b9084fb64 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts @@ -161,6 +161,7 @@ const authenticatedUser: SafeUser = { phoneNumber: undefined, profileId: "", employmentDetails: undefined, + promotions: [], } const badAuthenticatedUser: SafeUser = { @@ -193,6 +194,7 @@ const badAuthenticatedUser: SafeUser = { phoneNumber: undefined, profileId: "", employmentDetails: undefined, + promotions: [], } const loggedOutGuest: Guest = { diff --git a/packages/trpc/lib/api/endpoints.ts b/packages/trpc/lib/api/endpoints.ts index 31e0b82b8..00aaf8caa 100644 --- a/packages/trpc/lib/api/endpoints.ts +++ b/packages/trpc/lib/api/endpoints.ts @@ -221,6 +221,7 @@ export namespace endpoints { */ export namespace Profile { export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}` + export const promoCampaign = `${profile}/Promotion` export function teamMemberCard(employeeId: string) { return `${profile}/${employeeId}/TeamMemberCard` diff --git a/packages/trpc/lib/routers/user/input.ts b/packages/trpc/lib/routers/user/input.ts index 9c0dae675..5a4da6e1e 100644 --- a/packages/trpc/lib/routers/user/input.ts +++ b/packages/trpc/lib/routers/user/input.ts @@ -62,3 +62,7 @@ export const getSavedPaymentCardsInput = z.object({ export type GetSavedPaymentCardsInput = z.input< typeof getSavedPaymentCardsInput > + +export const addPromoCampaignInput = z.object({ + promotionId: z.string(), +}) diff --git a/packages/trpc/lib/routers/user/mutation.ts b/packages/trpc/lib/routers/user/mutation.ts index f179383b1..36cddff52 100644 --- a/packages/trpc/lib/routers/user/mutation.ts +++ b/packages/trpc/lib/routers/user/mutation.ts @@ -9,6 +9,7 @@ import { serverErrorByStatus } from "../../errors" import { protectedProcedure, serviceProcedure } from "../../procedures" import { addCreditCardInput, + addPromoCampaignInput, deleteCreditCardInput, saveCreditCardInput, signupInput, @@ -216,4 +217,48 @@ export const userMutationRouter = router({ redirectUrl: signupVerify[input.language], } }), + promoCampaign: router({ + add: protectedProcedure + .input(addPromoCampaignInput) + .mutation(async function ({ ctx, input }) { + userMutationLogger.info("api.user.promoCampaign.add start") + const apiResponse = await api.post( + api.endpoints.v2.Profile.promoCampaign, + { + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + body: { + promotionId: input.promotionId, + }, + } + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + userMutationLogger.error( + "api.user.promoCampaign.add error", + JSON.stringify({ + query: { + promotionId: input.promotionId, + }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return false + } + + userMutationLogger.info( + "api.user.promoCampaign.add success", + JSON.stringify({ + query: { promotionId: input.promotionId }, + }) + ) + return true + }), + }), }) diff --git a/packages/trpc/lib/routers/user/output.ts b/packages/trpc/lib/routers/user/output.ts index 32b6ab2cb..34a7d754b 100644 --- a/packages/trpc/lib/routers/user/output.ts +++ b/packages/trpc/lib/routers/user/output.ts @@ -113,6 +113,7 @@ export const getUserSchema = z .nullable(), loyalty: userLoyaltySchema.optional(), employmentDetails: employmentDetailsSchema, + promotions: z.array(z.string()).nullish(), }), type: z.string(), }), diff --git a/packages/trpc/lib/routers/user/utils.ts b/packages/trpc/lib/routers/user/utils.ts index 50882fdac..b8c98bd8b 100644 --- a/packages/trpc/lib/routers/user/utils.ts +++ b/packages/trpc/lib/routers/user/utils.ts @@ -93,8 +93,8 @@ export const getVerifiedUser = cache( ) return null } - const verifiedData = getUserSchema.safeParse(apiJson) + if (!verifiedData.success) { metricsGetVerifiedUser.validationError(verifiedData.error) return null @@ -224,6 +224,7 @@ export function parsedUser(data: User, isMFA: boolean) { dateOfBirth: data.dateOfBirth, email: data.email, employmentDetails: data.employmentDetails, + promotions: data.promotions || null, firstName: data.firstName, language: data.language, lastName: data.lastName, diff --git a/packages/trpc/lib/types/promoCampaignPage.ts b/packages/trpc/lib/types/promoCampaignPage.ts index c73f26f16..fa43bb7d7 100644 --- a/packages/trpc/lib/types/promoCampaignPage.ts +++ b/packages/trpc/lib/types/promoCampaignPage.ts @@ -26,6 +26,7 @@ export interface PromoCampaignPage export type PromoCampaignPageData = PromoCampaignPage["promo_campaign_page"] export type PromoHero = NonNullable +export type PromoCode = NonNullable /* REFS */ export interface GetPromoCampaignPageRefsData