Merged in fix/LOY-369-Redeem-tier-rewards (pull request #2822)

fix(LOY-369): Able to redeem tier rewards

* fix(LOY-369): able to redeem on site tier rewards

* fix(LOY-369): single mutation call

* fix(LOY-369): apply coupon check for all tier rewards


Approved-by: Linus Flood
Approved-by: Matilda Landström
This commit is contained in:
Chuma Mcphoy (We Ahead)
2025-09-22 08:27:30 +00:00
parent db546d7167
commit 9b8ed972ec
5 changed files with 91 additions and 68 deletions

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from "react" import { useRef, useState } from "react"
import Title from "@scandic-hotels/design-system/Title" import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
import { type Reward } from "@scandic-hotels/trpc/types/rewards" import { type Reward } from "@scandic-hotels/trpc/types/rewards"
@@ -75,14 +75,9 @@ export default function ClientCurrentRewards({
<div className={styles.content}> <div className={styles.content}>
<RewardIcon rewardId={reward.reward_id} /> <RewardIcon rewardId={reward.reward_id} />
{showRedeem && <ScriptedRewardText reward={reward} />} {showRedeem && <ScriptedRewardText reward={reward} />}
<Title <Typography variant="Title/smLowCase">
as="h4" <h4 className={styles.title}>{reward.label}</h4>
level="h3" </Typography>
textAlign="center"
textTransform="regular"
>
{reward.label}
</Title>
{earliestExpirationDate ? ( {earliestExpirationDate ? (
<ExpirationDate expirationDate={earliestExpirationDate} /> <ExpirationDate expirationDate={earliestExpirationDate} />
) : null} ) : null}

View File

@@ -24,6 +24,11 @@
padding: var(--Spacing-x3); padding: var(--Spacing-x3);
} }
.title {
color: var(--Text-Heading);
text-align: center;
}
.btnContainer { .btnContainer {
padding: 0 var(--Spacing-x3) var(--Spacing-x3); padding: 0 var(--Spacing-x3) var(--Spacing-x3);
} }

View File

@@ -11,8 +11,8 @@ import {
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang" import useLang from "@/hooks/useLang"
@@ -70,7 +70,12 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
<DialogTrigger <DialogTrigger
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")} onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
> >
<Button intent="primary" fullWidth> <Button
variant="Tertiary"
size="Large"
typography="Body/Paragraph/mdBold"
className={styles.redeemButton}
>
{reward.redeemLocation === "Non-redeemable" {reward.redeemLocation === "Non-redeemable"
? intl.formatMessage({ ? intl.formatMessage({
defaultMessage: "How to use", defaultMessage: "How to use",
@@ -79,65 +84,67 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
defaultMessage: "Open", defaultMessage: "Open",
})} })}
</Button> </Button>
<MotionOverlay {animation !== "unmounted" && (
className={styles.overlay} <MotionOverlay
isExiting={animation === "hidden"} className={styles.overlay}
onAnimationComplete={modalStateHandler} isExiting={animation === "hidden"}
variants={variants.fade} onAnimationComplete={modalStateHandler}
initial="hidden" variants={variants.fade}
animate={animation}
>
<MotionModal
className={styles.modal}
variants={variants.slideInOut}
initial="hidden" initial="hidden"
animate={animation} animate={animation}
> >
<Dialog className={styles.dialog} aria-label={reward.label}> <MotionModal
{({ close }) => { className={styles.modal}
function closeModal() { variants={variants.slideInOut}
if ( initial="hidden"
redeemStep === "redeemed" || animate={animation}
redeemStep === "confirm-close" >
) { <Dialog className={styles.dialog} aria-label={reward.label}>
utils.contentstack.rewards.current.invalidate({ {({ close }) => {
lang, function closeModal() {
}) if (
redeemStep === "redeemed" ||
redeemStep === "confirm-close"
) {
utils.contentstack.rewards.current.invalidate({
lang,
})
}
close()
} }
close() return (
} <>
return ( <header className={styles.modalHeader}>
<> <button
<header className={styles.modalHeader}> onClick={() => {
<button if (
onClick={() => { redeemStep === "redeemed" &&
if ( !isRestaurantOnSiteTierReward(reward)
redeemStep === "redeemed" && ) {
!isRestaurantOnSiteTierReward(reward) setRedeemStep("confirm-close")
) { } else {
setRedeemStep("confirm-close") closeModal()
} else { }
closeModal() }}
} type="button"
}} className={styles.modalClose}
type="button" >
className={styles.modalClose} <MaterialIcon icon="close" />
> </button>
<MaterialIcon icon="close" /> </header>
</button>
</header>
{redeemStep === "confirm-close" ? ( {redeemStep === "confirm-close" ? (
<ConfirmClose close={closeModal} /> <ConfirmClose close={closeModal} />
) : ( ) : (
getRedeemFlow(reward, membershipNumber || "") getRedeemFlow(reward, membershipNumber || "")
)} )}
</> </>
) )
}} }}
</Dialog> </Dialog>
</MotionModal> </MotionModal>
</MotionOverlay> </MotionOverlay>
)}
</DialogTrigger> </DialogTrigger>
</RedeemContext.Provider> </RedeemContext.Provider>
) )

View File

@@ -125,3 +125,7 @@
display: grid; display: grid;
gap: var(--Spacing-x-half); gap: var(--Spacing-x-half);
} }
.redeemButton {
width: 100%;
}

View File

@@ -5,7 +5,7 @@ import { createContext, useCallback, useContext, useEffect } from "react"
import { logger } from "@scandic-hotels/common/logger" import { logger } from "@scandic-hotels/common/logger"
import { trpc } from "@scandic-hotels/trpc/client" import { trpc } from "@scandic-hotels/trpc/client"
import { getFirstRedeemableCoupon } from "@/utils/rewards" import { getFirstRedeemableCoupon, isTierType } from "@/utils/rewards"
import type { Reward } from "@scandic-hotels/trpc/types/rewards" import type { Reward } from "@scandic-hotels/trpc/types/rewards"
@@ -34,9 +34,21 @@ export default function useRedeemFlow() {
const onRedeem = useCallback( const onRedeem = useCallback(
(reward: Reward) => { (reward: Reward) => {
const coupon = getFirstRedeemableCoupon(reward) let couponCode: string | undefined
if (isTierType(reward.rewardType)) {
couponCode = undefined
} else {
const coupon = getFirstRedeemableCoupon(reward)
if (!coupon) {
logger.error("No redeemable coupon found for reward", reward)
return
}
couponCode = coupon.couponCode
}
update.mutate( update.mutate(
{ rewardId: reward.id, couponCode: coupon.couponCode }, { rewardId: reward.id, couponCode },
{ {
onSuccess() { onSuccess() {
setRedeemStep("redeemed") setRedeemStep("redeemed")