Skip to content

fix(invites): consume invite tokens on signup - #290

Merged
chiptus merged 6 commits into
mainfrom
claude/long-running-task-mman6l
Aug 18, 2026
Merged

fix(invites): consume invite tokens on signup#290
chiptus merged 6 commits into
mainfrom
claude/long-running-task-mman6l

Conversation

@chiptus

@chiptus chiptus commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Wires the never-called use_invite_token RPC into a useInviteAcceptance effect keyed on user + invite token, so invite links actually add users to the group (magic-link, OTP, and already-signed-in paths). Also fixes InviteLandingPage sending invite_id instead of the real token into the auth flow, and removes the deadlock-prone RPC call from AuthContext's onAuthStateChange.

Closes #268
Closes #165

Verification

  • Generate a group invite link, open it logged out, sign up via the 6-digit OTP code; you land in the app with a "Welcome to {group}" toast, the group appears in your groups, and ?invite= is cleared from the URL.
  • Same flow but click the magic link in the email instead of entering the code; joining works the same way.
  • As an existing logged-out user, open an invite link and log in; you're added to the group.
  • Re-open an invite link for a group you're already in; a friendly "Already a member" notice shows, no error and no duplicate membership.
  • Open an expired/deactivated/unknown invite link; the existing "Invalid Invite" states render unchanged.

Generated by Claude Code

Copilot AI lite review requested due to automatic review settings August 6, 2026 15:13
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
upline Ready Ready Preview Aug 18, 2026 8:14pm

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 on user + inviteToken + validation) to call the accept-invite mutation and clear ?invite= from the URL on success.
  • Fixes InviteLandingPage to pass the real invite token into AuthDialog (instead of invite_id) so the auth flow preserves the correct token.
  • Removes the RPC call from AuthContext’s onAuthStateChange callback 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

  • alreadyMember detection relies on a hard-coded message string ("User already in group"), which is fragile. The RPC also returns group_id = NULL for several failure cases, so it’s safer to treat the "already member" case as the only failure where group_id is present.
  if (result.message === "User already in group") {
    return { ...result, alreadyMember: true };
  }

  throw new Error(result.message);

Comment thread src/api/invite-validation/useAcceptInviteMutation.ts
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Playwright test results

passed  50 passed

Details

stats  50 tests across 14 suites
duration  1 minute, 5 seconds
commit  9ef27b6

@chiptus chiptus left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all the different invite hooks?

Comment thread src/components/invite/useInviteAcceptance.test.tsx Outdated
Comment thread src/contexts/AuthContext.tsx Outdated

chiptus commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Why do we need all the different invite hooks?

Agreed it was one hook too many — consolidated in the latest push. There are now two layers:

  • src/api/invite-validation/useInviteValidationQuery and useAcceptInviteMutation, one file per endpoint as ADR-0001 prescribes (the mutation also owns the groups-query invalidation).
  • src/components/invite/useInviteFlow.ts — the single component-level hook. It validates the token, shows the invalid/expired toasts, and auto-consumes the invite once a session exists (which covers OTP, magic-link landing, and already-logged-in users in one code path). The former useInviteValidation + useInviteAcceptance pair is gone.

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

Comment thread src/components/invite/useInviteFlow.ts Outdated
Comment thread src/components/invite/useInviteFlow.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • attemptedTokenRef only keys on the invite token. If the signed-in user changes while the same inviteToken remains 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;

claude added 6 commits August 18, 2026 22:13
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 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
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
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:35

  • attemptedTokenRef is keyed only by inviteToken, so if a user signs out and a different user signs in (without a full page reload) while the same ?invite= token remains in the URL, invite acceptance will be skipped for the new user because the token was already “attempted”. Key the guard by both token and user id to ensure the effect runs once per (token,user).
  useEffect(() => {
    const groupName = inviteQuery.data?.group_name;
    if (!user || !inviteToken || inviteQuery.data?.is_valid !== true) return;
    if (attemptedTokenRef.current === inviteToken) return;
    attemptedTokenRef.current = inviteToken;

@chiptus
chiptus merged commit 54c13d4 into main Aug 18, 2026
11 checks passed
@chiptus
chiptus deleted the claude/long-running-task-mman6l branch August 18, 2026 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants