Files
memind/src/components/WechatBindGate.tsx
T
John 3cd322ccfe 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>
2026-06-15 15:47:19 -07:00

185 lines
5.8 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.
import { useEffect, useState } from 'react';
import {
completeWechatBind,
completeWechatRegister,
getMe,
getWechatPendingProfile,
} from '../api/client';
import type { CapabilityMap, PortalUser } from '../types';
import type { WechatPendingProfile } from '../api/client';
import { TKMindAvatar } from './TKMindAvatar';
type Step = 'choose' | 'bind';
export function WechatBindGate({
pendingToken,
returnTo,
onAuth,
}: {
pendingToken: string;
returnTo?: string | null;
onAuth: (
user?: PortalUser | null,
capabilities?: CapabilityMap,
grantedSkills?: string[],
) => void;
}) {
const [profile, setProfile] = useState<WechatPendingProfile | null>(null);
const [step, setStep] = useState<Step>('choose');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
void getWechatPendingProfile(pendingToken)
.then((data) => setProfile(data))
.catch((err) => {
setError(err instanceof Error ? err.message : '绑定会话无效');
})
.finally(() => setLoading(false));
}, [pendingToken]);
const finishAuth = async (user: PortalUser) => {
const me = await getMe().catch(() => null);
onAuth(user, me?.capabilities, me?.grantedSkills);
const target = returnTo || profile?.returnTo;
if (target && target !== '/') {
try {
const url = new URL(target, window.location.origin);
if (url.origin === window.location.origin) {
window.location.href = url.toString();
return;
}
} catch {
// ignore invalid return target
}
}
};
const handleNewUser = async () => {
setSubmitting(true);
setError(null);
try {
const result = await completeWechatRegister(pendingToken);
await finishAuth(result.user);
} catch (err) {
setError(err instanceof Error ? err.message : '注册失败');
} finally {
setSubmitting(false);
}
};
const handleBindExisting = async (e: React.FormEvent) => {
e.preventDefault();
setSubmitting(true);
setError(null);
try {
const result = await completeWechatBind(pendingToken, username, password);
await finishAuth(result.user);
} catch (err) {
setError(err instanceof Error ? err.message : '绑定失败');
} finally {
setSubmitting(false);
}
};
const displayName = profile?.nickname?.trim() || '微信用户';
return (
<div className="login-container">
<div className="login-card wechat-bind-card">
<div className="login-brand">
{profile?.avatarUrl ? (
<img src={profile.avatarUrl} alt="" className="wechat-bind-avatar" />
) : (
<TKMindAvatar size="md" className="login-brand-avatar" />
)}
<h1 className="login-title">你好,{displayName}</h1>
<p className="login-desc">
{step === 'choose'
? '请选择如何使用这个微信账号'
: '验证已有账号后,微信将与该账号绑定'}
</p>
</div>
{loading && <p className="login-desc">正在加载微信信息…</p>}
{!loading && step === 'choose' && (
<div className="wechat-bind-options">
<button
type="button"
className="wechat-bind-option wechat-bind-option-primary"
disabled={submitting}
onClick={() => void handleNewUser()}
>
<strong>我是新用户</strong>
<span>快速开始,稍后可设置密码</span>
</button>
<button
type="button"
className="wechat-bind-option"
disabled={submitting}
onClick={() => {
setStep('bind');
setError(null);
}}
>
<strong>我已有账号</strong>
<span>绑定微信,保留原有内容</span>
</button>
<p className="wechat-bind-hint">
绑定后,微信登录将进入此账号,你的 MindSpace 内容不会丢失。
</p>
</div>
)}
{!loading && step === 'bind' && (
<form className="wechat-bind-form" onSubmit={handleBindExisting}>
<div className="login-fields">
<input
className={`login-input${error ? ' login-input-error' : ''}`}
type="text"
placeholder="用户名"
value={username}
autoComplete="username"
autoFocus
onChange={(e) => setUsername(e.target.value)}
/>
<input
className={`login-input${error ? ' login-input-error' : ''}`}
type="password"
placeholder="密码"
value={password}
autoComplete="current-password"
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="login-error">{error}</p>}
<button
type="submit"
className="login-btn"
disabled={submitting || !username.trim() || !password.trim()}
>
{submitting ? '绑定中…' : '验证并绑定'}
</button>
<button
type="button"
className="login-link wechat-bind-back"
onClick={() => {
setStep('choose');
setError(null);
}}
>
返回选择
</button>
</form>
)}
{error && step === 'choose' && <p className="login-error">{error}</p>}
</div>
</div>
);
}