diff --git a/src/components/banners/TopBanner.tsx b/src/components/banners/TopBanner.tsx
index 5d0e1cc13..38cb11075 100644
--- a/src/components/banners/TopBanner.tsx
+++ b/src/components/banners/TopBanner.tsx
@@ -52,15 +52,10 @@ const TopBanner = ({ isBannerFixed, lang: pageLang }: TopBannerProps) => {
{textContent.title.normalText}
-
+
{textContent.title.cta}
-
+
diff --git a/src/hooks/usePricing.ts b/src/hooks/usePricing.ts
index 12f7c3fde..e21530fca 100644
--- a/src/hooks/usePricing.ts
+++ b/src/hooks/usePricing.ts
@@ -1,9 +1,11 @@
-/* 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';
import { useEffect, useReducer } from 'react';
import { PromoCodeName, PromoCodeProps } from '@/lib/types';
+import { notificationService } from '@/components/Snackbar';
+
+type LifetimeCoupons = Record;
type UsePricingOptions = {
couponCode?: PromoCodeName;
@@ -22,140 +24,155 @@ interface UseStripeProductsAndCurrencyResponse {
businessCoupon?: PromoCodeProps;
lifetimeCoupons?: any;
products?: ProductsDataProps;
+ pricesError: boolean;
+}
+
+interface PricesData {
+ products?: ProductsDataProps;
+ currency: string;
+ currencyValue: string;
+ lifetimeCoupons?: any;
+ pricesError: boolean;
+}
+
+interface CouponsData {
+ coupon?: PromoCodeProps;
+ lifetimeCoupon?: PromoCodeProps;
+ businessCoupon?: PromoCodeProps;
}
-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 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 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,
+};
+
+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;
}
};
+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);
+
+ 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 => {
+ return stripeService.getLifetimeCoupons();
+};
+
+const loadCoupon = async (couponName?: PromoCodeName): Promise => {
+ if (!couponName) return undefined;
+
+ try {
+ return await stripeService.getCoupon(couponName);
+ } catch (error) {
+ console.warn(`Coupon ${couponName} could not be loaded:`, error);
+ return undefined;
+ }
+};
+
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 [state, dispatch] = useReducer(reducer, initialState);
- 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);
- }
+ useEffect(() => {
+ let cancelled = false;
- dispatch({ type: 'SET_CURRENCY', payload: '€' });
- dispatch({ type: 'SET_CURRENCY_VALUE', payload: 'eur' });
- }
+ Promise.allSettled([
+ loadPrices(currencySpecified),
+ fetchLifetimeCoupons ? loadLifetimeCoupons() : Promise.resolve(undefined),
+ ]).then(([pricesResult, lifetimeCouponsResult]) => {
+ if (cancelled) return;
- if (fetchLifetimeCoupons) {
- try {
- const lifetimeCoupons = await stripeService.getLifetimeCoupons();
- dispatch({ type: 'SET_LIFETIME_COUPONS', payload: lifetimeCoupons });
- } catch (error) {
- console.error('Error fetching lifetime coupons:', error);
- }
- }
-
- if (couponCode) {
- try {
- const coupon = await stripeService.getCoupon(couponCode);
- dispatch({ type: 'SET_COUPON', payload: coupon });
- } catch (err) {
- console.error('Error fetching coupon:', err);
+ const pricesError = pricesResult.status === 'rejected';
+
+ if (pricesError) {
+ console.error('Error loading prices:', pricesResult.reason);
+ notificationService.openErrorToast(PRICES_ERROR_MESSAGE);
}
- }
- if (couponCodeForLifetime) {
- const lifetimeCoupon = await stripeService.getCoupon(couponCodeForLifetime);
+ if (lifetimeCouponsResult.status === 'rejected') {
+ console.error('Error fetching lifetime coupons:', lifetimeCouponsResult.reason);
+ }
- dispatch({ type: 'SET_COUPON_LIFETIME', payload: lifetimeCoupon });
- }
+ const prices = pricesResult.status === 'fulfilled' ? pricesResult.value : DEFAULT_CURRENCY;
+ const lifetimeCoupons = lifetimeCouponsResult.status === 'fulfilled' ? lifetimeCouponsResult.value : undefined;
- 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);
- }
- }
- };
+ dispatch({ type: 'SET_PRICES', payload: { ...prices, lifetimeCoupons, pricesError } });
+ }).catch((err) => console.error('Error loading prices:', err));
- useEffect(() => {
- fetchData().then(() => {
- dispatch({ type: 'SET_LOADING_CARDS', payload: false });
- });
+ 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.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 } });
+ }).catch((err) => console.error('Error loading coupons:', err));
+
+ 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,
businessCoupon: state.businessCoupon,
lifetimeCoupon: state.lifetimeCoupon,
lifetimeCoupons: state.lifetimeCoupons,
+ pricesError: state.pricesError,
};
}
diff --git a/src/lib/helpers/campaigns.ts b/src/lib/helpers/campaigns.ts
index 2b3bc92d4..49ac48d30 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 bab919aa6..e2d6a035d 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' });
}
}
diff --git a/src/services/currency.service.ts b/src/services/currency.service.ts
index bc898db7d..607be13ea 100644
--- a/src/services/currency.service.ts
+++ b/src/services/currency.service.ts
@@ -46,9 +46,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 253dcb9a0..3bd4394ae 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);