Merged in fix/LOY-137-fetch-surprises-client (pull request #1363)

fix(LOY-137): now fetches surprises client side in intervals

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Christian Andolf
2025-02-21 14:59:56 +00:00
4 changed files with 130 additions and 110 deletions

View File

@@ -24,10 +24,13 @@ import Slide from "./Slide"
import styles from "./surprises.module.css" import styles from "./surprises.module.css"
import type { SurprisesProps } from "@/types/components/blocks/surprises" import type { SurprisesProps } from "@/types/components/blocks/surprises"
import type { Surprise } from "@/server/routers/contentstack/reward/output"
const MotionModal = motion(Modal) const MotionModal = motion(Modal)
export default function SurprisesNotification({ surprises }: SurprisesProps) { export default function SurprisesNotification({
surprises: initialData,
}: SurprisesProps) {
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
const pathname = usePathname() const pathname = usePathname()
@@ -35,6 +38,20 @@ export default function SurprisesNotification({ surprises }: SurprisesProps) {
const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0]) const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0])
const [showSurprises, setShowSurprises] = useState(false) const [showSurprises, setShowSurprises] = useState(false)
const utils = trpc.useUtils() const utils = trpc.useUtils()
const { data: surprises } = trpc.contentstack.rewards.surprises.useQuery<
Surprise[]
>(
{
lang,
},
{
initialData,
refetchInterval: 1000 * 60 * 5, // every 5 minutes
refetchIntervalInBackground: false,
}
)
const unwrap = trpc.contentstack.rewards.unwrap.useMutation({ const unwrap = trpc.contentstack.rewards.unwrap.useMutation({
onSuccess: () => { onSuccess: () => {
utils.contentstack.rewards.current.invalidate({ lang }) utils.contentstack.rewards.current.invalidate({ lang })

View File

@@ -223,6 +223,10 @@ export type RewardWithRedeem = CMSRewardWithRedeem & {
couponCode: string | undefined couponCode: string | undefined
} }
export interface Surprise extends Omit<Reward, "operaRewardId" | "couponCode"> {
coupons: { couponCode?: string | undefined; expiresAt?: string }[]
}
// New endpoint related types and schemas. // New endpoint related types and schemas.
const BaseReward = z.object({ const BaseReward = z.object({
title: z.string().optional(), title: z.string().optional(),

View File

@@ -19,6 +19,7 @@ import {
} from "./input" } from "./input"
import { import {
type Reward, type Reward,
type Surprise,
validateApiRewardSchema, validateApiRewardSchema,
validateCategorizedRewardsSchema, validateCategorizedRewardsSchema,
} from "./output" } from "./output"
@@ -293,121 +294,123 @@ export const rewardQueryRouter = router({
return { rewards } return { rewards }
}), }),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { surprises: contentStackBaseWithProtectedProcedure
getCurrentRewardCounter.add(1) .input(langInput.optional()) // lang is required for client, but not for server
.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
const endpoint = isNewEndpoint const endpoint = isNewEndpoint
? api.endpoints.v1.Profile.Reward.reward ? api.endpoints.v1.Profile.Reward.reward
: api.endpoints.v1.Profile.reward : api.endpoints.v1.Profile.reward
const apiResponse = await api.get(endpoint, { const apiResponse = await api.get(endpoint, {
cache: undefined, cache: undefined,
headers: { headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`, Authorization: `Bearer ${ctx.session.token.access_token}`,
}, },
next: { revalidate: ONE_HOUR }, next: { revalidate: ONE_HOUR },
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
}) })
console.error(
"api.reward error ", if (!apiResponse.ok) {
JSON.stringify({ const text = await apiResponse.text()
error: { getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status, status: apiResponse.status,
statusText: apiResponse.statusText, statusText: apiResponse.statusText,
text, text,
}, }),
}) })
) console.error(
return null "api.reward error ",
} JSON.stringify({
error: {
const data = await apiResponse.json() status: apiResponse.status,
const validatedApiRewards = isNewEndpoint statusText: apiResponse.statusText,
? validateCategorizedRewardsSchema.safeParse(data) text,
: validateApiRewardSchema.safeParse(data) },
})
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {
locale: ctx.lang,
error_type: "validation_error",
error: JSON.stringify(validatedApiRewards.error),
})
console.error(validatedApiRewards.error)
console.error(
"contentstack.surprises validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
return null
}
const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
getCurrentRewardSuccessCounter.add(1)
const surprises = validatedApiRewards.data
// TODO: Add predicates once legacy endpoints are removed
.filter((reward) => {
if (reward?.rewardType !== "Surprise") {
return false
}
if (!("coupon" in reward)) {
return false
}
const unwrappedCoupons =
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
if (unwrappedCoupons.length === 0) {
return false
}
return true
})
.map((surprise) => {
const reward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
) )
return null
}
if (!reward) { const data = await apiResponse.json()
return null const validatedApiRewards = isNewEndpoint
} ? validateCategorizedRewardsSchema.safeParse(data)
: validateApiRewardSchema.safeParse(data)
return { if (!validatedApiRewards.success) {
...reward, getCurrentRewardFailCounter.add(1, {
id: surprise.id, locale: ctx.lang,
rewardType: surprise.rewardType, error_type: "validation_error",
rewardTierLevel: undefined, error: JSON.stringify(validatedApiRewards.error),
redeemLocation: surprise.redeemLocation, })
coupons: "coupon" in surprise ? surprise.coupon || [] : [], console.error(validatedApiRewards.error)
} console.error(
}) "contentstack.surprises validation error",
.flatMap((surprises) => (surprises ? [surprises] : [])) JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
return null
}
return surprises const rewardIds = validatedApiRewards.data
}), .map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
getCurrentRewardSuccessCounter.add(1)
const surprises: Surprise[] = validatedApiRewards.data
// TODO: Add predicates once legacy endpoints are removed
.filter((reward) => {
if (reward?.rewardType !== "Surprise") {
return false
}
if (!("coupon" in reward)) {
return false
}
const unwrappedCoupons =
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
if (unwrappedCoupons.length === 0) {
return false
}
return true
})
.map((surprise) => {
const reward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
)
if (!reward) {
return null
}
return {
...reward,
id: surprise.id,
rewardType: surprise.rewardType,
rewardTierLevel: undefined,
redeemLocation: surprise.redeemLocation,
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
}
})
.flatMap((surprises) => (surprises ? [surprises] : []))
return surprises
}),
unwrap: protectedProcedure unwrap: protectedProcedure
.input(rewardsUpdateInput) .input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@@ -1,8 +1,4 @@
import type { Reward } from "@/server/routers/contentstack/reward/output" import type { Surprise } from "@/server/routers/contentstack/reward/output"
export interface Surprise extends Omit<Reward, "operaRewardId" | "couponCode"> {
coupons: { couponCode?: string | undefined; expiresAt?: string }[]
}
export interface SurprisesProps { export interface SurprisesProps {
surprises: Surprise[] surprises: Surprise[]