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

@@ -0,0 +1,46 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/auth';
import { useToast } from './Toast';
export default function CampaignBonusNotifier() {
const { t } = useTranslation();
const { showToast } = useToast();
const bonus = useAuthStore((s) => s.pendingCampaignBonus);
const clearBonus = useAuthStore((s) => s.clearCampaignBonus);
useEffect(() => {
if (!bonus) return;
let message: string | null = null;
if (bonus.bonus_type === 'balance' && bonus.balance_kopeks > 0) {
message = t('campaignBonus.balance', {
amount: (bonus.balance_kopeks / 100).toFixed(0),
name: bonus.campaign_name,
});
} else if (bonus.bonus_type === 'subscription' && bonus.subscription_days) {
message = t('campaignBonus.subscription', {
days: bonus.subscription_days,
name: bonus.campaign_name,
});
} else if (bonus.bonus_type === 'tariff' && bonus.tariff_name) {
message = t('campaignBonus.tariff', {
tariff: bonus.tariff_name,
name: bonus.campaign_name,
});
}
if (message) {
showToast({
type: 'success',
title: t('campaignBonus.title'),
message,
duration: 8000,
});
}
clearBonus();
}, [bonus, clearBonus, showToast, t]);
return null;
}

View File

@@ -16,6 +16,7 @@ import { cn } from '@/lib/utils';
import { UI } from '@/config/constants';
import WebSocketNotifications from '@/components/WebSocketNotifications';
import CampaignBonusNotifier from '@/components/CampaignBonusNotifier';
import SuccessNotificationModal from '@/components/SuccessNotificationModal';
import LanguageSwitcher from '@/components/LanguageSwitcher';
import TicketNotificationBell from '@/components/TicketNotificationBell';
@@ -281,6 +282,7 @@ export function AppShell({ children }: AppShellProps) {
{/* Global components */}
<WebSocketNotifications />
<CampaignBonusNotifier />
<SuccessNotificationModal />
{/* Desktop Header */}