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

@@ -3,32 +3,44 @@ import type { AuthResponse, OAuthProvider, RegisterResponse, TokenResponse, User
export const authApi = {
// Telegram WebApp authentication
loginTelegram: async (initData: string): Promise<AuthResponse> => {
loginTelegram: async (initData: string, campaignSlug?: string | null): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram', {
init_data: initData,
campaign_slug: campaignSlug || undefined,
});
return response.data;
},
// Telegram Login Widget authentication
loginTelegramWidget: async (data: {
id: number;
first_name: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
hash: string;
}): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', data);
loginTelegramWidget: async (
data: {
id: number;
first_name: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
hash: string;
},
campaignSlug?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/widget', {
...data,
campaign_slug: campaignSlug || undefined,
});
return response.data;
},
// Email login
loginEmail: async (email: string, password: string): Promise<AuthResponse> => {
loginEmail: async (
email: string,
password: string,
campaignSlug?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/login', {
email,
password,
campaign_slug: campaignSlug || undefined,
});
return response.data;
},
@@ -62,8 +74,11 @@ export const authApi = {
},
// Verify email and get auth tokens
verifyEmail: async (token: string): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', { token });
verifyEmail: async (token: string, campaignSlug?: string | null): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/cabinet/auth/email/verify', {
token,
campaign_slug: campaignSlug || undefined,
});
return response.data;
},
@@ -146,10 +161,15 @@ export const authApi = {
},
// OAuth: callback (exchange code for tokens)
oauthCallback: async (provider: string, code: string, state: string): Promise<AuthResponse> => {
oauthCallback: async (
provider: string,
code: string,
state: string,
campaignSlug?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>(
`/cabinet/auth/oauth/${encodeURIComponent(provider)}/callback`,
{ code, state },
{ code, state, campaign_slug: campaignSlug || undefined },
);
return response.data;
},

View File

@@ -44,6 +44,7 @@ export interface CampaignDetail {
created_at: string;
updated_at: string | null;
deep_link: string | null;
web_link: string | null;
}
export interface CampaignCreateRequest {
@@ -104,6 +105,7 @@ export interface CampaignStatistics {
conversion_rate: number;
trial_conversion_rate: number;
deep_link: string | null;
web_link: string | null;
}
export interface CampaignRegistrationItem {