Skip to content

fix(console): route /accept-invitation to DefaultAcceptInvitationPage, retire the duplicate acceptInvitation.* namespace (#3811) - #4168

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3811-accept-invitation-converge
Aug 10, 2026
Merged

fix(console): route /accept-invitation to DefaultAcceptInvitationPage, retire the duplicate acceptInvitation.* namespace (#3811)#4168
yinlianghui merged 1 commit into
mainfrom
claude/issue-3811-accept-invitation-converge

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3811

Option A, per the maintainer ruling of 2026-08-09 and the delegated ruling carried in the PM claim: one route, one component, one namespace.

What changed

apps/console/src/App.tsx now routes /accept-invitation/:invitationId to app-shell's DefaultAcceptInvitationPage. The console-local thin page is deleted, and its acceptInvitation.* namespace (12 keys in each of the ten packs, 120 lines) goes with it. Nothing published was removedDefaultAcceptInvitationPage keeps its export and simply becomes the routed implementation.

The user-visible gain is the whole point of the ruling: the old page offered an Accept and a Decline button and nothing else. The new one fetches the invitation and shows which organization, which role, and when the link expires — then switches the user into that organization on accept.

Premise check (all claims held on origin/main @ f7c6430ec)

ClaimVerified
Two components for one URLyes — apps/console/src/pages/auth/AcceptInvitationPage.tsx and packages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsx
Console routes the thin oneyes — App.tsx:186-189
26 keys across two namespacesyes — acceptInvitation 12, organization.accept 14, in all ten packs
Nothing else reads acceptInvitation.*yes — repo-wide grep including template-literal t() forms found only the deleted page, the slice-three test, and CHANGELOG.md history

Binding condition 1 — the real-render verification

The ruling presumed the app-shell page renders acceptably inside console's layout. That presumption is now measured rather than assumed, in apps/console/src/pages/auth/__tests__/AcceptInvitationRoute.test.tsx.

First, what console's route actually is: bare. App.tsx wraps this route in no layout at all — AuthLayout exists and LoginPage / ResetPasswordPage / SetPasswordPage use it, but /accept-invitation never did, and neither page (old or new) references it. Both paint their own full-viewport shell. So the page is mounted in the test exactly as the route mounts it: inside a real BrowserRouter, on the real path, with no wrapper.

Second, it is the shipped export that is mounted — imported from @object-ui/app-shell, not from a source path — and rendered with the real@object-ui/componentsButton and real lucide icons, not passthrough stubs. A stubbed primitive can tell you the component called it; it can never tell you the result is a coherent screen. Asserted: the card keeps its chrome (rounded-xl border bg-card p-8 shadow-sm), it centres inside a min-h-svh shell, both buttons render as real enabled button elements carrying cva-generated Shadcn classes, and the title is a real h1.

Result: the render is fine. Six of the eight new cases passed against the unmodified app-shell page on the first run. The ruling's presumption holds — no styling work was needed.

Binding condition 5 — behavior pins, and one repair they turned up

Pinned at route level: anonymous visitors still bounce to /login?redirect=; an authenticated visitor sees organization, role and expiry; Accept calls acceptInvitation and then switchOrganization (order asserted, not just both-were-called) and lands on /home; Decline rejects and lands on /organizations; and rendering in zh proves the copy now arrives from the surviving organization.accept.* pack.

The redirect pin is the one that found something, and it is a deliberate deviation from the "app-shell untouched" expectation in the dispatch note — flagged here so it can be rejected on its own:

?redirect= is a basename-stripped path by contract in this console. LoginPage.withConsoleBase() re-prefixes the mount before its full-page navigation, and its own doc comment spells out the failure mode; App.tsx's LoginRedirect produces the parameter from useLocation(). The deleted thin page built it from the route param — correct. App-shell's page built it from window.location.pathname, which already carries the mount. A console served under a base href mount would therefore have sent a user who signed in from an invitation link back to /console/console/accept-invitation/….

That is a regression this swap would have introduced, so it is repaired here rather than filed: the page now reads the router. Under the default / mount both spellings are byte-identical, which is why the plain case alone would have shipped it green — the basename case is what separates them, and it is measured in both directions:

pre-fix AssertionError: redirect carries the mount prefix:
Received: "/login?redirect=/console/accept-invitation/inv_1"
post-fix pass

Binding condition 4 — the #3546 slice-three assertion

packages/i18n/src/__tests__/auth-namespace-3546.test.tsx held an assertion titled "acceptInvitation and organization.accept stay separate namespaces". It was right to exist: slice three noticed the duplication and pinned the fact in place rather than guessing which page should win. It now pins the inverse, and negatively, because the defect it guards is silent by construction — two namespaces for one screen read as perfectly healthy from inside either half, at full key parity with real translations in ten packs, and are visible only when both halves are laid side by side, which no gate does.

Three assertions replace the one:

  1. No pack defines any of the 12 retired keys, nor an emptied acceptInvitation: {} root — the shape a partial revert leaves behind. The retired keys are listed by name rather than counted, so a single key returning alone fails just as a full restore does.
  2. organization.accept.* is intact in all ten packs, checked on the four keys the thin page never had (accept, organization, role, expiresAt) — i.e. the deletion did not hollow out the surviving half.
  3. Neither consuming package still asks t() for an acceptInvitation.* key. The scan is call-shaped (t('acceptInvitation. and the template form), not a bare substring, because this file and App.tsx both name the retired namespace in prose to explain why it is retired. Pinning the reader matters because check:i18n-keys would report a revived call site as "key missing from en", and the obvious repair for a missing key is to backfill it — which is exactly the move that rebuilds the second namespace.

The rest of the file re-scopes from 54 keys to the 42 that survive (auth 26 + oauth 16); 42 + 12 = 54 is asserted so the slice's arithmetic stays legible. The fr and ko cases that sampled retired keys moved to other user-facing buttons in the same packs, keeping the writing-system spread intact.

Reverse-verified: reviving acceptInvitation.accept in zh alone turns two independent gates red — this pin (reporting both the key and the namespace root) and all-locales-key-parity's "zh defines no key that en lacks".

Verification

CheckResult
vitest run apps/console/ packages/app-shell/351 files, 3319 passed, 1 skipped, 0 failed
vitest run packages/i18n/ + the new console test36 files, 640 passed
check:i18n-keys (all five classes)exit 0 — 3484 call sites, every in-scope key resolves against en
check:i18n-driftexit 0 — "0 en value(s) changed (0 key(s) added, 12 removed)", i.e. removals reported as removals, zero value edits
all-locales-key-paritygreen (inside the i18n run above)
type-check, downstream sweep --filter '...@object-ui/i18n' (prefix = consumers)all green, incl. examples/console-starter and examples/byo-backend-console
type-check — console / i18n / app-shellgreen
lint — console / i18n / app-shell0 errors. One pre-existing warning on the app-shell page (setIsLoading inside the fetch effect) is unchanged from origin/main and only shifted line number
check-control-bytes, check-changeset-presence, check-changeset-fixed, check-changeset-no-major, check-skills-paths, docs:check-linksall exit 0

TranslationKeys is typeof en, so removing 12 keys narrows an exported type. The downstream sweep above is the prefix direction on purpose. It was also reverse-verified against a stale-.d.ts false green — a temporary probe confirmed TranslationKeys['acceptInvitation'] now errors with TS2339: Property 'acceptInvitation' does not exist, while TranslationKeys['organization']['accept']['accept'] still resolves to the literal "Accept invitation" (so the check is not vacuous). Probe removed.

Incidental

skills/objectui/guides/console-development.md:65 already documented this route as going to DefaultAcceptInvitationPage. The documentation was ahead of the code; this PR makes it true. No doc edit needed.


Generated by Claude Code

…#3811)
Two components shipped for `/accept-invitation/:invitationId` under two i18n
namespaces, and console routed the weaker one: a thin accept/decline pair that
never showed which organization, which role or when the link expired, and left
the user in whatever organization they were already in. App-shell's page —
exported as `DefaultAcceptInvitationPage`, routed by nobody — does all of that
plus `switchOrganization` on accept. Maintainer ruling on #3811 is option A:
console routes the richer page; the thin page and its `acceptInvitation.*`
namespace (12 keys x 10 packs) are deleted. Nothing published is removed —
`DefaultAcceptInvitationPage` keeps its export and becomes the routed one.
The #3546 slice-three assertion that pinned "two namespaces stay separate" now
pins the inverse, negatively: no pack may define any of the 12 retired keys or
an emptied namespace root, and neither consuming package may ask `t()` for one.
One repair was required before the swap was safe. `?redirect=` is a
basename-stripped path by contract here (`LoginPage.withConsoleBase` re-prefixes
the mount); the thin page built it from the route param and app-shell's built it
from `window.location.pathname`, which already carries the mount. A console under
`<base href="/console/">` would have returned the user to
`/console/console/accept-invitation/…`. It now reads the router, like every other
producer of that parameter in this repo. Measured, not argued: the basename case
fails on the pre-fix source and passes after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 3:14pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)27.9 KB350 KB
Entry fileindex-DfJcpdHt.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)485.06KB107.21KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)140.66KB36.25KB
fields (index.js)229.40KB56.93KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.52KB30.68KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)162.81KB39.67KB
plugin-grid (index.js)188.04KB49.91KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.04KB26.67KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude