/** * `#/invite` โ an invitation link, opened (docs/MESHBAY_DESIGN.md ยง3.4). * * By the time this renders, `invite-link.js` has taken the invitation out of * the address and kept it in this tab. Signed out, the page says what is * waiting and offers to register or sign in; both come back here. Signed in, it * asks the hub what the ticket is for and shows it โ **joining is a click, never * automatic**, or a link would put anyone in any group without asking. * * Only the ticket goes to the hub from here. The code stays in the tab until * the group page hands it to the one node the link names. */ import { html, useCallback, useEffect, useState } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import { hubFetch, navigate } from './hub-client.js'; import { clearPending, linkOrigin, loadPending, parseInvite, savePending, } from './invite-link.js'; export function InvitePage({ user, onJoined }) { const [inv] = useState(loadPending); const [phase, setPhase] = useState(inv ? 'loading' : 'none'); const [preview, setPreview] = useState(null); const [error, setError] = useState(''); const refused = useCallback((err) => { if (err.message === 'invite_other_account') { // Kept: signing out and in again as the right account is the fix. setPhase('other'); } else if (err.message === 'invite_not_valid') { clearPending(); setPhase('invalid'); } else { setError(err.message); setPhase('error'); } }, []); useEffect(() => { if (!inv || !user) return undefined; let cancelled = false; hubFetch('/v1/invite-links/preview', { method: 'POST', token: user.token, body: { ticket: inv.t }, }) .then((p) => { if (cancelled) return; // The group the hub names must be the group the link names. if (p.group_id !== inv.g) { clearPending(); setPhase('invalid'); return; } setPreview(p); setPhase('confirm'); }) .catch((err) => { if (!cancelled) refused(err); }); return () => { cancelled = true; }; }, [inv, user, refused]); const join = useCallback(async () => { setPhase('joining'); try { const r = await hubFetch('/v1/invite-links/redeem', { method: 'POST', token: user.token, body: { ticket: inv.t }, }); if (r.group_id !== inv.g) { clearPending(); setPhase('invalid'); return; } // The code is still pending: the group page takes it to the node. if (onJoined) await onJoined(inv.g); navigate(`/group/${inv.g}`); } catch (err) { refused(err); } }, [inv, user, onJoined, refused]); const ignore = useCallback(() => { clearPending(); navigate('/'); }, []); let body; if (phase === 'none') { body = html`
${t('invite.none')}
`; } else if (!user) { body = html`${t('invite.signed_out')}
`; } else if (phase === 'loading' || phase === 'joining') { body = html`${phase === 'joining' ? t('invite.joining') : t('explore.loading')}
`; } else if (phase === 'confirm' && preview) { body = preview.already_member ? html`${t('invite.already_member', { group: preview.group_name })}
${t('invite.confirm', { inviter: preview.inviter, group: preview.group_name })}
${t('invite.other_account')}
${t('invite.invalid')}
`; } else { body = html`${error}
`; } return html`