From 0ba71f205df97f712fd49f1bc294bce7b41997dc Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 17 Feb 2025 15:39:47 +0100 Subject: [PATCH] fix(LOY-137): now fetches surprises client side in intervals --- components/MyPages/Surprises/Client.tsx | 19 +- server/routers/contentstack/reward/output.ts | 4 + server/routers/contentstack/reward/query.ts | 211 ++++++++++--------- types/components/blocks/surprises.ts | 6 +- 4 files changed, 130 insertions(+), 110 deletions(-) diff --git a/components/MyPages/Surprises/Client.tsx b/components/MyPages/Surprises/Client.tsx index 07d5ebb0f..a62a1140c 100644 --- a/components/MyPages/Surprises/Client.tsx +++ b/components/MyPages/Surprises/Client.tsx @@ -24,10 +24,13 @@ import Slide from "./Slide" import styles from "./surprises.module.css" import type { SurprisesProps } from "@/types/components/blocks/surprises" +import type { Surprise } from "@/server/routers/contentstack/reward/output" const MotionModal = motion(Modal) -export default function SurprisesNotification({ surprises }: SurprisesProps) { +export default function SurprisesNotification({ + surprises: initialData, +}: SurprisesProps) { const lang = useLang() const intl = useIntl() const pathname = usePathname() @@ -35,6 +38,20 @@ export default function SurprisesNotification({ surprises }: SurprisesProps) { const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0]) const [showSurprises, setShowSurprises] = useState(false) 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({ onSuccess: () => { utils.contentstack.rewards.current.invalidate({ lang }) diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts index 25e07a22e..3d6a0b20e 100644 --- a/server/routers/contentstack/reward/output.ts +++ b/server/routers/contentstack/reward/output.ts @@ -223,6 +223,10 @@ export type RewardWithRedeem = CMSRewardWithRedeem & { couponCode: string | undefined } +export interface Surprise extends Omit { + coupons: { couponCode?: string | undefined; expiresAt?: string }[] +} + // New endpoint related types and schemas. const BaseReward = z.object({ title: z.string().optional(), diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 51e111918..8b3fef759 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -19,6 +19,7 @@ import { } from "./input" import { type Reward, + type Surprise, validateApiRewardSchema, validateCategorizedRewardsSchema, } from "./output" @@ -293,121 +294,123 @@ export const rewardQueryRouter = router({ return { rewards } }), - surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => { - getCurrentRewardCounter.add(1) + surprises: contentStackBaseWithProtectedProcedure + .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 endpoint = isNewEndpoint - ? api.endpoints.v1.Profile.Reward.reward - : api.endpoints.v1.Profile.reward + const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT + const endpoint = isNewEndpoint + ? api.endpoints.v1.Profile.Reward.reward + : api.endpoints.v1.Profile.reward - const apiResponse = await api.get(endpoint, { - cache: undefined, - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - 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, - }), + const apiResponse = await api.get(endpoint, { + cache: undefined, + headers: { + Authorization: `Bearer ${ctx.session.token.access_token}`, + }, + next: { revalidate: ONE_HOUR }, }) - console.error( - "api.reward error ", - JSON.stringify({ - error: { + + 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, - }, + }), }) - ) - return null - } - - const data = await apiResponse.json() - const validatedApiRewards = isNewEndpoint - ? validateCategorizedRewardsSchema.safeParse(data) - : 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 + console.error( + "api.reward error ", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) ) + return null + } - if (!reward) { - return null - } + const data = await apiResponse.json() + const validatedApiRewards = isNewEndpoint + ? validateCategorizedRewardsSchema.safeParse(data) + : validateApiRewardSchema.safeParse(data) - return { - ...reward, - id: surprise.id, - rewardType: surprise.rewardType, - rewardTierLevel: undefined, - redeemLocation: surprise.redeemLocation, - coupons: "coupon" in surprise ? surprise.coupon || [] : [], - } - }) - .flatMap((surprises) => (surprises ? [surprises] : [])) + 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 + } - 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 .input(rewardsUpdateInput) .mutation(async ({ input, ctx }) => { diff --git a/types/components/blocks/surprises.ts b/types/components/blocks/surprises.ts index 45ae2a666..d4bdfdbbf 100644 --- a/types/components/blocks/surprises.ts +++ b/types/components/blocks/surprises.ts @@ -1,8 +1,4 @@ -import type { Reward } from "@/server/routers/contentstack/reward/output" - -export interface Surprise extends Omit { - coupons: { couponCode?: string | undefined; expiresAt?: string }[] -} +import type { Surprise } from "@/server/routers/contentstack/reward/output" export interface SurprisesProps { surprises: Surprise[]