From f556782c86e8bb97f7e23c86f7d1fc7d44dab79f Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sun, 8 Mar 2026 17:15:37 +0800 Subject: [PATCH] feat: add invite code handling from URL for registration flow --- webapp/src/App.tsx | 62 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 3d46505..18208ee 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -111,6 +111,22 @@ function asText(value: unknown): string { return String(value); } +function readInviteCodeFromUrl(): string { + if (typeof window === 'undefined') return ''; + + const searchInvite = new URLSearchParams(window.location.search || '').get('invite'); + if (searchInvite && searchInvite.trim()) return searchInvite.trim(); + + const rawHash = String(window.location.hash || ''); + const queryIndex = rawHash.indexOf('?'); + if (queryIndex >= 0) { + const hashInvite = new URLSearchParams(rawHash.slice(queryIndex + 1)).get('invite'); + if (hashInvite && hashInvite.trim()) return hashInvite.trim(); + } + + return ''; +} + function summarizeImportResult( ciphers: Array>, folderCount: number, @@ -302,6 +318,7 @@ export default function App() { password2: '', inviteCode: '', }); + const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(''); const [unlockPassword, setUnlockPassword] = useState(''); const [pendingTotp, setPendingTotp] = useState(null); const [totpCode, setTotpCode] = useState(''); @@ -326,6 +343,35 @@ export default function App() { const [decryptedSends, setDecryptedSends] = useState([]); const migratedPlainFolderIdsRef = useRef>(new Set()); + useEffect(() => { + const syncInviteFromUrl = () => { + setInviteCodeFromUrl(readInviteCodeFromUrl()); + }; + syncInviteFromUrl(); + window.addEventListener('hashchange', syncInviteFromUrl); + window.addEventListener('popstate', syncInviteFromUrl); + return () => { + window.removeEventListener('hashchange', syncInviteFromUrl); + window.removeEventListener('popstate', syncInviteFromUrl); + }; + }, []); + + useEffect(() => { + if (!inviteCodeFromUrl) return; + setRegisterValues((prev) => (prev.inviteCode === inviteCodeFromUrl ? prev : { ...prev, inviteCode: inviteCodeFromUrl })); + }, [inviteCodeFromUrl]); + + useEffect(() => { + if (!inviteCodeFromUrl) return; + if (phase === 'loading' || phase === 'locked' || phase === 'app') return; + setPhase('register'); + if (location !== '/register') navigate('/register'); + if (typeof window !== 'undefined' && typeof window.history?.replaceState === 'function') { + window.history.replaceState(null, '', '/register'); + } + setInviteCodeFromUrl(''); + }, [inviteCodeFromUrl, phase, location, navigate]); + useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; const media = window.matchMedia('(max-width: 900px)'); @@ -539,6 +585,7 @@ export default function App() { } setLoginValues({ email: registerValues.email.toLowerCase(), password: '' }); setPhase('login'); + navigate('/login'); pushToast('success', t('txt_registration_succeeded_please_sign_in')); } @@ -577,7 +624,7 @@ export default function App() { setProfile(null); setPendingTotp(null); setPhase(setupRegistered ? 'login' : 'register'); - navigate('/login'); + navigate(setupRegistered ? '/login' : '/register'); } function handleLogout() { @@ -1695,8 +1742,17 @@ export default function App() { onSubmitLogin={() => void handleLogin()} onSubmitRegister={() => void handleRegister()} onSubmitUnlock={() => void handleUnlock()} - onGotoLogin={() => setPhase('login')} - onGotoRegister={() => setPhase('register')} + onGotoLogin={() => { + setPhase('login'); + navigate('/login'); + }} + onGotoRegister={() => { + if (inviteCodeFromUrl) { + setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl })); + } + setPhase('register'); + navigate('/register'); + }} onLogout={logoutNow} /> setToasts((prev) => prev.filter((x) => x.id !== id))} />