rwadurian/backend/mpc-system/services/service-party-app/src/pages/Join.tsx

256 lines
7.9 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 { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import styles from './Join.module.css';
interface SessionInfo {
sessionId: string;
walletName: string;
threshold: { t: number; n: number };
initiator: string;
createdAt: string;
currentParticipants: number;
totalParticipants?: number;
}
interface ValidateResult {
success: boolean;
error?: string;
sessionInfo?: SessionInfo;
joinToken?: string;
}
export default function Join() {
const { inviteCode } = useParams<{ inviteCode?: string }>();
const navigate = useNavigate();
const [code, setCode] = useState(inviteCode || '');
const [participantName, setParticipantName] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
const [joinToken, setJoinToken] = useState<string | null>(null);
const [partyId, setPartyId] = useState<string | null>(null);
const [step, setStep] = useState<'input' | 'confirm' | 'joining'>('input');
useEffect(() => {
if (inviteCode) {
handleValidateCode(inviteCode);
}
}, [inviteCode]);
const handleValidateCode = async (codeToValidate: string) => {
if (!codeToValidate.trim()) {
setError('请输入邀请码');
return;
}
setIsLoading(true);
setError(null);
try {
// 获取当前 partyId
const partyResult = await window.electronAPI.grpc.getPartyId();
if (!partyResult.success || !partyResult.partyId) {
setError('请先连接到消息路由器');
setIsLoading(false);
return;
}
setPartyId(partyResult.partyId);
// 解析邀请码获取会话信息
const result: ValidateResult = await window.electronAPI.grpc.validateInviteCode(codeToValidate);
if (result.success && result.sessionInfo) {
setSessionInfo(result.sessionInfo);
if (result.joinToken) {
setJoinToken(result.joinToken);
}
setStep('confirm');
} else {
setError(result.error || '无效的邀请码');
}
} catch (err) {
setError('验证邀请码失败,请检查网络连接');
} finally {
setIsLoading(false);
}
};
const handleJoinSession = async () => {
if (!sessionInfo || !participantName.trim()) {
setError('请输入参与者名称');
return;
}
if (!partyId) {
setError('未获取到 Party ID请重试');
return;
}
if (!joinToken) {
setError('未获取到加入令牌,请重新验证邀请码');
return;
}
setStep('joining');
setIsLoading(true);
setError(null);
try {
const result = await window.electronAPI.grpc.joinSession({
sessionId: sessionInfo.sessionId,
partyId: partyId,
joinToken: joinToken,
walletName: sessionInfo.walletName,
});
if (result.success) {
navigate(`/session/${sessionInfo.sessionId}`);
} else {
setError(result.error || '加入会话失败');
setStep('confirm');
}
} catch (err) {
setError('加入会话失败,请重试');
setStep('confirm');
} finally {
setIsLoading(false);
}
};
const handlePaste = async () => {
try {
const text = await navigator.clipboard.readText();
setCode(text.trim());
} catch (err) {
console.error('Failed to read clipboard:', err);
}
};
return (
<div className={styles.container}>
<div className={styles.card}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}></p>
{step === 'input' && (
<div className={styles.form}>
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<div className={styles.inputWrapper}>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="粘贴邀请码或邀请链接"
className={styles.input}
disabled={isLoading}
/>
<button
className={styles.pasteButton}
onClick={handlePaste}
disabled={isLoading}
>
</button>
</div>
</div>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => navigate('/')}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={() => handleValidateCode(code)}
disabled={isLoading || !code.trim()}
>
{isLoading ? '验证中...' : '下一步'}
</button>
</div>
</div>
)}
{step === 'confirm' && sessionInfo && (
<div className={styles.form}>
<div className={styles.sessionInfo}>
<h3 className={styles.sessionTitle}></h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{sessionInfo.walletName}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.threshold.t}-of-{sessionInfo.threshold.n}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>{sessionInfo.initiator}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}></span>
<span className={styles.infoValue}>
{sessionInfo.currentParticipants} / {sessionInfo.threshold.n}
</span>
</div>
</div>
</div>
<div className={styles.inputGroup}>
<label className={styles.label}></label>
<input
type="text"
value={participantName}
onChange={(e) => setParticipantName(e.target.value)}
placeholder="输入您的名称(其他参与者可见)"
className={styles.input}
disabled={isLoading}
/>
</div>
{error && <div className={styles.error}>{error}</div>}
<div className={styles.actions}>
<button
className={styles.secondaryButton}
onClick={() => {
setStep('input');
setSessionInfo(null);
setJoinToken(null);
setPartyId(null);
setError(null);
}}
disabled={isLoading}
>
</button>
<button
className={styles.primaryButton}
onClick={handleJoinSession}
disabled={isLoading || !participantName.trim()}
>
{isLoading ? '加入中...' : '确认加入'}
</button>
</div>
</div>
)}
{step === 'joining' && (
<div className={styles.joining}>
<div className={styles.spinner}></div>
<p>...</p>
</div>
)}
</div>
</div>
);
}