Merge pull request #229 from BEDOLAGA-DEV/dev

dev
This commit is contained in:
Egor
2026-02-19 02:14:10 +03:00
committed by GitHub
5 changed files with 129 additions and 15 deletions

View File

@@ -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<AuthResponse> => {
loginTelegram: async (
initData: string,
campaignSlug?: string | null,
referralCode?: string | null,
): Promise<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/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<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/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<AuthResponse> => {
const response = await apiClient.post<AuthResponse>('/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<AuthResponse> => {
const response = await apiClient.post<AuthResponse>(
`/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;
},

View File

@@ -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<HTMLDivElement>(null);
@@ -51,7 +55,11 @@ export default function TelegramLoginButton({ botUsername }: TelegramLoginButton
<div className="text-center">
<p className="mb-2 text-xs text-gray-500">{t('auth.orOpenInApp')}</p>
<a
href={`https://t.me/${botUsername}`}
href={
referralCode
? `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`
: `https://t.me/${botUsername}`
}
target="_blank"
rel="noopener noreferrer"
className="text-telegram-blue inline-flex items-center text-sm hover:underline"

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useNavigate, useLocation, useSearchParams } from 'react-router';
import { useNavigate, useLocation } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '../store/auth';
@@ -20,12 +20,12 @@ import LanguageSwitcher from '../components/LanguageSwitcher';
import TelegramLoginButton from '../components/TelegramLoginButton';
import OAuthProviderIcon from '../components/OAuthProviderIcon';
import { saveOAuthState } from './OAuthCallback';
import { consumeReferralCode, getPendingReferralCode } from '../utils/referral';
export default function Login() {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const {
isAuthenticated,
isLoading: isAuthInitializing,
@@ -34,8 +34,8 @@ export default function Login() {
registerWithEmail,
} = useAuthStore();
// Extract referral code from URL
const referralCode = searchParams.get('ref') || '';
// Get referral code from localStorage (captured from ?ref= param at module level in auth store)
const referralCode = getPendingReferralCode() || '';
const [authMode, setAuthMode] = useState<'login' | 'register'>(() =>
referralCode ? 'register' : 'login',
@@ -140,6 +140,7 @@ export default function Login() {
// 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]);
@@ -474,7 +475,10 @@ export default function Login() {
</p>
</div>
) : (
<TelegramLoginButton botUsername={botUsername} />
<TelegramLoginButton
botUsername={botUsername}
referralCode={referralCode || undefined}
/>
)}
</div>

View File

@@ -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<AuthState>()(
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<AuthState>()(
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<AuthState>()(
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<AuthState>()(
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<AuthState>()(
// 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<AuthState>()(
),
);
// 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();

74
src/utils/referral.ts Normal file
View File

@@ -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();
}