mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: referral system — stop cabinet redirect to Telegram, fix deep link code handling
- Remove redirect to Telegram bot when email auth disabled + referral code present (cabinet links should stay on cabinet, bot links should go to bot) - Remove referral_code from deep link auth (existing users can't get referrals) - Don't consume referralCode in deep link path — leave it in localStorage for OIDC/widget auth methods that actually send it to the backend - Consume campaign slug once into ref to survive retries (codesConsumedRef pattern) - Update loginWithDeepLink to only accept (token, campaignSlug) — no referralCode - Update pollDeepLinkToken API to match backend schema change
This commit is contained in:
@@ -290,7 +290,7 @@ export const authApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
pollDeepLinkToken: async (token: string): Promise<AuthResponse> => {
|
pollDeepLinkToken: async (token: string, campaignSlug?: string | null): Promise<AuthResponse> => {
|
||||||
// validateStatus: only treat 200 as success.
|
// validateStatus: only treat 200 as success.
|
||||||
// Server returns 202 for "pending" and 410 for "expired" —
|
// Server returns 202 for "pending" and 410 for "expired" —
|
||||||
// these must reject so the polling catch-block can handle them.
|
// these must reject so the polling catch-block can handle them.
|
||||||
@@ -299,7 +299,7 @@ export const authApi = {
|
|||||||
// which triggers checkAdminStatus() → 401 → safeRedirectToLogin() → infinite reload.
|
// which triggers checkAdminStatus() → 401 → safeRedirectToLogin() → infinite reload.
|
||||||
const response = await apiClient.post<AuthResponse>(
|
const response = await apiClient.post<AuthResponse>(
|
||||||
'/cabinet/auth/deeplink/poll',
|
'/cabinet/auth/deeplink/poll',
|
||||||
{ token },
|
{ token, campaign_slug: campaignSlug || undefined },
|
||||||
{ validateStatus: (status) => status === 200 },
|
{ validateStatus: (status) => status === 200 },
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { brandingApi, type TelegramWidgetConfig } from '../api/branding';
|
|||||||
import { authApi } from '../api/auth';
|
import { authApi } from '../api/auth';
|
||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { consumeCampaignSlug } from '../utils/campaign';
|
||||||
|
|
||||||
interface TelegramLoginButtonProps {
|
interface TelegramLoginButtonProps {
|
||||||
referralCode?: string;
|
referralCode?: string;
|
||||||
@@ -34,6 +35,10 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
|
|
||||||
const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink);
|
const loginWithDeepLink = useAuthStore((s) => s.loginWithDeepLink);
|
||||||
|
|
||||||
|
// Capture campaign slug once on mount (before any retry clears it)
|
||||||
|
const capturedCampaignRef = useRef<string | null>(null);
|
||||||
|
const codesConsumedRef = useRef(false);
|
||||||
|
|
||||||
const { data: widgetConfig } = useQuery<TelegramWidgetConfig>({
|
const { data: widgetConfig } = useQuery<TelegramWidgetConfig>({
|
||||||
queryKey: ['telegram-widget-config'],
|
queryKey: ['telegram-widget-config'],
|
||||||
queryFn: brandingApi.getTelegramWidgetConfig,
|
queryFn: brandingApi.getTelegramWidgetConfig,
|
||||||
@@ -239,6 +244,17 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Consume campaign slug ONCE (first call only).
|
||||||
|
// Clears localStorage on first call, so subsequent retries reuse the ref.
|
||||||
|
// Note: referral code is NOT consumed here — deep link auth is for existing
|
||||||
|
// bot users where referrals don't apply. Leaving it in localStorage allows
|
||||||
|
// other auth methods (OIDC, widget) to pick it up if the user switches paths.
|
||||||
|
if (!codesConsumedRef.current) {
|
||||||
|
capturedCampaignRef.current = consumeCampaignSlug();
|
||||||
|
codesConsumedRef.current = true;
|
||||||
|
}
|
||||||
|
const capturedCampaign = capturedCampaignRef.current;
|
||||||
|
|
||||||
const response = await authApi.requestDeepLinkToken();
|
const response = await authApi.requestDeepLinkToken();
|
||||||
const { token, bot_username, expires_in } = response;
|
const { token, bot_username, expires_in } = response;
|
||||||
setDeepLinkToken(token);
|
setDeepLinkToken(token);
|
||||||
@@ -249,7 +265,8 @@ export default function TelegramLoginButton({ referralCode }: TelegramLoginButto
|
|||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
try {
|
try {
|
||||||
await loginWithDeepLink(token);
|
// Deep link auth is for existing bot users — only campaign_slug applies
|
||||||
|
await loginWithDeepLink(token, capturedCampaign);
|
||||||
// Success — auth store is updated, navigate
|
// Success — auth store is updated, navigate
|
||||||
if (expireTimeoutRef.current) {
|
if (expireTimeoutRef.current) {
|
||||||
clearTimeout(expireTimeoutRef.current);
|
clearTimeout(expireTimeoutRef.current);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import LanguageSwitcher from '../components/LanguageSwitcher';
|
|||||||
import TelegramLoginButton from '../components/TelegramLoginButton';
|
import TelegramLoginButton from '../components/TelegramLoginButton';
|
||||||
import OAuthProviderIcon from '../components/OAuthProviderIcon';
|
import OAuthProviderIcon from '../components/OAuthProviderIcon';
|
||||||
import { saveOAuthState } from '../utils/oauth';
|
import { saveOAuthState } from '../utils/oauth';
|
||||||
import { consumeReferralCode, getPendingReferralCode } from '../utils/referral';
|
import { getPendingReferralCode } from '../utils/referral';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -146,16 +146,6 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '';
|
|
||||||
|
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN';
|
const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN';
|
||||||
const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V';
|
const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V';
|
||||||
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null;
|
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ interface AuthState {
|
|||||||
state: string,
|
state: string,
|
||||||
deviceId?: string | null,
|
deviceId?: string | null,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
loginWithDeepLink: (token: string) => Promise<void>;
|
loginWithDeepLink: (token: string, campaignSlug?: string | null) => Promise<void>;
|
||||||
registerWithEmail: (
|
registerWithEmail: (
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
@@ -322,8 +322,8 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
await get().checkAdminStatus();
|
await get().checkAdminStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
loginWithDeepLink: async (token) => {
|
loginWithDeepLink: async (token, campaignSlug) => {
|
||||||
const response = await authApi.pollDeepLinkToken(token);
|
const response = await authApi.pollDeepLinkToken(token, campaignSlug);
|
||||||
if (!response.access_token || !response.refresh_token) {
|
if (!response.access_token || !response.refresh_token) {
|
||||||
throw new Error('Invalid auth response: missing tokens');
|
throw new Error('Invalid auth response: missing tokens');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user