Merge commit 'fcf1d45522e3ba6e6faf2590b60f2db1ef90e73c' into dev_jacy
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import TabLayout from './layouts/TabLayout';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import HomePage from './pages/HomePage';
|
||||
@@ -19,10 +20,20 @@ import RedeemSuccessPage from './pages/RedeemSuccessPage';
|
||||
import PayPage from './pages/PayPage';
|
||||
import CustomerServicePage from './pages/CustomerServicePage';
|
||||
import { UserSessionProvider } from './contexts/UserSessionContext';
|
||||
import { capturePromoFromUrl, touchPromoIfNeeded } from './lib/promo';
|
||||
|
||||
function PromoBootstrap() {
|
||||
useEffect(() => {
|
||||
capturePromoFromUrl();
|
||||
void touchPromoIfNeeded();
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<UserSessionProvider>
|
||||
<PromoBootstrap />
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route element={<TabLayout />}>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type SessionPayload,
|
||||
type UserProfile,
|
||||
} from '../lib/api';
|
||||
import { touchPromoIfNeeded } from '../lib/promo';
|
||||
|
||||
type UserSessionContextValue = {
|
||||
ready: boolean;
|
||||
@@ -62,6 +63,7 @@ export function UserSessionProvider({ children }: { children: ReactNode }) {
|
||||
if (!session.user) {
|
||||
await refreshProfile();
|
||||
}
|
||||
await touchPromoIfNeeded();
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { apiBase } from './api';
|
||||
|
||||
export const PROMO_STORAGE_KEY = 'dukang_promo_code';
|
||||
|
||||
function readPromoFromSearch(search: string): string | null {
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
||||
const code = params.get('promo')?.trim();
|
||||
return code ? code.toUpperCase() : null;
|
||||
}
|
||||
|
||||
/** 解析 URL 中的 ?promo= 并写入 sessionStorage */
|
||||
export function capturePromoFromUrl(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
let code = readPromoFromSearch(window.location.search);
|
||||
if (!code && window.location.hash.includes('?')) {
|
||||
const hashQuery = window.location.hash.slice(window.location.hash.indexOf('?'));
|
||||
code = readPromoFromSearch(hashQuery);
|
||||
}
|
||||
if (code) {
|
||||
sessionStorage.setItem(PROMO_STORAGE_KEY, code);
|
||||
}
|
||||
return code ?? sessionStorage.getItem(PROMO_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function getStoredPromoCode(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return sessionStorage.getItem(PROMO_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/** 调用 /promo/touch 完成扫码归因(OptionalJwt:未登录也累加 scan_count) */
|
||||
export async function touchPromoIfNeeded(): Promise<void> {
|
||||
const promoCode = getStoredPromoCode();
|
||||
if (!promoCode) return;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': 'USER_H5',
|
||||
};
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${apiBase}/promo/touch`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ promoCode }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.code !== 0) return;
|
||||
} catch {
|
||||
/* 静默失败,不阻断用户流程 */
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@ import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
|
||||
import { useSmsCode } from '../lib/use-sms-code';
|
||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||
import { useUserSession } from '../contexts/UserSessionContext';
|
||||
import { touchPromoIfNeeded } from '../lib/promo';
|
||||
|
||||
async function finishLogin(navigate: (path: string) => void, returnTo: string) {
|
||||
await touchPromoIfNeeded();
|
||||
navigate(returnTo.startsWith('/') ? returnTo : '/');
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -50,7 +56,8 @@ export default function LoginPage() {
|
||||
phoneVerified: !!result.phoneVerified,
|
||||
user: result.user as SessionPayload['user'],
|
||||
});
|
||||
navigate(returnTo.startsWith('/') ? returnTo : '/');
|
||||
void finishLogin(navigate, returnTo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ export default function LoginPage() {
|
||||
body: JSON.stringify({ phone, code }),
|
||||
});
|
||||
applySession(data);
|
||||
navigate(returnTo.startsWith('/') ? returnTo : '/');
|
||||
await finishLogin(navigate, returnTo);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '登录失败');
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user