feat: add web campaign links — capture, auth integration, bonus UI

- Add campaign.ts utility (capture from URL, localStorage with TTL, validation)
- Pass campaign_slug in all auth API methods (telegram, widget, email, oauth)
- Consume slug in auth store login methods, set pendingCampaignBonus state
- Add CampaignBonusNotifier component (toast on bonus, mounted in AppShell)
- Show web_link alongside bot deep_link in AdminCampaignStats
- Add CampaignBonusInfo type with union bonus_type
- Fix VerifyEmail: ref guard against StrictMode double-fire, setTimeout cleanup
- Fix AdminCampaignStats: setTimeout refs with cleanup, i18n-aware locale
- Wrap all localStorage access in try/catch (Safari private browsing safety)
- Add campaignBonus translations (ru/en/fa/zh)
This commit is contained in:
Fringg
2026-02-17 06:44:10 +03:00
parent 850124f469
commit e0dd21fd0b
13 changed files with 332 additions and 49 deletions

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useSearchParams, Link, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { authApi } from '../api/auth';
import { useAuthStore } from '../store/auth';
import { consumeCampaignSlug } from '../utils/campaign';
import { tokenStorage } from '../utils/token';
import LanguageSwitcher from '../components/LanguageSwitcher';
@@ -13,8 +14,11 @@ export default function VerifyEmail() {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [error, setError] = useState('');
const { setTokens, setUser, checkAdminStatus } = useAuthStore();
const hasVerified = useRef(false);
useEffect(() => {
if (hasVerified.current) return;
const token = searchParams.get('token');
if (!token) {
@@ -23,17 +27,24 @@ export default function VerifyEmail() {
return;
}
hasVerified.current = true;
let redirectTimer: ReturnType<typeof setTimeout>;
const verify = async () => {
try {
const response = await authApi.verifyEmail(token);
const campaignSlug = consumeCampaignSlug();
const response = await authApi.verifyEmail(token, campaignSlug);
// Save tokens and log user in
tokenStorage.setTokens(response.access_token, response.refresh_token);
setTokens(response.access_token, response.refresh_token);
setUser(response.user);
if (response.campaign_bonus) {
useAuthStore.setState({ pendingCampaignBonus: response.campaign_bonus });
}
checkAdminStatus();
setStatus('success');
// Redirect to dashboard after short delay
setTimeout(() => navigate('/', { replace: true }), 1500);
redirectTimer = setTimeout(() => navigate('/', { replace: true }), 1500);
} catch (err: unknown) {
setStatus('error');
const error = err as { response?: { data?: { detail?: string } } };
@@ -42,6 +53,8 @@ export default function VerifyEmail() {
};
verify();
return () => clearTimeout(redirectTimer);
}, [searchParams, t, navigate, setTokens, setUser, checkAdminStatus]);
return (