mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
74
src/utils/referral.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user