Skip to content

Unified invite landing for collection and document invites - #638

Open
icarusz wants to merge 14 commits into
mainfrom
onboarding/invite-landing
Open

Unified invite landing for collection and document invites#638
icarusz wants to merge 14 commits into
mainfrom
onboarding/invite-landing

Conversation

@icarusz

@icarusz icarusz commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

Redesign of QuartoHub's two invite entry paths (bd-fxdcxbpq), per the design handoff in design_handoff_invite_landing/ (spec + mocks committed on this branch):

  • One InviteLanding card for both invite kinds: kicker, inviter line, title, display-only payload preview, what-is-QuartoHub explainer, single CTA. Replaces JoinCollectionLanding (the name/cursor-color form is gone; identity comes from the Google account with the anonymous fallback).
  • Share links no longer cold-open the editor. The URL is still scrubbed on mount, but connection happens on the CTA click. Signed-out invitees see the landing (not the login wall) and round-trip back to it through Google via the pre-auth hash.
  • Collection joins land on the projects home with the joined collection promoted to the top — the invite is to the collection, not a document.
  • CTA verb follows sign-in friction: signed out shows "Join to collaborate on <file/collection>" above the GIS button; signed in is just "Open ".
  • One-time welcome banner in the editor (per-target dismissal persisted): document invites on that document's project, collection invites on the first project opened from the collection.
  • Sender side: buildInviteUrl / buildShareableUrl embed from= and a display-only preview= (base64url JSON, capped, never doc ids) built from the cached peek summaries. Legacy links parse unchanged.
  • Fixes found while dogfooding: peek summaries are now carried when adding a project to a collection (invites showed "0 files"), the dead "People & invite…" collection menu item works, and the home header uses the real Quarto logo.

Out of scope (deferred per the handoff): zero-setup cold start, seeded first-run samples, revocable/role-scoped links, sender UI redesign.

Verification

  • TDD throughout; new suites for the URL codec, routing params, InviteLanding matrix, welcome banner, and the ProjectsHome menu→popover path.
  • hub-client: 476 unit / 119 integration / 133 wasm green; Playwright e2e 71/72. Every red is pre-existing main breakage tracked in bd-qvjc6gdp (useAutomergeSync mock missing vfsAddFile, BranchBar).
  • Verified end-to-end in a real browser against a local hub (local-prod): share link → landing → CTA → editor on file → banner; collection invite → join → home with collection on top; invite URLs decoded and inspected.

Plan and running log: claude-notes/plans/2026-09-01-unified-invite-landing.md. Screenshots of the implemented screens follow in a comment.

🤖 Generated with Claude Code

icarusz and others added 13 commits September 1, 2026 13:50
Both invite entry paths (#/join-collection/... and #/share/...) now land on
one InviteLanding card: kicker, inviter line, title, display-only payload
preview, what-is-QuartoHub explainer, and a single CTA. Signed-out users see
the card (not the login wall) with a GIS "Continue with Google" CTA that
round-trips back to the invite via the pre-auth hash; signed-in users get
one-click join/open. Share routes no longer auto-connect on load: the URL is
still scrubbed on mount, but connection happens on the CTA click. After
joining, the user lands in the editor on the intended file with a one-time
dismissible welcome banner (per-target localStorage dismissal).

- routing.ts: ShareRoute gains from=/preview=; JoinCollectionRoute gains
  preview=/start= (base64url JSON codec in utils/invitePreview.ts, versioned,
  capped at 3 projects x 2 files, display-only). Legacy URLs parse unchanged;
  malformed payloads degrade to undefined. savePreAuthHash takes an explicit
  hash; clearPreAuthHash drops it once an invite is consumed.
- App.tsx: pendingShare captured at boot; connectToSharedProject hoisted with
  {quiet, addToSet} opts; collection CTA subscribes then opens the start=
  target (else home); invite-first silent root creation now covers share
  invitees; ephemeral preview boots keep eager connect.
- JoinCollectionLanding (name/color form) deleted; identity comes from the
  Google-account name upgrade with anonymous fallback.
- Sender side: buildInviteUrl embeds preview from cached peek summaries and
  start=first project's first file; buildShareableUrl gains {from, preview},
  wired at both call sites (ProjectsHome menu, Editor ShareDialog).
- AuthProvider SignInButtonProps gains text variant ('continue_with').
- Dev harness pages for all landing variants + welcome banner.

Tests: TDD throughout (routing/codec/component/banner suites written first,
verified red). 476 unit + 118 integration + 133 wasm green except the
pre-existing main breakage tracked in bd-qvjc6gdp (useAutomergeSync mock,
BranchBar). e2e share-link specs updated to click through the landing; suite
71/72 (BranchBar again). Verified end-to-end in a real browser against a
local hub: share link -> landing -> CTA -> editor on file -> banner, and
collection invite -> join -> editor on start target -> banner.

Design handoff bundle (spec + authoritative mocks) committed at
design_handoff_invite_landing/; plan at
claude-notes/plans/2026-09-01-unified-invite-landing.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xdcxbpq)

Andrew's manual testing surfaced invite previews showing "0 files" for
populated projects. Cause: two layers dropped the cached summary when an
entry was copied into a collection. ProjectsHome's entryFor projected only
{indexDocId, syncServer, description}, and addProjectToSet discarded
entry.summary on write. The collection copy therefore had no file counts
until the project's next open (updateProjectSummaryEverywhere then healed
every copy — which is why reopening "fixed" it).

Both layers now carry the summary: entryFor includes it when the source
entry has one, and addProjectToSet persists it on create and adopts it on
re-add only when the existing entry has none (freshness stays owned by
updateProjectSummaryInSet, so a stale re-add can never clobber a live
summary).

TDD: three new schema tests written first and verified red; 75/75 green
after. Verified live against a local hub: adding a project to a brand-new
collection immediately shows "2 files" on the copy, and the collection's
invite preview payload decodes with the real fileCount without reopening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-fxdcxbpq)

Direction change from Andrew's design review: the invite is to a collection,
so joining should land on the QuartoHub home screen with that collection's
section promoted to the top — not cold-open the first document's editor.

- handleCollectionCta: subscribe, then navigate home; the start-target
  editor open is gone. ProjectsHome gains promoteCollectionId (session-
  scoped, set on join) and sorts that section first.
- InviteLanding: startName prop and "Join and open X" CTA removed; the
  collection CTA reads "Join <name>" (or "Join collection" on legacy links).
- buildInviteUrl no longer emits start= (the parser still tolerates it on
  links already in the wild — verified live against one).
- Welcome banner: now scoped to the invite target — a document invite's
  banner shows only on that project; a collection invite's banner shows on
  the first project opened from that collection, reworded to "Carlos
  invited you." (the "suggested starting here" claim died with start=).

TDD: banner-copy test updated first and verified red; 235 affected tests
green after. Verified live against a local hub: join CTA lands on home with
the joined collection sorted above an older one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Andrew's rule from design review: if the user is about to go through the
Google sign-in flow for the first time, frame the invite as joining; if
they are already signed in, one click just opens the thing.

- Signed out: a "Join to collaborate on <target>" lead-in renders above
  the Google button (whose label GIS locks to "Continue with Google").
  The target is the file name for document invites (from the preview,
  falling back to the title) and the collection name otherwise.
- Signed in: the CTA is "Open <name>" for both kinds ("Open collection"/
  "Open document" on legacy links); the busy label is "Opening…". The
  collection join still happens on click — only the verb changed.

TDD: CTA-matrix tests updated first; 18/18 green, CSS lint at baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header's hand-drawn stacked-bars mark is replaced with the app's
actual quarto-icon.svg (the segmented circle), tinted for dark mode via
--logo-filter like the other uses of the asset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Andrew's manual testing: clicking "People & invite…" in a collection's
⋯ menu did nothing. The activation click bubbled from the MenuItem to
the Menu root's closer, whose onClose (closeAllMenus) also resets
membersFor — cancelling the popover in the same React batch that
requested it. Marked the item keepOpen (its onSelect already closes the
menu explicitly), matching the component's documented escape hatch.

TDD: new ProjectsHome.integration.test.tsx drives the real menu path
(actions button -> menu item -> popover role=dialog), verified red then
green; full integration suite 119/119.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captured from the dev harness pages so reviewers can assess the revised
invite screens next to the design mocks without running the branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@icarusz
icarusz requested review from cscheid and vezwork September 1, 2026 19:35
@posit-snyk-bot

posit-snyk-bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@icarusz

icarusz commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Implemented screens

The four invite-card states, for review without checking out the branch. The design mocks these implement are in design_handoff_invite_landing/screenshots/ (section 3a is authoritative).

Collection invite — signed out ("join to collaborate" lead-in above the Google button)

collection invite, signed out

Collection invite — signed in (one click; lands on the projects home with this collection promoted to the top)

collection invite, signed in

Document invite — signed out

document invite, signed out

Document invite — signed in (opens the editor on the shared file)

document invite, signed in

Two further captures (the editor welcome banner and the post-join home with the promoted collection) were removed rather than posted misleading: the banner shot was a thin bar above a blank page and did not show its inline rename, and the projects-home harness fixture has no collections, so it could not show the promotion this PR adds. Purpose-built harness pages for both are coming; until then those two behaviors are best seen by running the branch, or see the plan doc.

🤖 Generated with Claude Code

@icarusz

icarusz commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

I think I will spend some time (maybe early next week) harmonizing the different styles a bit. The new editor updates give it a different feel.

The welcome-banner capture was a 40px bar above 900px of blank page (the
harness renders the banner alone, and it was captured fullPage), and it
did not show the inline rename its caption advertised.

The projects-home capture could not show the behavior the PR describes:
the harness fixture FAKE_COLLECTIONS holds only the root set, so the page
renders 'Everything else' with no collection sections — nothing of the
promoted-collection landing. Replacements need harness pages built for
the purpose; removing them beats leaving reviewers with artifacts that
imply the wrong thing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants