diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 9ef5c49..8d1f678 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -7,7 +7,7 @@ import { brandingApi, type TelegramWidgetConfig } from '../api/branding'; import { authApi } from '../api/auth'; import { useAuthStore } from '../store/auth'; import { useNavigate } from 'react-router'; -import { consumeCampaignSlug } from '../utils/campaign'; +import { getPendingCampaignSlug } from '../utils/campaign'; import { copyToClipboard } from '../utils/clipboard'; interface TelegramLoginButtonProps { @@ -253,7 +253,7 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto // bot users where referrals don't apply. Leaving it in localStorage allows // other auth methods (OIDC, widget) to pick it up if the user switches paths. if (!codesConsumedRef.current) { - capturedCampaignRef.current = consumeCampaignSlug(); + capturedCampaignRef.current = getPendingCampaignSlug(); codesConsumedRef.current = true; } const capturedCampaign = capturedCampaignRef.current; diff --git a/src/hooks/useAnalyticsCounters.ts b/src/hooks/useAnalyticsCounters.ts index ddaa5d6..fdf4660 100644 --- a/src/hooks/useAnalyticsCounters.ts +++ b/src/hooks/useAnalyticsCounters.ts @@ -24,7 +24,8 @@ function injectYandexMetrika(counterId: string) { ym(${counterId}, "init", { clickmap:true, trackLinks:true, - accurateTrackBounce:true + accurateTrackBounce:true, + webvisor:true }); `; document.head.appendChild(script); diff --git a/src/locales/en.json b/src/locales/en.json index 1221050..e800455 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -677,6 +677,8 @@ "errors": { "not_found": "Promo code not found", "expired": "Promo code has expired", + "inactive": "Promo code is deactivated", + "not_yet_valid": "Promo code is not yet active", "used": "Promo code has already been used", "already_used_by_user": "You have already used this promo code", "user_not_found": "User not found", diff --git a/src/locales/fa.json b/src/locales/fa.json index 0b3c251..8a5c66a 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -524,6 +524,8 @@ "errors": { "not_found": "کد تخفیف یافت نشد", "expired": "کد تخفیف منقضی شده", + "inactive": "کد تخفیف غیرفعال شده", + "not_yet_valid": "کد تخفیف هنوز فعال نشده", "used": "کد تخفیف قبلاً استفاده شده", "already_used_by_user": "شما قبلاً از این کد استفاده کرده‌اید", "user_not_found": "کاربر یافت نشد", diff --git a/src/locales/ru.json b/src/locales/ru.json index fdc4051..f875bb9 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -705,6 +705,8 @@ "errors": { "not_found": "Промокод не найден", "expired": "Срок действия промокода истёк", + "inactive": "Промокод деактивирован", + "not_yet_valid": "Промокод ещё не начал действовать", "used": "Промокод уже использован", "already_used_by_user": "Вы уже использовали этот промокод", "user_not_found": "Пользователь не найден", diff --git a/src/locales/zh.json b/src/locales/zh.json index e851133..7781b78 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -524,6 +524,8 @@ "errors": { "not_found": "优惠码不存在", "expired": "优惠码已过期", + "inactive": "优惠码已停用", + "not_yet_valid": "优惠码尚未生效", "used": "优惠码已被使用", "already_used_by_user": "您已使用过此优惠码", "user_not_found": "用户不存在", diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 9744be4..8c4739d 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -43,6 +43,9 @@ const ChevronRightIcon = () => ( ); +// Settings that require SALES_MODE=tariffs to be visible +const TARIFF_MODE_SETTINGS = ['MULTI_TARIFF_ENABLED', 'MAX_ACTIVE_SUBSCRIPTIONS']; + export default function AdminSettings() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -85,9 +88,6 @@ export default function AdminSettings() { } } - // Settings that require SALES_MODE=tariffs to be visible - const TARIFF_MODE_SETTINGS = ['MULTI_TARIFF_ENABLED', 'MAX_ACTIVE_SUBSCRIPTIONS']; - // Check if tariffs mode is active const isTariffsMode = useMemo(() => { if (!allSettings || !Array.isArray(allSettings)) return false; diff --git a/src/pages/Balance.tsx b/src/pages/Balance.tsx index 3879c2f..01e5677 100644 --- a/src/pages/Balance.tsx +++ b/src/pages/Balance.tsx @@ -177,15 +177,20 @@ export default function Balance() { } catch (error: unknown) { const axiosError = error as { response?: { data?: { detail?: string } } }; const errorDetail = axiosError.response?.data?.detail || 'server_error'; - const errorKey = errorDetail.toLowerCase().includes('not found') + const detail = errorDetail.toLowerCase(); + const errorKey = detail.includes('not found') ? 'not_found' - : errorDetail.toLowerCase().includes('expired') - ? 'expired' - : errorDetail.toLowerCase().includes('fully used') - ? 'used' - : errorDetail.toLowerCase().includes('already used') - ? 'already_used_by_user' - : 'server_error'; + : detail.includes('deactivated') + ? 'inactive' + : detail.includes('not yet active') + ? 'not_yet_valid' + : detail.includes('expired') + ? 'expired' + : detail.includes('fully used') + ? 'used' + : detail.includes('already used') + ? 'already_used_by_user' + : 'server_error'; setPromocodeError(t(`balance.promocode.errors.${errorKey}`)); setPromoSelectSubs(null); setPromoSelectCode(null); diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx index 0d9fd60..f178001 100644 --- a/src/pages/SubscriptionPurchase.tsx +++ b/src/pages/SubscriptionPurchase.tsx @@ -873,7 +873,7 @@ export default function SubscriptionPurchase() { const dailyPrice = tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0; const originalDailyPrice = tariff.original_daily_price_kopeks || 0; - if (dailyPrice > 0) { + if (dailyPrice > 0 || originalDailyPrice > 0) { const promoDaily = applyPromoDiscount( dailyPrice, originalDailyPrice > dailyPrice ? originalDailyPrice : undefined, diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx index 4ade266..4b643a5 100644 --- a/src/pages/VerifyEmail.tsx +++ b/src/pages/VerifyEmail.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { authApi } from '../api/auth'; import { useAuthStore } from '../store/auth'; import { useShallow } from 'zustand/shallow'; -import { consumeCampaignSlug } from '../utils/campaign'; +import { consumeCampaignSlug, getPendingCampaignSlug } from '../utils/campaign'; import { tokenStorage } from '../utils/token'; import LanguageSwitcher from '../components/LanguageSwitcher'; @@ -39,8 +39,9 @@ export default function VerifyEmail() { const verify = async () => { try { - const campaignSlug = consumeCampaignSlug(); + const campaignSlug = getPendingCampaignSlug(); const response = await authApi.verifyEmail(token, campaignSlug); + consumeCampaignSlug(); // Save tokens and log user in tokenStorage.setTokens(response.access_token, response.refresh_token); setTokens(response.access_token, response.refresh_token); diff --git a/src/store/auth.ts b/src/store/auth.ts index 8d0b841..abea27d 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -3,8 +3,16 @@ import { persist } from 'zustand/middleware'; import type { CampaignBonusInfo, RegisterResponse, User } from '../types'; import { authApi } from '../api/auth'; import { apiClient } from '../api/client'; -import { captureCampaignFromUrl, consumeCampaignSlug } from '../utils/campaign'; -import { captureReferralFromUrl, consumeReferralCode } from '../utils/referral'; +import { + captureCampaignFromUrl, + consumeCampaignSlug, + getPendingCampaignSlug, +} from '../utils/campaign'; +import { + captureReferralFromUrl, + consumeReferralCode, + getPendingReferralCode, +} from '../utils/referral'; import { tokenStorage, isTokenValid, tokenRefreshManager } from '../utils/token'; import { usePermissionStore } from './permissions'; @@ -241,9 +249,12 @@ export const useAuthStore = create()( }, loginWithTelegram: async (initData) => { - const campaignSlug = consumeCampaignSlug(); - const referralCode = consumeReferralCode(); + const campaignSlug = getPendingCampaignSlug(); + const referralCode = getPendingReferralCode(); const response = await authApi.loginTelegram(initData, campaignSlug, referralCode); + // Clear only after successful auth — retry keeps the slugs + consumeCampaignSlug(); + consumeReferralCode(); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -256,9 +267,11 @@ export const useAuthStore = create()( }, loginWithTelegramWidget: async (data) => { - const campaignSlug = consumeCampaignSlug(); - const referralCode = consumeReferralCode(); + const campaignSlug = getPendingCampaignSlug(); + const referralCode = getPendingReferralCode(); const response = await authApi.loginTelegramWidget(data, campaignSlug, referralCode); + consumeCampaignSlug(); + consumeReferralCode(); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -271,9 +284,11 @@ export const useAuthStore = create()( }, loginWithTelegramOIDC: async (idToken) => { - const campaignSlug = consumeCampaignSlug(); - const referralCode = consumeReferralCode(); + const campaignSlug = getPendingCampaignSlug(); + const referralCode = getPendingReferralCode(); const response = await authApi.loginTelegramOIDC(idToken, campaignSlug, referralCode); + consumeCampaignSlug(); + consumeReferralCode(); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -286,9 +301,11 @@ export const useAuthStore = create()( }, loginWithEmail: async (email, password) => { - const campaignSlug = consumeCampaignSlug(); - const referralCode = consumeReferralCode(); + const campaignSlug = getPendingCampaignSlug(); + const referralCode = getPendingReferralCode(); const response = await authApi.loginEmail(email, password, campaignSlug, referralCode); + consumeCampaignSlug(); + consumeReferralCode(); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -301,8 +318,8 @@ export const useAuthStore = create()( }, loginWithOAuth: async (provider, code, state, deviceId) => { - const campaignSlug = consumeCampaignSlug(); - const referralCode = consumeReferralCode(); + const campaignSlug = getPendingCampaignSlug(); + const referralCode = getPendingReferralCode(); const response = await authApi.oauthCallback( provider, code, @@ -311,6 +328,8 @@ export const useAuthStore = create()( campaignSlug, referralCode, ); + consumeCampaignSlug(); + consumeReferralCode(); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -339,7 +358,7 @@ export const useAuthStore = create()( }, registerWithEmail: async (email, password, firstName, referralCode) => { - const code = referralCode || consumeReferralCode() || undefined; + const code = referralCode || getPendingReferralCode() || undefined; const response = await authApi.registerEmailStandalone({ email, password, @@ -347,6 +366,7 @@ export const useAuthStore = create()( language: navigator.language.split('-')[0] || 'ru', referral_code: code, }); + consumeReferralCode(); return response; }, }),