From 6f6b37c0896dd061d689e437b8003931786a7271 Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 10:45:33 +0100 Subject: [PATCH 1/9] cached load coupon --- src/components/banners/TopBanner.tsx | 8 +- src/hooks/usePricing.ts | 197 +++++++++++------------- src/lib/helpers/campaigns.ts | 45 +++++- src/pages/api/stripe/get_coupons.ts | 1 + src/pages/api/stripe/stripe_products.ts | 1 + src/services/currency.service.ts | 12 +- src/services/stripe.service.ts | 47 ++++-- 7 files changed, 187 insertions(+), 124 deletions(-) diff --git a/src/components/banners/TopBanner.tsx b/src/components/banners/TopBanner.tsx index 03be3152d3..33539b0e51 100644 --- a/src/components/banners/TopBanner.tsx +++ b/src/components/banners/TopBanner.tsx @@ -49,14 +49,10 @@ const TopBanner = ({ isBannerFixed }: TopBannerProps) => {

{textContent.title.normalText}

- +

{textContent.title.cta}

- +
diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts index 12f7c3fde6..f1575db8b2 100644 --- a/src/hooks/usePricing.ts +++ b/src/hooks/usePricing.ts @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { currencyService } from '@/services/currency.service'; import { ProductsDataProps, stripeService } from '@/services/stripe.service'; @@ -24,132 +23,122 @@ interface UseStripeProductsAndCurrencyResponse { products?: ProductsDataProps; } -type ActionType = - | { type: 'SET_PRODUCTS'; payload: ProductsDataProps | undefined } - | { type: 'SET_LOADING_CARDS'; payload: boolean } - | { type: 'SET_CURRENCY'; payload: string } - | { type: 'SET_CURRENCY_VALUE'; payload: string } - | { type: 'SET_COUPON'; payload: PromoCodeProps | undefined } - | { type: 'SET_COUPON_LIFETIME'; payload: PromoCodeProps | undefined } - | { type: 'SET_BUSINESS_COUPON'; payload: PromoCodeProps | undefined } - | { type: 'SET_LIFETIME_COUPONS'; payload: any | undefined }; - -const reducer = (state: any, action: ActionType) => { +interface PricesData { + products?: ProductsDataProps; + currency: string; + currencyValue: string; + lifetimeCoupons?: any; +} + +interface CouponsData { + coupon?: PromoCodeProps; + lifetimeCoupon?: PromoCodeProps; + businessCoupon?: PromoCodeProps; +} + +interface PricingState extends PricesData, CouponsData { + pricesLoaded: boolean; + couponsLoaded: boolean; +} + +type ActionType = { type: 'SET_PRICES'; payload: PricesData } | { type: 'SET_COUPONS'; payload: CouponsData }; + +const DEFAULT_CURRENCY = { currency: '€', currencyValue: 'eur' }; + +const initialState: PricingState = { + ...DEFAULT_CURRENCY, + pricesLoaded: false, + couponsLoaded: false, +}; + +const reducer = (state: PricingState, action: ActionType): PricingState => { switch (action.type) { - case 'SET_PRODUCTS': - return { ...state, products: action.payload }; - case 'SET_LOADING_CARDS': - return { ...state, loadingCards: action.payload }; - case 'SET_CURRENCY': - return { ...state, currency: action.payload }; - case 'SET_CURRENCY_VALUE': - return { ...state, currencyValue: action.payload }; - case 'SET_COUPON': - return { ...state, coupon: action.payload }; - case 'SET_COUPON_LIFETIME': - return { ...state, lifetimeCoupon: action.payload }; - case 'SET_BUSINESS_COUPON': - return { ...state, businessCoupon: action.payload }; - case 'SET_LIFETIME_COUPONS': - return { ...state, lifetimeCoupons: action.payload }; + case 'SET_PRICES': + return { ...state, ...action.payload, pricesLoaded: true }; + case 'SET_COUPONS': + return { ...state, ...action.payload, couponsLoaded: true }; default: return state; } }; -function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurrencyResponse { - const { couponCode, couponCodeForLifetime, couponCodeForBusiness, currencySpecified, fetchLifetimeCoupons } = options; - const initialState = { - products: undefined, - loadingCards: true, - currency: '€', - coupon: undefined, - lifetimeCoupon: undefined, - businessCoupon: undefined, - lifetimeCoupons: undefined, - currencyValue: 'eur', - }; +const loadPrices = async (currencySpecified?: string): Promise> => { + try { + const { currency, currencyValue } = await currencyService.filterCurrencyByCountry(currencySpecified); + const products = await stripeService.getPrices(currencyValue); - const [state, dispatch] = useReducer(reducer, initialState); + return { products, currency, currencyValue }; + } catch (err) { + console.warn('Primary price fetch failed, falling back to EUR:', err); - const fetchData = async () => { try { - const res = await currencyService.filterCurrencyByCountry(currencySpecified); - const prices = await stripeService.getPrices(res.currencyValue); - - dispatch({ type: 'SET_PRODUCTS', payload: prices }); - dispatch({ type: 'SET_CURRENCY', payload: res.currency }); - dispatch({ type: 'SET_CURRENCY_VALUE', payload: res.currencyValue }); - } catch (err) { - console.warn('Primary price fetch failed, falling back to EUR:', err); - - try { - const prices = await stripeService.getPrices('eur'); - dispatch({ type: 'SET_PRODUCTS', payload: prices }); - } catch (error) { - console.error('Critical error: Failed to fetch fallback prices:', error); - } - - dispatch({ type: 'SET_CURRENCY', payload: '€' }); - dispatch({ type: 'SET_CURRENCY_VALUE', payload: 'eur' }); + const products = await stripeService.getPrices(DEFAULT_CURRENCY.currencyValue); + return { products, ...DEFAULT_CURRENCY }; + } catch (error) { + console.error('Critical error: Failed to fetch fallback prices:', error); + return { products: undefined, ...DEFAULT_CURRENCY }; } + } +}; - if (fetchLifetimeCoupons) { - try { - const lifetimeCoupons = await stripeService.getLifetimeCoupons(); - dispatch({ type: 'SET_LIFETIME_COUPONS', payload: lifetimeCoupons }); - } catch (error) { - console.error('Error fetching lifetime coupons:', error); - } - } +const loadLifetimeCoupons = async (): Promise => { + try { + return await stripeService.getLifetimeCoupons(); + } catch (error) { + console.error('Error fetching lifetime coupons:', error); + return undefined; + } +}; - if (couponCode) { - try { - const coupon = await stripeService.getCoupon(couponCode); - dispatch({ type: 'SET_COUPON', payload: coupon }); - } catch (err) { - console.error('Error fetching coupon:', err); - } - } +const loadCoupon = async (couponName?: PromoCodeName): Promise => { + if (!couponName) return undefined; - if (couponCodeForLifetime) { - const lifetimeCoupon = await stripeService.getCoupon(couponCodeForLifetime); + try { + return await stripeService.getCoupon(couponName); + } catch (error) { + console.warn(`Coupon ${couponName} could not be loaded:`, error); + return undefined; + } +}; - dispatch({ type: 'SET_COUPON_LIFETIME', payload: lifetimeCoupon }); - } +function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurrencyResponse { + const { couponCode, couponCodeForLifetime, couponCodeForBusiness, currencySpecified, fetchLifetimeCoupons } = options; - if (couponCodeForBusiness) { - try { - const businessCoupon = await stripeService.getCoupon(couponCodeForBusiness); - dispatch({ type: 'SET_BUSINESS_COUPON', payload: businessCoupon }); - } catch (err) { - console.warn('Invalid or expired business coupon ignored', err); - } - } - }; + const [state, dispatch] = useReducer(reducer, initialState); + // Prices and coupons don't depend on each other, so they are requested in parallel. useEffect(() => { - fetchData().then(() => { - dispatch({ type: 'SET_LOADING_CARDS', payload: false }); - }); + let cancelled = false; + + Promise.all([loadPrices(currencySpecified), fetchLifetimeCoupons ? loadLifetimeCoupons() : undefined]).then( + ([prices, lifetimeCoupons]) => { + if (!cancelled) dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons } }); + }, + ); + + return () => { + cancelled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (couponCode) { - stripeService - .getCoupon(couponCode) - .then((coupon) => { - dispatch({ type: 'SET_COUPON', payload: coupon }); - }) - .catch(() => { - // - }); - } - }, [couponCode]); + let cancelled = false; + + Promise.all([loadCoupon(couponCode), loadCoupon(couponCodeForLifetime), loadCoupon(couponCodeForBusiness)]).then( + ([coupon, lifetimeCoupon, businessCoupon]) => { + if (!cancelled) dispatch({ type: 'SET_COUPONS', payload: { coupon, lifetimeCoupon, businessCoupon } }); + }, + ); + + return () => { + cancelled = true; + }; + }, [couponCode, couponCodeForLifetime, couponCodeForBusiness]); return { products: state.products, - loadingCards: state.loadingCards, + loadingCards: !state.pricesLoaded || !state.couponsLoaded, currency: state.currency, currencyValue: state.currencyValue, coupon: state.coupon, diff --git a/src/lib/helpers/campaigns.ts b/src/lib/helpers/campaigns.ts index 2b3bc92d42..49ac48d301 100644 --- a/src/lib/helpers/campaigns.ts +++ b/src/lib/helpers/campaigns.ts @@ -21,7 +21,46 @@ export interface ActiveCampaigns { earlyReleaseCampaign?: Campaign; } -const CAMPAIGN_EXCEPTIONS: ReadonlySet = new Set([]); +const CAMPAIGN_EXCEPTIONS: ReadonlySet = new Set([ + PromoCodeName.blackmonth87, + PromoCodeName.blackmonthcn87, + PromoCodeName.cloudwardsbm87, + PromoCodeName.privacybm87, + PromoCodeName.heisebm87, + PromoCodeName.xatakabm87, + PromoCodeName.clubicbm87, + PromoCodeName.tdesbm87, + PromoCodeName.tdptbm87, + PromoCodeName.tditbm87, + PromoCodeName.gpesbm87, + PromoCodeName.gpitbm87, + PromoCodeName.chollobm87, + + PromoCodeName.blackfriday90, + PromoCodeName.blackfridaycn90, + PromoCodeName.cloudwardsbf90, + PromoCodeName.privacybf90, + PromoCodeName.heisebf90, + PromoCodeName.xatakabf90, + PromoCodeName.clubicbf90, + PromoCodeName.tdesbf90, + PromoCodeName.tdptbf90, + PromoCodeName.tditbf90, + PromoCodeName.gpesbf90, + PromoCodeName.gpitbf90, + PromoCodeName.chollobf90, + + PromoCodeName.cyberweek90, + PromoCodeName.cyberweekcn90, + PromoCodeName.cloudwardscm90, + PromoCodeName.privacycm90, + PromoCodeName.heisecm90, + PromoCodeName.xatakacm90, + PromoCodeName.clubiccm90, + PromoCodeName.gpescw90, + PromoCodeName.gpitcw90, + PromoCodeName.chollocw90, +]); export const EARLY_RELEASE_CAMPAIGNS: ReadonlySet = new Set([ Campaign.BlackFriday, @@ -36,8 +75,10 @@ export const isCampaign = (value: unknown): value is Campaign => export const hasEarlyRelease = (name: PromoCodeName): boolean => EARLY_RELEASE_CODES.has(name); +export const isCampaignException = (name: PromoCodeName): boolean => CAMPAIGN_EXCEPTIONS.has(name); + export const applyCampaign = (name: PromoCodeName, campaign?: Campaign): ResolvedPromoCode => { - if (!campaign || CAMPAIGN_EXCEPTIONS.has(name)) return name; + if (!campaign || isCampaignException(name)) return name; return `${name}${CAMPAIGN_SUFFIXES[campaign]}`; }; diff --git a/src/pages/api/stripe/get_coupons.ts b/src/pages/api/stripe/get_coupons.ts index bab919aa6e..60655e468d 100644 --- a/src/pages/api/stripe/get_coupons.ts +++ b/src/pages/api/stripe/get_coupons.ts @@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); //Return the coupon with its data + res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).send(promoCodeData); } else { res.status(405).end(); // Method Not Allowed diff --git a/src/pages/api/stripe/stripe_products.ts b/src/pages/api/stripe/stripe_products.ts index 4f5a4f05e6..999f558c1f 100644 --- a/src/pages/api/stripe/stripe_products.ts +++ b/src/pages/api/stripe/stripe_products.ts @@ -30,6 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) business: businessProductsData, }; + res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).json(productsData); } catch (err) { const error = err as Error; diff --git a/src/services/currency.service.ts b/src/services/currency.service.ts index 86c33ccc0e..c55b661e14 100644 --- a/src/services/currency.service.ts +++ b/src/services/currency.service.ts @@ -27,9 +27,15 @@ const objectStorageCurrencyValue = { brl: 'eur', }; -const getCountry = async () => { - const { data } = await axios.get(`${process.env.NEXT_PUBLIC_COUNTRY_API_URL}`); - return data; +let countryRequest: Promise<{ country?: string }> | undefined; + +const getCountry = () => { + if (!countryRequest) { + countryRequest = axios.get(`${process.env.NEXT_PUBLIC_COUNTRY_API_URL}`).then(({ data }) => data); + countryRequest.catch(() => (countryRequest = undefined)); + } + + return countryRequest; }; const filterCurrencyByCountry = async (currencySpecified?: string) => { diff --git a/src/services/stripe.service.ts b/src/services/stripe.service.ts index 253dcb9a09..3bd4394aea 100644 --- a/src/services/stripe.service.ts +++ b/src/services/stripe.service.ts @@ -3,7 +3,14 @@ import bytes from 'bytes'; import { currency as CURRENCY_MAP, currencyService } from './currency.service'; import { checkout, checkoutForPcComponentes } from '@/lib/auth'; import { PromoCodeName, PromoCodeProps } from '@/lib/types'; -import { ActiveCampaigns, ResolvedPromoCode, applyCampaign, hasEarlyRelease, isCampaign } from '@/lib/helpers/campaigns'; +import { + ActiveCampaigns, + ResolvedPromoCode, + applyCampaign, + hasEarlyRelease, + isCampaign, + isCampaignException, +} from '@/lib/helpers/campaigns'; import { getGclidFromURL, saveGclidToCookie } from '@/lib/cookies'; import { analyticsService } from './ga.services'; @@ -51,13 +58,19 @@ export interface ProductsDataProps { }; } -async function getPrices(currencySpecified?: string) { +const pricesRequests = new Map>(); + +async function getPrices(currencySpecified?: string): Promise { const currency = await getCurrency(currencySpecified); - const data = await fetchProductData(currency); - if (data) { - const transformedData = transformProductData(data.individuals, data.business); - return transformedData; + let request = pricesRequests.get(currency); + + if (!request) { + request = fetchProductData(currency).then((data) => (data ? transformProductData(data.individuals, data.business) : undefined)); + pricesRequests.set(currency, request); + request.then((prices) => prices ?? pricesRequests.delete(currency)).catch(() => pricesRequests.delete(currency)); } + + return request; } async function getCurrency(currencySpecified?: string): Promise { @@ -123,8 +136,6 @@ function transformProductData(individualsData: ProductValue[], businessData: Pro transform(individualsData, 'individuals'); transform(businessData, 'business'); - console.log(transformedData); - return transformedData; } @@ -155,7 +166,7 @@ function fetchActiveCampaigns(): Promise { return activeCampaigns; } -async function requestCoupon(couponName: ResolvedPromoCode): Promise { +async function fetchCoupon(couponName: ResolvedPromoCode): Promise { try { const res = await axios.get(`${window.origin}/api/stripe/get_coupons`, { params: { @@ -175,7 +186,25 @@ async function requestCoupon(couponName: ResolvedPromoCode): Promise>(); + +function requestCoupon(couponName: ResolvedPromoCode): Promise { + let request = couponRequests.get(couponName); + + if (!request) { + request = fetchCoupon(couponName); + couponRequests.set(couponName, request); + request.catch(() => couponRequests.delete(couponName)); + } + + return request; +} + async function getCoupon(couponName: PromoCodeName): Promise { + if (isCampaignException(couponName)) { + return requestCoupon(couponName); + } + const { campaign, earlyReleaseCampaign } = await fetchActiveCampaigns(); const resolvedName = applyCampaign(couponName, hasEarlyRelease(couponName) ? earlyReleaseCampaign : campaign); From c6d25458bef552fbaf32910493816be6f75e6ce5 Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 10:56:31 +0100 Subject: [PATCH 2/9] fix CypressTest Error --- src/services/currency.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/currency.service.ts b/src/services/currency.service.ts index c55b661e14..79b6dd4346 100644 --- a/src/services/currency.service.ts +++ b/src/services/currency.service.ts @@ -27,7 +27,7 @@ const objectStorageCurrencyValue = { brl: 'eur', }; -let countryRequest: Promise<{ country?: string }> | undefined; +let countryRequest: Promise<{ country: string }> | undefined; const getCountry = () => { if (!countryRequest) { From 4737a6ba262bd630098235491c8b4a79d52c5d9a Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 12:08:02 +0100 Subject: [PATCH 3/9] changes for sonarqube --- src/hooks/usePricing.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts index f1575db8b2..2367f446c4 100644 --- a/src/hooks/usePricing.ts +++ b/src/hooks/usePricing.ts @@ -110,11 +110,11 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren useEffect(() => { let cancelled = false; - Promise.all([loadPrices(currencySpecified), fetchLifetimeCoupons ? loadLifetimeCoupons() : undefined]).then( - ([prices, lifetimeCoupons]) => { + Promise.all([loadPrices(currencySpecified), fetchLifetimeCoupons ? loadLifetimeCoupons() : undefined]) + .then(([prices, lifetimeCoupons]) => { if (!cancelled) dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons } }); - }, - ); + }) + .catch((error) => console.error('Error loading prices:', error)); return () => { cancelled = true; @@ -125,11 +125,11 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren useEffect(() => { let cancelled = false; - Promise.all([loadCoupon(couponCode), loadCoupon(couponCodeForLifetime), loadCoupon(couponCodeForBusiness)]).then( - ([coupon, lifetimeCoupon, businessCoupon]) => { + Promise.all([loadCoupon(couponCode), loadCoupon(couponCodeForLifetime), loadCoupon(couponCodeForBusiness)]) + .then(([coupon, lifetimeCoupon, businessCoupon]) => { if (!cancelled) dispatch({ type: 'SET_COUPONS', payload: { coupon, lifetimeCoupon, businessCoupon } }); - }, - ); + }) + .catch((error) => console.error('Error loading coupons:', error)); return () => { cancelled = true; From e96841b3735834d90a0e7dc1019d09bdd315740e Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 13:02:14 +0100 Subject: [PATCH 4/9] remove cache header --- src/pages/api/stripe/get_coupons.ts | 1 - src/pages/api/stripe/stripe_products.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pages/api/stripe/get_coupons.ts b/src/pages/api/stripe/get_coupons.ts index 60655e468d..bab919aa6e 100644 --- a/src/pages/api/stripe/get_coupons.ts +++ b/src/pages/api/stripe/get_coupons.ts @@ -27,7 +27,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); //Return the coupon with its data - res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).send(promoCodeData); } else { res.status(405).end(); // Method Not Allowed diff --git a/src/pages/api/stripe/stripe_products.ts b/src/pages/api/stripe/stripe_products.ts index 999f558c1f..4f5a4f05e6 100644 --- a/src/pages/api/stripe/stripe_products.ts +++ b/src/pages/api/stripe/stripe_products.ts @@ -30,7 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) business: businessProductsData, }; - res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300'); res.status(200).json(productsData); } catch (err) { const error = err as Error; From 14f49ebd5034b6649e1edfb2a8c7bb62276991fd Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 13:18:27 +0100 Subject: [PATCH 5/9] added the change of refactored code as welll --- src/pages/api/stripe/get_coupons.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pages/api/stripe/get_coupons.ts b/src/pages/api/stripe/get_coupons.ts index bab919aa6e..e2d6a035dc 100644 --- a/src/pages/api/stripe/get_coupons.ts +++ b/src/pages/api/stripe/get_coupons.ts @@ -11,15 +11,21 @@ interface PromotionCode { export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { if (req.method === 'OPTIONS') { res.status(200).json({}); + return; } - if (req.method === 'GET') { - const couponName = req.query.couponName; - if (!couponName) { - res.status(404).end(); - return; - } + if (req.method !== 'GET') { + res.status(405).end(); + return; + } + + const couponName = req.query.couponName; + if (!couponName) { + res.status(404).end(); + return; + } + try { const { data: promoCodeData } = await axios.get(`${process.env.NEXT_PUBLIC_PAYMENTS_API}/promo-code-info`, { params: { promotionCode: couponName, @@ -28,7 +34,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) //Return the coupon with its data res.status(200).send(promoCodeData); - } else { - res.status(405).end(); // Method Not Allowed + } catch (err) { + const status = axios.isAxiosError(err) && err.response ? err.response.status : 502; + res.status(status).json({ message: 'Coupon not available' }); } } From 90833a46f205d461f311c86f88ebcc3520db6f96 Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Wed, 11 Nov 2026 15:14:15 +0100 Subject: [PATCH 6/9] remove comment --- src/hooks/usePricing.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts index 2367f446c4..e2c37c95b3 100644 --- a/src/hooks/usePricing.ts +++ b/src/hooks/usePricing.ts @@ -106,7 +106,6 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren const [state, dispatch] = useReducer(reducer, initialState); - // Prices and coupons don't depend on each other, so they are requested in parallel. useEffect(() => { let cancelled = false; From e0429f2a08017df9bae20f59ebf1d9b9dd77e804 Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Thu, 12 Nov 2026 10:29:17 +0100 Subject: [PATCH 7/9] changes in usePricing --- src/hooks/usePricing.ts | 79 ++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts index e2c37c95b3..60e5288f1b 100644 --- a/src/hooks/usePricing.ts +++ b/src/hooks/usePricing.ts @@ -3,6 +3,9 @@ import { currencyService } from '@/services/currency.service'; import { ProductsDataProps, stripeService } from '@/services/stripe.service'; import { useEffect, useReducer } from 'react'; import { PromoCodeName, PromoCodeProps } from '@/lib/types'; +import { notificationService } from '@/components/Snackbar'; + +type LifetimeCoupons = Record; type UsePricingOptions = { couponCode?: PromoCodeName; @@ -21,6 +24,7 @@ interface UseStripeProductsAndCurrencyResponse { businessCoupon?: PromoCodeProps; lifetimeCoupons?: any; products?: ProductsDataProps; + pricesError: boolean; } interface PricesData { @@ -28,6 +32,7 @@ interface PricesData { currency: string; currencyValue: string; lifetimeCoupons?: any; + pricesError: boolean; } interface CouponsData { @@ -44,9 +49,11 @@ interface PricingState extends PricesData, CouponsData { type ActionType = { type: 'SET_PRICES'; payload: PricesData } | { type: 'SET_COUPONS'; payload: CouponsData }; const DEFAULT_CURRENCY = { currency: '€', currencyValue: 'eur' }; +const PRICES_ERROR_MESSAGE = 'We could not load the prices. Please reload the page and try again.'; const initialState: PricingState = { ...DEFAULT_CURRENCY, + pricesError: false, pricesLoaded: false, couponsLoaded: false, }; @@ -62,32 +69,27 @@ const reducer = (state: PricingState, action: ActionType): PricingState => { } }; -const loadPrices = async (currencySpecified?: string): Promise> => { +const loadPrices = async (currencySpecified?: string): Promise> => { try { const { currency, currencyValue } = await currencyService.filterCurrencyByCountry(currencySpecified); const products = await stripeService.getPrices(currencyValue); + if (!products) throw new Error(`No prices available for ${currencyValue}`); + return { products, currency, currencyValue }; } catch (err) { console.warn('Primary price fetch failed, falling back to EUR:', err); - try { - const products = await stripeService.getPrices(DEFAULT_CURRENCY.currencyValue); - return { products, ...DEFAULT_CURRENCY }; - } catch (error) { - console.error('Critical error: Failed to fetch fallback prices:', error); - return { products: undefined, ...DEFAULT_CURRENCY }; - } + const products = await stripeService.getPrices(DEFAULT_CURRENCY.currencyValue); + + if (!products) throw new Error(`No prices available for ${DEFAULT_CURRENCY.currencyValue}`); + + return { products, ...DEFAULT_CURRENCY }; } }; -const loadLifetimeCoupons = async (): Promise => { - try { - return await stripeService.getLifetimeCoupons(); - } catch (error) { - console.error('Error fetching lifetime coupons:', error); - return undefined; - } +const loadLifetimeCoupons = async (): Promise => { + return stripeService.getLifetimeCoupons(); }; const loadCoupon = async (couponName?: PromoCodeName): Promise => { @@ -109,11 +111,28 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren useEffect(() => { let cancelled = false; - Promise.all([loadPrices(currencySpecified), fetchLifetimeCoupons ? loadLifetimeCoupons() : undefined]) - .then(([prices, lifetimeCoupons]) => { - if (!cancelled) dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons } }); - }) - .catch((error) => console.error('Error loading prices:', error)); + Promise.allSettled([ + loadPrices(currencySpecified), + fetchLifetimeCoupons ? loadLifetimeCoupons() : Promise.resolve(undefined), + ]).then(([pricesResult, lifetimeCouponsResult]) => { + if (cancelled) return; + + const pricesError = pricesResult.status === 'rejected'; + + if (pricesError) { + console.error('Error loading prices:', pricesResult.reason); + notificationService.openErrorToast(PRICES_ERROR_MESSAGE); + } + + if (lifetimeCouponsResult.status === 'rejected') { + console.error('Error fetching lifetime coupons:', lifetimeCouponsResult.reason); + } + + const prices = pricesResult.status === 'fulfilled' ? pricesResult.value : DEFAULT_CURRENCY; + const lifetimeCoupons = lifetimeCouponsResult.status === 'fulfilled' ? lifetimeCouponsResult.value : undefined; + + dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons, pricesError } }); + }); return () => { cancelled = true; @@ -124,11 +143,20 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren useEffect(() => { let cancelled = false; - Promise.all([loadCoupon(couponCode), loadCoupon(couponCodeForLifetime), loadCoupon(couponCodeForBusiness)]) - .then(([coupon, lifetimeCoupon, businessCoupon]) => { - if (!cancelled) dispatch({ type: 'SET_COUPONS', payload: { coupon, lifetimeCoupon, businessCoupon } }); - }) - .catch((error) => console.error('Error loading coupons:', error)); + Promise.allSettled([ + loadCoupon(couponCode), + loadCoupon(couponCodeForLifetime), + loadCoupon(couponCodeForBusiness), + ]).then((results) => { + if (cancelled) return; + + const [coupon, lifetimeCoupon, businessCoupon] = results.map((result) => { + if (result.status === 'rejected') console.error('Error loading coupons:', result.reason); + return result.status === 'fulfilled' ? result.value : undefined; + }); + + dispatch({ type: 'SET_COUPONS', payload: { coupon, lifetimeCoupon, businessCoupon } }); + }); return () => { cancelled = true; @@ -144,6 +172,7 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren businessCoupon: state.businessCoupon, lifetimeCoupon: state.lifetimeCoupon, lifetimeCoupons: state.lifetimeCoupons, + pricesError: state.pricesError, }; } From b9d5d16d5b03d86326acb0c9b9eb5231e0eab8c8 Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Thu, 12 Nov 2026 11:02:26 +0100 Subject: [PATCH 8/9] changes for sonarqube --- src/hooks/usePricing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts index 60e5288f1b..e21530fca6 100644 --- a/src/hooks/usePricing.ts +++ b/src/hooks/usePricing.ts @@ -132,7 +132,7 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren const lifetimeCoupons = lifetimeCouponsResult.status === 'fulfilled' ? lifetimeCouponsResult.value : undefined; dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons, pricesError } }); - }); + }).catch((err) => console.error('Error loading prices:', err)); return () => { cancelled = true; @@ -156,7 +156,7 @@ function usePricing(options: UsePricingOptions = {}): UseStripeProductsAndCurren }); dispatch({ type: 'SET_COUPONS', payload: { coupon, lifetimeCoupon, businessCoupon } }); - }); + }).catch((err) => console.error('Error loading coupons:', err)); return () => { cancelled = true; From 48dc6a11bb5e217457b446e1360fd8b03edcfe2a Mon Sep 17 00:00:00 2001 From: internxt-yu Date: Fri, 13 Nov 2026 12:09:51 +0100 Subject: [PATCH 9/9] fix topBanner --- src/components/banners/TopBanner.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/banners/TopBanner.tsx b/src/components/banners/TopBanner.tsx index a5da9444d7..38cb110755 100644 --- a/src/components/banners/TopBanner.tsx +++ b/src/components/banners/TopBanner.tsx @@ -53,12 +53,6 @@ const TopBanner = ({ isBannerFixed, lang: pageLang }: TopBannerProps) => { {textContent.title.normalText}

-

{textContent.title.cta}