// AcceptInvitation.jsx — v0.16.0 / roadmap item #12. // // The /invitations/accept?token=... landing page the invitation email // links to. The page: // // 1. Reads `?token=...` from the URL. // 2. Calls GET /api/invitations/accept?token=... to preview what the // invitation grants (RFC title, role-in-RFC, expiry, whether the // currently-signed-in user's email matches the invitee's). // 3. Renders a confirmation surface — name the RFC, name the role, // and either show "Accept" (when the email matches and the // invitation is still pending) or a refusal message (expired, // revoked, email mismatch). // 4. On accept, POST /api/invitations/accept lands the // rfc_collaborators row and the page redirects to the RFC's view. // // For an anonymous viewer who lands here without signing in, the // preview call 401s and the page tells them to sign in. After // signing in (via the existing OTC/passcode surface at /login) they // can return to the same URL — the token is stable. import { useEffect, useState } from 'react' import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { acceptInvitation, previewInvitation } from '../api' import { EVENTS, identify, track } from '../lib/analytics' import { entryPath, useProjectId } from '../lib/entryPaths' export default function AcceptInvitation({ viewer }) { const [searchParams] = useSearchParams() const navigate = useNavigate() const pid = useProjectId() const token = searchParams.get('token') || '' const [preview, setPreview] = useState(null) const [previewError, setPreviewError] = useState(null) const [accepting, setAccepting] = useState(false) const [acceptError, setAcceptError] = useState(null) useEffect(() => { if (!token) { setPreviewError('No invitation token in the URL.') return } if (!viewer) { // Not signed in — the preview endpoint will 401. We surface a // sign-in prompt without making the request. return } previewInvitation(token) .then(setPreview) .catch(err => setPreviewError(err.message || 'Could not load invitation.')) }, [token, viewer]) async function handleAccept() { setAccepting(true) setAcceptError(null) try { const result = await acceptInvitation(token) // v0.16.0 + #21 Part C — re-identify with per-RFC invite // properties on accept, BEFORE the track event fires, so the // Amplitude user record carries the invite context from the // moment of acceptance. setOnce on invited_at preserves the // first-accepted timestamp if the same user accepts multiple // RFC invitations. if (viewer?.id != null) { identify({ user_id: String(viewer.id), properties: { invited_at: ['__setOnce__', new Date().toISOString()], last_invited_to_rfc: result.rfc_slug, last_invite_role_in_rfc: result.role_in_rfc || preview?.role_in_rfc, claim_method: 'rfc-invite', }, }) } track(EVENTS.INVITATION_ACCEPTED, { rfc_slug: result.rfc_slug, role_in_rfc: result.role_in_rfc || preview?.role_in_rfc, }) navigate(entryPath(pid, result.rfc_slug)) } catch (err) { setAcceptError(err.message || 'Could not accept invitation.') } finally { setAccepting(false) } } if (!token) { return (

Invitation link is malformed

No token parameter was found. Ask the person who invited you to re-send the link.

Return to the catalog

) } if (!viewer) { return (

Sign in to accept your invitation

You've been invited to collaborate on an RFC. Sign in first so we can attach the membership to your account, then return to this link.

Sign in

) } if (previewError) { return (

Invitation unavailable

{previewError}

Return to the catalog

) } if (!preview) { return
Loading invitation…
} const { rfc_title, rfc_slug, role_in_rfc, status, invitee_email, email_matches_you } = preview if (status === 'revoked') { return (

Invitation revoked

The owner of {rfc_title} revoked this invitation. Ask them to re-issue it if you should still have access.

Read the RFC anyway

) } if (status === 'expired') { return (

Invitation expired

This invitation to {rfc_title} has expired. Ask the RFC's owner to issue a fresh one.

Read the RFC anyway

) } if (status === 'accepted') { return (

Already accepted

You've already accepted this invitation. You can{' '} open {rfc_title} now.

) } if (!email_matches_you) { return (

This invitation is for a different account

This invitation was sent to {invitee_email}. You're currently signed in as {viewer.email || viewer.gitea_login}. Sign out and sign back in with the invited address to accept.

Sign out

) } return (

Join {rfc_title}

You've been invited to {rfc_title} as a{' '} {role_in_rfc}.

{role_in_rfc === 'contributor' ? 'Contributors can open PRs against this RFC and join its discussion.' : 'Discussants can post in this RFC\'s discussion.'}

{acceptError &&
{acceptError}
}

or just read the RFC without accepting

) }