diff --git a/src/api/auth.ts b/src/api/auth.ts index d35b9d6..297fb77 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -290,7 +290,7 @@ export const authApi = { return response.data; }, - pollDeepLinkToken: async (token: string): Promise => { + pollDeepLinkToken: async (token: string, campaignSlug?: string | null): Promise => { // validateStatus: only treat 200 as success. // Server returns 202 for "pending" and 410 for "expired" — // these must reject so the polling catch-block can handle them. @@ -299,7 +299,7 @@ export const authApi = { // which triggers checkAdminStatus() → 401 → safeRedirectToLogin() → infinite reload. const response = await apiClient.post( '/cabinet/auth/deeplink/poll', - { token }, + { token, campaign_slug: campaignSlug || undefined }, { validateStatus: (status) => status === 200 }, ); return response.data; diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 197015c..71bcc99 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -6,6 +6,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'; interface TelegramLoginButtonProps { referralCode?: string; @@ -34,6 +35,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink); + // Capture campaign slug once on mount (before any retry clears it) + const capturedCampaignRef = useRef(null); + const codesConsumedRef = useRef(false); + const { data: widgetConfig } = useQuery({ queryKey: ['telegram-widget-config'], queryFn: brandingApi.getTelegramWidgetConfig, @@ -239,6 +244,17 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto } try { + // Consume campaign slug ONCE (first call only). + // Clears localStorage on first call, so subsequent retries reuse the ref. + // Note: referral code is NOT consumed here — deep link auth is for existing + // 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(); + codesConsumedRef.current = true; + } + const capturedCampaign = capturedCampaignRef.current; + const response = await authApi.requestDeepLinkToken(); const { token, bot_username, expires_in } = response; setDeepLinkToken(token); @@ -249,7 +265,8 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto const poll = async () => { if (!mountedRef.current) return; try { - await loginWithDeepLink(token); + // Deep link auth is for existing bot users — only campaign_slug applies + await loginWithDeepLink(token, capturedCampaign); // Success — auth store is updated, navigate if (expireTimeoutRef.current) { clearTimeout(expireTimeoutRef.current); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e32272e..eb3e628 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -22,7 +22,7 @@ import LanguageSwitcher from '../components/LanguageSwitcher'; import TelegramLoginButton from '../components/TelegramLoginButton'; import OAuthProviderIcon from '../components/OAuthProviderIcon'; import { saveOAuthState } from '../utils/oauth'; -import { consumeReferralCode, getPendingReferralCode } from '../utils/referral'; +import { getPendingReferralCode } from '../utils/referral'; export default function Login() { const { t } = useTranslation(); @@ -146,16 +146,6 @@ export default function Login() { } }; - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; - - // If email auth is disabled but user came with ref param, redirect to bot - useEffect(() => { - if (referralCode && emailAuthConfig?.enabled === false && botUsername) { - consumeReferralCode(); - window.location.href = `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`; - } - }, [referralCode, emailAuthConfig, botUsername]); - const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN'; const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'; const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; diff --git a/src/store/auth.ts b/src/store/auth.ts index 81b9602..8d0b841 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -45,7 +45,7 @@ interface AuthState { state: string, deviceId?: string | null, ) => Promise; - loginWithDeepLink: (token: string) => Promise; + loginWithDeepLink: (token: string, campaignSlug?: string | null) => Promise; registerWithEmail: ( email: string, password: string, @@ -322,8 +322,8 @@ export const useAuthStore = create()( await get().checkAdminStatus(); }, - loginWithDeepLink: async (token) => { - const response = await authApi.pollDeepLinkToken(token); + loginWithDeepLink: async (token, campaignSlug) => { + const response = await authApi.pollDeepLinkToken(token, campaignSlug); if (!response.access_token || !response.refresh_token) { throw new Error('Invalid auth response: missing tokens'); }