front/src/utils/wechatOAuth.ts
zzh 37d534edc3 feat: H5 WeChat one-tap authorize login/register and UI polish
Replace QR login with OAuth button; auto-authorize register/bind; follow OA in settings; advisor text styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 15:56:46 +08:00

114 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 微信公众号 H5 网页授权(注册 / 绑定页无 code 时跳转) */
import {
clearStashedInviteRef,
readStashedInviteRef,
stashInviteRef,
} from '@/utils/inviteRefStorage';
export type WeChatH5Page = 'register' | 'bind' | 'login';
const DEFAULT_MP_APP_ID = 'wxd5141ba46c856e1f';
export function resolveWeChatMpAppId(): string {
const fromEnv = import.meta.env.VITE_WECHAT_MP_APPID as string | undefined;
return fromEnv?.trim() || DEFAULT_MP_APP_ID;
}
/** @deprecated 使用 stashInviteRef */
export function stashWeChatInviteRef(inviteCode?: string | null): void {
stashInviteRef(inviteCode);
}
/** @deprecated 使用 readStashedInviteRef */
export function readStashedWeChatInviteRef(): string {
return readStashedInviteRef();
}
/** @deprecated 使用 clearStashedInviteRef */
export function clearStashedWeChatInviteRef(): void {
clearStashedInviteRef();
}
/** 从 OAuth state 解析邀请码(格式 register:CODE) */
export function parseInviteRefFromOAuthState(state: unknown): string {
const raw = String(state ?? '').trim();
const match = raw.match(/^(?:register|bind):(.+)$/i);
if (!match?.[1]) return '';
try {
return decodeURIComponent(match[1]).trim();
} catch {
return match[1].trim();
}
}
/**
* 解析当前页邀请码:query.ref / query.invite → OAuth state → 7 天暂存
*/
export function resolveWeChatInviteRef(query: Record<string, unknown>): string {
const fromQuery = String(query.ref ?? query.invite ?? '').trim();
if (fromQuery) {
stashInviteRef(fromQuery);
return fromQuery;
}
const fromState = parseInviteRefFromOAuthState(query.state);
if (fromState) {
stashInviteRef(fromState);
return fromState;
}
return readStashedInviteRef();
}
function resolveOAuthRedirectPath(page: WeChatH5Page): string {
if (page === 'login') {
return `${window.location.origin}/h5/login`;
}
return `${window.location.origin}/wechat/${page}`;
}
/**
* 跳转微信 OAuth。
* - register/bind:邀请码写入 state + 7 天暂存
* - login:可选把登录后回跳路径写入 state(login:/h5/orders)
*/
export function redirectToWeChatOAuth(
page: WeChatH5Page,
inviteCode?: string | null,
options?: { afterLoginPath?: string | null },
): void {
const appId = resolveWeChatMpAppId();
const redirectUri = encodeURIComponent(resolveOAuthRedirectPath(page));
let state = page;
if (page === 'login') {
const after = String(options?.afterLoginPath ?? '').trim();
if (after.startsWith('/h5')) {
state = `login:${encodeURIComponent(after)}`;
}
} else {
const code = String(inviteCode ?? '').trim() || readStashedInviteRef();
if (code) stashInviteRef(code);
state = code ? `${page}:${encodeURIComponent(code)}` : page;
}
window.location.replace(
`https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}` +
`&redirect_uri=${redirectUri}` +
`&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`,
);
}
/** 从 OAuth state 解析登录后回跳路径 */
export function parseAfterLoginPathFromOAuthState(state: unknown): string {
const raw = String(state ?? '').trim();
const match = raw.match(/^login:(.+)$/i);
if (!match?.[1]) return '';
try {
const path = decodeURIComponent(match[1]).trim();
return path.startsWith('/h5') ? path : '';
} catch {
const path = match[1].trim();
return path.startsWith('/h5') ? path : '';
}
}