fix(invites): consume invite tokens on signup - #290
Conversation
Wires the never-called use_invite_token RPC into a central useInviteAcceptance hook keyed on user + invite token, covering magic-link, OTP, and already-signed-in paths. Fixes InviteLandingPage passing invite_id instead of the real token into the auth flow (broken magic-link redirect), removes the deadlock-prone RPC call from AuthContext's onAuthStateChange, treats invite reuse by an existing member as a clean no-op, and invalidates group queries after joining. Closes #268 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
Reset the attempted-token guard on failure so a transient error doesn't permanently block acceptance, drop the unused hook return value, and unify the group-name fallback copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Pull request overview
This PR fixes the invite flow so that a validated ?invite=<token> link is actually consumed after a session exists, ensuring users are inserted into group_members via the existing use_invite_token RPC (OTP, magic-link redirect, and already-signed-in cases).
Changes:
- Adds
useInviteAcceptance(effect keyed onuser + inviteToken + validation) to call the accept-invite mutation and clear?invite=from the URL on success. - Fixes
InviteLandingPageto pass the real invite token intoAuthDialog(instead ofinvite_id) so the auth flow preserves the correct token. - Removes the RPC call from
AuthContext’sonAuthStateChangecallback to avoid the deadlock-prone pattern described in the PR.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/routes/__root.tsx | Wires invite acceptance into the root route and passes inviteToken into InviteLandingPage. |
| src/contexts/AuthContext.tsx | Removes invite consumption side-effects from the auth callback; keeps auth dialog close on SIGNED_IN. |
| src/components/invite/useInviteValidation.ts | Limits this hook to validation + toast side effects (removes unused “accept” behavior). |
| src/components/invite/useInviteAcceptance.ts | New hook that consumes valid invites once a user exists, toasts outcome, and clears the URL param. |
| src/components/invite/useInviteAcceptance.test.tsx | Adds unit tests covering success, already-member, invalid, error, and single-attempt behavior. |
| src/components/invite/InviteLandingPage.tsx | Passes the actual token into AuthDialog and closes dialog on OTP success. |
| src/api/invite-validation/useInviteValidationQuery.ts | Removes the unused invite-accept mutation from this query module. |
| src/api/invite-validation/useAcceptInviteMutation.ts | Adds a dedicated mutation wrapper around use_invite_token and invalidates groups on join. |
Suppressed comments (1)
src/api/invite-validation/useAcceptInviteMutation.ts:42
alreadyMemberdetection relies on a hard-coded message string ("User already in group"), which is fragile. The RPC also returnsgroup_id = NULLfor several failure cases, so it’s safer to treat the "already member" case as the only failure wheregroup_idis present.
if (result.message === "User already in group") {
return { ...result, alreadyMember: true };
}
throw new Error(result.message);
The use_invite_token RPC returns a group_id only for the already-in-group failure, so keying on it survives server copy changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
Playwright test resultsDetails
|
chiptus
left a comment
There was a problem hiding this comment.
Why do we need all the different invite hooks?
Collapses useInviteValidation + useInviteAcceptance (both only used by the root route) into a single flow hook, drops the AuthContext comment, and reworks the test to drive the whole flow through the real query/mutation hooks with only the supabase client mocked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
Agreed it was one hook too many — consolidated in the latest push. There are now two layers:
The AuthContext comment is also removed, and the test now runs the whole flow through the real query/mutation hooks with only the supabase client mocked (details in the thread on the test file). Generated by Claude Code |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
A single effect driven by one getError helper also removes the edge case where a stale validation result plus a refetch error could toast twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AAZzVYMN7csUv8kehZEsp
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/components/invite/useInviteFlow.ts:36
attemptedTokenRefonly keys on the invite token. If the signed-in user changes while the sameinviteTokenremains in the URL (e.g., sign out/in in the same tab, or switch accounts after a successful acceptance but the token is reintroduced via navigation), the hook will incorrectly skip consuming the invite for the new user. Key the “attempted” guard by both user ID and token.
const groupName = inviteQuery.data?.group_name;
if (!user || !inviteToken || inviteQuery.data?.is_valid !== true) return;
if (attemptedTokenRef.current === inviteToken) return;
attemptedTokenRef.current = inviteToken;
Wires the never-called
use_invite_tokenRPC into auseInviteAcceptanceeffect keyed on user + invite token, so invite links actually add users to the group (magic-link, OTP, and already-signed-in paths). Also fixesInviteLandingPagesendinginvite_idinstead of the real token into the auth flow, and removes the deadlock-prone RPC call fromAuthContext'sonAuthStateChange.Closes #268
Closes #165
Verification
?invite=is cleared from the URL.Generated by Claude Code