Add WeChat login binding gate and context-aware auth UX.

Stop auto-creating duplicate accounts on OAuth, add bind-or-register gate,
PC scan login, mobile open-in-WeChat guide, and fix localhost session cookies.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-15 15:47:19 -07:00
parent 2e14873f2d
commit 3cd322ccfe
19 changed files with 2141 additions and 156 deletions
+67
View File
@@ -3,6 +3,58 @@ export function isWechatBrowser(userAgent = navigator.userAgent) {
return /MicroMessenger/i.test(ua) || /WindowsWechat/i.test(ua);
}
export function isMobileBrowser(userAgent = navigator.userAgent) {
return /Android|iPhone|iPad|iPod|Mobile/i.test(userAgent || '');
}
export function isIosBrowser(userAgent = navigator.userAgent) {
return /iPhone|iPad|iPod/i.test(userAgent || '');
}
/** 手机外部浏览器(Safari / Chrome 等),不在微信内 */
export function isMobileExternalBrowser(options?: {
userAgent?: string;
search?: string;
serverInWechat?: boolean;
}) {
return isMobileBrowser(options?.userAgent) && !isWechatContext(options);
}
/** 构建「在微信中打开」后应访问的链接(带上 from=wechat 以展示一键登录) */
export function buildPageUrlForWechatOpen(
search = window.location.search,
pathname = window.location.pathname,
) {
const params = new URLSearchParams(search);
params.set('from', 'wechat');
if (!params.get('utm_source')) params.set('utm_source', 'wechat');
const query = params.toString();
return `${window.location.origin}${pathname}${query ? `?${query}` : ''}${window.location.hash}`;
}
export async function copyWechatOpenLink(): Promise<boolean> {
const url = buildPageUrlForWechatOpen();
try {
await navigator.clipboard.writeText(url);
return true;
} catch {
try {
const input = document.createElement('textarea');
input.value = url;
input.setAttribute('readonly', 'true');
input.style.position = 'fixed';
input.style.left = '-9999px';
document.body.appendChild(input);
input.select();
const ok = document.execCommand('copy');
document.body.removeChild(input);
return ok;
} catch {
return false;
}
}
}
/** 从服务号链接进入时带的参数(即使 UA 识别失败也展示微信登录) */
export function isWechatEntryUrl(search = window.location.search) {
const params = new URLSearchParams(search);
@@ -26,6 +78,8 @@ export function buildWechatAuthorizeUrl(options?: {
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
intent?: 'login' | 'register' | 'bind';
authMode?: 'mp' | 'open';
}) {
const params = new URLSearchParams();
const returnTo =
@@ -37,10 +91,23 @@ export function buildWechatAuthorizeUrl(options?: {
if (options?.utmSource) params.set('utm_source', options.utmSource);
if (options?.utmMedium) params.set('utm_medium', options.utmMedium);
if (options?.utmCampaign) params.set('utm_campaign', options.utmCampaign);
if (options?.intent) params.set('intent', options.intent);
if (options?.authMode) params.set('auth_mode', options.authMode);
const query = params.toString();
return query ? `/auth/wechat/authorize?${query}` : '/auth/wechat/authorize';
}
export function readWechatPendingToken(): string | null {
const params = new URLSearchParams(window.location.search);
const pending = params.get('wechat_pending');
if (!pending) return null;
params.delete('wechat_pending');
const nextSearch = params.toString();
const nextUrl = `${window.location.pathname}${nextSearch ? `?${nextSearch}` : ''}${window.location.hash}`;
window.history.replaceState({}, '', nextUrl);
return pending;
}
export function readWechatAuthError(): string | null {
const params = new URLSearchParams(window.location.search);
const error = params.get('wechat_error');