diff --git a/src/api/auth.ts b/src/api/auth.ts index 8b0605c..8082967 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -3,10 +3,15 @@ import type { AuthResponse, OAuthProvider, RegisterResponse, TokenResponse, User export const authApi = { // Telegram WebApp authentication - loginTelegram: async (initData: string, campaignSlug?: string | null): Promise => { + loginTelegram: async ( + initData: string, + campaignSlug?: string | null, + referralCode?: string | null, + ): Promise => { const response = await apiClient.post('/cabinet/auth/telegram', { init_data: initData, campaign_slug: campaignSlug || undefined, + referral_code: referralCode || undefined, }); return response.data; }, @@ -23,10 +28,12 @@ export const authApi = { hash: string; }, campaignSlug?: string | null, + referralCode?: string | null, ): Promise => { const response = await apiClient.post('/cabinet/auth/telegram/widget', { ...data, campaign_slug: campaignSlug || undefined, + referral_code: referralCode || undefined, }); return response.data; }, @@ -36,11 +43,13 @@ export const authApi = { email: string, password: string, campaignSlug?: string | null, + referralCode?: string | null, ): Promise => { const response = await apiClient.post('/cabinet/auth/email/login', { email, password, campaign_slug: campaignSlug || undefined, + referral_code: referralCode || undefined, }); return response.data; }, @@ -166,10 +175,16 @@ export const authApi = { code: string, state: string, campaignSlug?: string | null, + referralCode?: string | null, ): Promise => { const response = await apiClient.post( `/cabinet/auth/oauth/${encodeURIComponent(provider)}/callback`, - { code, state, campaign_slug: campaignSlug || undefined }, + { + code, + state, + campaign_slug: campaignSlug || undefined, + referral_code: referralCode || undefined, + }, ); return response.data; }, diff --git a/src/components/TelegramLoginButton.tsx b/src/components/TelegramLoginButton.tsx index 8d7723f..ebbd986 100644 --- a/src/components/TelegramLoginButton.tsx +++ b/src/components/TelegramLoginButton.tsx @@ -3,9 +3,13 @@ import { useTranslation } from 'react-i18next'; interface TelegramLoginButtonProps { botUsername: string; + referralCode?: string; } -export default function TelegramLoginButton({ botUsername }: TelegramLoginButtonProps) { +export default function TelegramLoginButton({ + botUsername, + referralCode, +}: TelegramLoginButtonProps) { const { t } = useTranslation(); const containerRef = useRef(null); @@ -51,7 +55,11 @@ export default function TelegramLoginButton({ botUsername }: TelegramLoginButton ) : ( - + )} diff --git a/src/store/auth.ts b/src/store/auth.ts index 31d77e5..fd94ce6 100644 --- a/src/store/auth.ts +++ b/src/store/auth.ts @@ -4,6 +4,7 @@ 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 { tokenStorage, isTokenValid, tokenRefreshManager } from '../utils/token'; export interface TelegramWidgetData { @@ -242,7 +243,8 @@ export const useAuthStore = create()( loginWithTelegram: async (initData) => { const campaignSlug = consumeCampaignSlug(); - const response = await authApi.loginTelegram(initData, campaignSlug); + const referralCode = consumeReferralCode(); + const response = await authApi.loginTelegram(initData, campaignSlug, referralCode); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -256,7 +258,8 @@ export const useAuthStore = create()( loginWithTelegramWidget: async (data) => { const campaignSlug = consumeCampaignSlug(); - const response = await authApi.loginTelegramWidget(data, campaignSlug); + const referralCode = consumeReferralCode(); + const response = await authApi.loginTelegramWidget(data, campaignSlug, referralCode); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -270,7 +273,8 @@ export const useAuthStore = create()( loginWithEmail: async (email, password) => { const campaignSlug = consumeCampaignSlug(); - const response = await authApi.loginEmail(email, password, campaignSlug); + const referralCode = consumeReferralCode(); + const response = await authApi.loginEmail(email, password, campaignSlug, referralCode); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -284,7 +288,14 @@ export const useAuthStore = create()( loginWithOAuth: async (provider, code, state) => { const campaignSlug = consumeCampaignSlug(); - const response = await authApi.oauthCallback(provider, code, state, campaignSlug); + const referralCode = consumeReferralCode(); + const response = await authApi.oauthCallback( + provider, + code, + state, + campaignSlug, + referralCode, + ); tokenStorage.setTokens(response.access_token, response.refresh_token); set({ accessToken: response.access_token, @@ -300,12 +311,13 @@ export const useAuthStore = create()( // Registration now returns message, not tokens // User must verify email before they can login // Campaign slug stays in localStorage — consumed during verify_email step + const code = referralCode || consumeReferralCode() || undefined; const response = await authApi.registerEmailStandalone({ email, password, first_name: firstName, language: navigator.language.split('-')[0] || 'ru', - referral_code: referralCode, + referral_code: code, }); return response; }, @@ -321,8 +333,9 @@ export const useAuthStore = create()( ), ); -// Capture campaign slug from URL before auth initialization +// Capture campaign slug and referral code from URL before auth initialization captureCampaignFromUrl(); +captureReferralFromUrl(); // Initialize auth on app load useAuthStore.getState().initialize(); diff --git a/src/utils/referral.ts b/src/utils/referral.ts new file mode 100644 index 0000000..b0dfbb1 --- /dev/null +++ b/src/utils/referral.ts @@ -0,0 +1,74 @@ +const REFERRAL_KEY = 'referral_code'; +const REFERRAL_TTL_KEY = 'referral_code_ttl'; +const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const CODE_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/; + +/** + * Get valid referral code from localStorage, clearing expired entries. + */ +function getValidCode(): string | null { + try { + const code = localStorage.getItem(REFERRAL_KEY); + if (!code) return null; + + const ttl = localStorage.getItem(REFERRAL_TTL_KEY); + if (!ttl || Number.isNaN(Number(ttl)) || Date.now() > Number(ttl)) { + localStorage.removeItem(REFERRAL_KEY); + localStorage.removeItem(REFERRAL_TTL_KEY); + return null; + } + + return code; + } catch { + return null; + } +} + +function clearCode(): void { + try { + localStorage.removeItem(REFERRAL_KEY); + localStorage.removeItem(REFERRAL_TTL_KEY); + } catch { + // localStorage unavailable + } +} + +/** + * Capture referral code from URL query param (?ref=), store in localStorage with TTL, + * and clean the URL. + */ +export function captureReferralFromUrl(): void { + try { + const params = new URLSearchParams(window.location.search); + const code = params.get('ref'); + if (!code || !CODE_PATTERN.test(code)) return; + + localStorage.setItem(REFERRAL_KEY, code); + localStorage.setItem(REFERRAL_TTL_KEY, String(Date.now() + TTL_MS)); + + // Clean URL + params.delete('ref'); + const newSearch = params.toString(); + const newUrl = + window.location.pathname + (newSearch ? `?${newSearch}` : '') + window.location.hash; + window.history.replaceState(null, '', newUrl); + } catch { + // localStorage or history API unavailable + } +} + +/** + * Consume (get + clear) the stored referral code. One-time use during auth. + */ +export function consumeReferralCode(): string | null { + const code = getValidCode(); + if (code) clearCode(); + return code; +} + +/** + * Read stored referral code without clearing it (for UI display). + */ +export function getPendingReferralCode(): string | null { + return getValidCode(); +}