Skip to content

feat(dashboard): platform accounts — sign-in modal (SIWE/email/Google) + drafts on the User API#2045

Open
brunod-e wants to merge 10 commits into
feat/user-apifrom
feat/user-accounts-dashboard
Open

feat(dashboard): platform accounts — sign-in modal (SIWE/email/Google) + drafts on the User API#2045
brunod-e wants to merge 10 commits into
feat/user-apifrom
feat/user-accounts-dashboard

Conversation

@brunod-e

Copy link
Copy Markdown
Collaborator

Summary

Dashboard side of the platform-accounts architecture (stacked on #2044 — the apps/user-api service). Adds the sign-in modal from the Figma design and moves draft proposals onto the session-scoped User API.

Changes

  • /api/user proxy: same-origin, cookie-relaying reverse proxy to the User API (every Set-Cookie preserved; forwards Origin + x-forwarded-host, which better-auth needs behind a path-rewriting proxy). Unlike the gateful proxy, no shared bearer is attached — identity is the user's own session.
  • Sign-in modal (design ③.1/③.2): wallet (full SIWE ceremony via useSiweLogin — opens RainbowKit connect when needed, resumes after connect), email (magic link with enter-email → “Check your inbox” + 30s resend cooldown), Google (social sign-in). Email/Google render only where enabled (NEXT_PUBLIC_EMAIL_LOGIN / NEXT_PUBLIC_GOOGLE_LOGIN); whitelabel is wallet-only. Mounted app-wide via LoginProvider (useLogin().openLogin()).
  • Design system: Modal gains an optional header — headerless modals render a bare close button + screen-reader-only name (ariaLabel), so bespoke layouts like the login surface reuse the same dialog.
  • Drafts on the User API: hand-written /api/user/drafts client (the User API is not in gateful's spec, so no generated SDK). useDrafts is session-gated (useSession), sends no address params, and exposes needsAuth; ProposalCreationForm uses the server-derived isOwner for Editor-vs-Preview (correct for email/Google authors, who have no wallet), and Save prompts sign-in when unauthenticated. Removes dead useDraftRecipient.

Verification

  • pnpm typecheck clean; eslint clean on changed files; 63 create-proposal jest tests green.
  • Manual E2E pending local User API Postgres (follow-up: dev-stack lane for the service).

Notes

  • The old gateful draft endpoints remain live until the follow-up PR removes them from the DAO APIs (contract change + client major).

Refs DEV-978, DEV-979.

🤖 Generated with Claude Code

brunod-e and others added 3 commits July 9, 2026 21:22
…er API

Frontend for the user-accounts architecture (stacked on the @anticapture/
user-api service). Depends on the User API being deployed and reachable
via USER_API_URL.

- /api/user proxy: same-origin, cookie-relaying reverse proxy to the User
  API (forwards x-forwarded-host + Origin for better-auth behind a
  path-rewriting proxy).
- better-auth react client (siweClient) over the /api/user path.
- Sign-in modal from the design, mounted app-wide via LoginProvider; the
  design-system Modal gains an optional header for bespoke layouts. Full
  SIWE ceremony via useSiweLogin (opens RainbowKit connect, then signs).
  Whitelabel is wallet-only; Email/Google shown per design, disabled until
  the Phase 5 server plugins land.
- Drafts moved off the gateful draft SDK onto a session-scoped /api/user
  drafts client: no address params (identity from the cookie), server-
  derived isOwner replaces author-vs-wallet comparison, Save prompts
  sign-in when unauthenticated. Removes dead useDraftRecipient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The modal gains email and Google methods alongside wallet SIWE:
- Email: a two-step flow (enter email -> Check your inbox) sending a
  magic link via the User API, with a 30s resend cooldown.
- Google: better-auth social sign-in, returning to the current page.
- Each method renders only where enabled via NEXT_PUBLIC_EMAIL_LOGIN /
  NEXT_PUBLIC_GOOGLE_LOGIN; whitelabel stays wallet-only.
- authClient gains the magicLink client plugin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
anticapture-storybook Ready Ready Preview, Comment Jul 10, 2026 7:57pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
anticapture Ignored Ignored Jul 10, 2026 7:57pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24ede5e5e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


// Each method is enabled only where its server plugin is configured; a disabled
// method is hidden rather than shown broken. Whitelabel is wallet-only.
const emailLoginEnabled = process.env.NEXT_PUBLIC_EMAIL_LOGIN !== "false";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep email login hidden unless explicitly enabled

Because this defaults to enabled, any environment that does not set NEXT_PUBLIC_EMAIL_LOGIN renders the Email button even when the User API has no magic-link sender configured; the User API only registers that route when RESEND_API_KEY creates a magicLink plugin, so clicking Email in a SIWE-only local/new deployment sends users to a 404. Make this opt-in like Google, or otherwise derive it from the server capability, so disabled methods stay hidden as the comment says.

Useful? React with 👍 / 👎.

hydratedDraftIdRef.current = draftId;
if (!shared) return;
setSharedAuthor(shared.author);
setSharedAuthor(shared.authorAddress ?? undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve walletless draft authors in previews

When a draft is authored via email/Google, the User API returns authorAddress: null; converting that to undefined makes the later sharedAuthor ?? address ?? "" fallback use the viewer's connected wallet. A recipient opening such a shared draft is then displayed as the author/proposer, while an owner without a wallet passes an empty address into the preview. Keep the null walletless-author state distinct and render a non-wallet author instead of falling back to the viewer.

Useful? React with 👍 / 👎.


export const useDrafts = (daoId: string): UseDraftsReturn => {
const { address } = useAccount();
const { data: session, isPending: isSessionPending } = useSession();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop hiding session drafts behind wallet connection

Switching this hook to Better Auth sessions means drafts can belong to email/Google users who have no connected wagmi wallet, but the drafts list consumer still renders drafts only under activeTab === "drafts" && isConnected in GovernanceSection.tsx. After one of these users saves a draft and is redirected to ?tab=drafts, they see the normal proposals list instead of their saved drafts. Gate that view on the auth session/needsAuth state rather than wallet connection.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

🎨 UI Review

⚠️ Figma link found but Figma MCP unreachable — visual validation skipped, configuration error reported. This review is code-only + UX-heuristic; no finding below is validated against the actual Figma design. Vercel preview (anticapture-storybook-git-feat-user-accounts-dashboard-ful.vercel.app) returned 403 Forbidden (deployment protection) — could not load the live preview either.

Sign-in modal (LoginModal.tsx) — options screen

  1. [Code-only] Headerless close button loses the established 44px touch target. In Modal.tsx, the header-present desktop dialog gives its close button an explicit hit-area override:

    // ModalHeader.tsx
    <IconButton variant="ghost" size="sm" icon={X} aria-label="Close" className="-m-2 size-11" />

    but the new headerless branch (used by LoginModal, since it passes no title) omits that override:

    // Modal.tsx, headerless branch
    <IconButton variant="ghost" size="sm" icon={X} aria-label="Close" className="absolute right-3 top-3 z-10" />

    Without -m-2 size-11 (or equivalent), the button falls back to IconButton's default sm box (p-1 + size-3.5 ≈ 22×22px) — about half the tap target used one branch away in the same file. Add size-11 -m-2 (adjusting the absolute offsets accordingly) to match the DS convention already established in ModalHeader.tsx.

  2. [Code-only] Legal-copy link uses the wrong link variant. The "Terms of Use & Privacy Policy" link at the bottom of the sign-in modal renders via DefaultLink, whose base classes are flex items-center gap-1 font-mono tracking-wider uppercase leading-none font-medium (DefaultLink.tsx). That's the nav/meta-link style (e.g. "VIEW ON ETHERSCAN"-type links), so this will render as small, tracked, monospace, ALL-CAPS text:

    <DefaultLink href="/terms-of-service" openInNewTab size="sm" className="text-dimmed hover:text-secondary">
      Terms of Use & Privacy Policy
    </DefaultLink>

    That's an unusual treatment for an inline legal disclaimer sentence sitting under a centered login card. Recommend a plain inline text link (no font-mono/uppercase/tracking-wider) rather than reusing DefaultLink here.

Error states (SIWE + email)

  1. [Code-only] Raw provider/server errors are shown to the user verbatim, with no friendly-copy mapping, which is inconsistent with the otherwise polished copy elsewhere ("Check your inbox", the resend countdown):
    // useSiweLogin.ts
    setError(err instanceof Error ? err.message : "Sign-in failed");
    // useEmailLogin.ts
    setError(sendError.message ?? "Could not send the login link");
    A wallet rejection (e.g. viem/wagmi's UserRejectedRequestError: User rejected the request.) or a raw better-auth error string will surface directly in the modal. Suggest mapping known error codes to short user-facing copy (e.g. "Signature request was cancelled") and falling back to the generic string only for unknowns.

Accessibility (minor)

  1. [UX-heuristic] Resend cooldown countdown isn't in an aria-live region. ResendButton updates its label every second ("Resend in 30s" → … → "Resend link") purely via the disabled button's text; screen-reader users get no announcement when the button becomes actionable. Wrapping the label (or the button) in aria-live="polite" would close this gap. Low severity since it's a disabled control, but worth a one-line fix.

  2. [Code-only] "Back" is a hand-rolled <button> with bespoke classes (text-secondary hover:text-primary mx-auto flex items-center gap-1 text-sm transition-colors) rather than a DS Button/link primitive. Minor DS-adherence nit — functionally fine, just not reusing existing primitives like the rest of the modal does.

Mobile

  1. [UX-heuristic, low confidence] Mobile drawer keeps a full header band even in "headerless" mode. On mobile, Modal renders via DrawerHeader, which still paints its bg-surface-contrast header row (with only an invisible sr-only title) above the centered logo/content. On desktop the headerless variant has no separate header band at all — just a bare floating close button over the body. That's a real compositional difference between breakpoints for the same modal; it may be intentional (bottom sheets in this codebase apparently always keep the header band for the close-button anchor), but I can't confirm intent without Figma this run — worth a quick visual check against the design.

Automated UI review — Path B (Figma MCP unreachable this run). Findings are [Code-only]/[UX-heuristic]; none are [Figma-confirmed].


Generated by Claude Code

@isadorable-png isadorable-png left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🎨 UI Review

Review basis (Path B — Figma link found, MCP unreachable): The PR description links Figma node 3315-19892 (sign-in modal, states ③.1/③.2), but the Figma MCP connector needs an OAuth authorization this non-interactive session can't complete. Findings below are grounded in the diff, the design-system source (Modal, Button, IconButton, DrawerHeader), and UX heuristics — not a pixel-level Figma diff. Please cross-check against the Figma frames directly, especially #3 and #4 below.

Preview: Only the anticapture-storybook Vercel project deploys for this branch (the apps/dashboard project is "Ignored" per this repo's root-directory scoping quirk). The reachable storybook preview URL returned 403 Forbidden (SAML-gated), so no live app preview could be checked either.


Sign-in modal — apps/dashboard/shared/components/auth/LoginModal.tsx

Must-fix

  1. Save → sign-in flow silently drops the user's action. [Code-only] ProposalCreationForm.handleSaveDraft calls openLogin() when drafts.needsAuth (apps/dashboard/features/create-proposal/components/ProposalCreationForm.tsx ~L311-317), but LoginProvider is mounted once globally with only isWhitelabel (apps/dashboard/shared/providers/GlobalProviders.tsx L43) and useLogin()'s openLogin() takes no resume callback. On successful sign-in, LoginModal.tsx L45-48 just does onOpenChange(false); onAuthenticated?.() — but nothing is ever passed as onAuthenticated at the global mount, so the modal closes and the save the user asked for never happens, with zero feedback. The user has to notice nothing happened and click Save again. Wire a resume callback through openLogin(onSuccess), or at minimum toast "Signed in — click Save again" after auth.

  2. Headerless close button loses the 44px touch target. [Code-only] ModalHeader's close button explicitly widens its hit area to 44px (className="-m-2 size-11", apps/dashboard/shared/components/design-system/modal/modal-header/ModalHeader.tsx L52). The new headerless branch added in Modal.tsx (~L146-152) renders the same IconButton at size="sm" with no size override (className="absolute right-3 top-3 z-10"), so it renders at its default ~22px box — this is the only way to close the sign-in modal on desktop, and it's half the sibling header's target and below the 44px minimum. Add the equivalent size override.

  3. Subtitle copy doesn't match which methods are actually shown. [Code-only] The default view always renders "Use your wallet, or just your email." (LoginModal.tsx L145), independent of showEmail/showGoogle (L51-53). If a deployment enables Google but disables email (NEXT_PUBLIC_EMAIL_LOGIN=false, NEXT_PUBLIC_GOOGLE_LOGIN=true), the modal renders a Google button while the copy still promises "just your email." Make the subtitle conditional on which methods are enabled.

Nice-to-have

  1. Mobile bottom sheet still shows a header bar in headerless mode; desktop doesn't. [UX-heuristic] On mobile, Modal routes through DrawerHeader, which always renders its bg-surface-contrast bar (apps/dashboard/shared/components/design-system/drawer/drawer-header/DrawerHeader.tsx L22) — headerless mode only swaps the title for an sr-only span, so the bar (and its own close button) stays visible above the login content. Desktop's headerless branch removes the chrome entirely and floats a bare icon over the body instead. That's an inconsistent "headerless" treatment across breakpoints — worth confirming against the Figma mobile frame once access is restored.

  2. Screen-reader accessible name doesn't track the visible step. [UX-heuristic] The sr-only DialogPrimitive.Title is pinned to the static ariaLabel="Sign in to Anticapture" (LoginModal.tsx L80) across all three views. A screen-reader user always hears "Sign in to Anticapture," even on the "Check your inbox" step, while sighted users see the step-specific heading. Consider passing the current view's heading as the accessible name.


Validated — no change needed

  • Keyboard dismiss (Esc) and focus trap: unaffected by the headerless change — both come from the underlying Radix Dialog.Root/Dialog.Content, independent of whether a header renders.
  • Accessible name for the headerless dialog: correctly supplied via a DialogPrimitive.Title (sr-only), so Radix's aria-labelledby requirement is satisfied — no missing-label a11y warning.
  • Touch targets inside the modal body: Connect wallet / Email / Google / Resend all use Button size="lg" (py-3, ≥44px) — fine, only the standalone close IconButton (#2) is undersized.

Drafts flow — ProposalCreationForm.tsx

Reviewed only for the visible consequence of the session-gating change (must-fix #1 above); the server-derived isOwner logic itself is out of scope for a UI pass.


Caveat recap: Figma MCP could not be authorized in this session — please have a human confirm #3 (copy per config) and #4 (mobile chrome) against the linked frames (node 3315-19892) before merge. No live app preview was reachable either.

Findings: 5 total — 3 must-fix, 2 nice-to-have.


Generated by Claude Code

scripts/dev.sh gains an optional User API lane (port 4003) mirroring the
Address Enrichment one — started via `railway run` when the service is
available (it needs the Railway dev Postgres), exporting USER_API_URL for
the dashboard /api/user proxy, and skipped otherwise. Adds the `pnpm
user-api` root alias and documents the port in the local-dev-stack skill.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

if (!address) {
showCustomToast("Connect a wallet to save and share drafts", "error");
return;

P2 Badge Let walletless sessions save before sharing

When a signed-in email/Google user has no connected wallet and uses Preview → Copy link on a new or dirty draft, this wallet check returns before drafts.saveDraft, even though saving is now session-scoped and supports walletless authors. Gate this path the same way as Save Draft (drafts.needsAuth/openLogin) so authenticated walletless users can save and share drafts without being told to connect a wallet.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +56 to +59
// Drafts are session-scoped: load them once authenticated. Identity comes
// from the session cookie, never a request param. Legacy drafts created
// before accounts are migrated server-side (DAO DB -> User DB, claimed on
// first SIWE login), so there is nothing to migrate from the browser here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve local drafts during migration

Because this hook now only loads User API rows, users who still have pre-API browser drafts under the old draftKey(daoId, address) localStorage key lose access to them after signing in; the parent hook explicitly read those drafts and posted missing ones on first wallet connect. Keep a one-time browser-local migration/import path here before relying solely on server-side rows, otherwise those local-only drafts never appear or get saved to the User API.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 763895281a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +312 to +315
// Saving is session-gated now, not wallet-gated: prompt sign-in when there
// is no session (the user may be connected but not authenticated).
if (drafts.needsAuth) {
openLogin();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Share drafts through the session gate

After this change, saving drafts is session-gated so email/Google users can create User API drafts without a connected wallet, but handleShare still checks !address before it calls the same drafts.saveDraft path for new or dirty drafts. In an email/Google session with no wallet, Copy Link/Share always stops with “Connect a wallet…” even though the server can persist the draft from the session; make the share path use the same drafts.needsAuth/login gate instead of wallet presence.

Useful? React with 👍 / 👎.

brunod-e and others added 2 commits July 10, 2026 10:22
Railway's edge proxy in front of the User API overwrites the
x-forwarded-* set, so the per-host SIWE resolution never saw the real
dashboard host and rejected every request as untrusted_host. Send the
host in a custom x-anticapture-host header (passed through untouched by
the edge); keep x-forwarded-host for edge-less local dev.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56bf29d262

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

daoId as GetDraftProposalPathParamsDaoEnumKey,
draftId,
)
void getDraft(draftId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject drafts from a different DAO

Because this new User API lookup is global by draft id, the response can be for a different DAO than the current route; the returned shared.daoId is ignored and the form is hydrated/published using the route's daoIdEnum. If an ENS draft id is opened under /shu/proposals/new?draftId=..., the ENS draft contents can be edited/shared/published as a SHU proposal. Check shared.daoId against daoId before resetting the form (or redirect to the draft's DAO).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cbc3db9eb8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +70 to +74
const signInWithGoogle = () =>
authClient.signIn.social({
provider: "google",
callbackURL: window.location.href,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route Google OAuth callbacks through the proxy

When NEXT_PUBLIC_GOOGLE_LOGIN=true, this starts Better Auth's hosted OAuth flow, but the User API mounts Better Auth at /api/auth/* and its BETTER_AUTH_URL is the dashboard origin, while this PR only adds a Next proxy for /api/user/*. After the provider redirects back to /api/auth/callback/google, the dashboard has no route for that path (and middleware skips api), so Google sign-in reaches a 404 after successful provider auth; add an /api/auth/* proxy/redirect or configure callbacks to use /api/user/api/auth/*.

Useful? React with 👍 / 👎.

Comment on lines 73 to +74
} catch {
if (cancelled) return;
// Migration failed — fall back to local drafts and surface an error so
// the UI can offer a manual retry. The lock stays unset so a later
// retry (via the `retry()` callback) can replay the migration.
setDrafts(localDrafts);
setError(true);
if (!cancelled) setError(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear stale drafts when loading fails

If listDrafts(daoId) fails after the user has already loaded drafts for another DAO/session, this catch only flips error and leaves the previous drafts array in state. GovernanceSection treats draftsError && drafts.length !== 0 as a warning and still renders/maps those rows, so an API error during a DAO switch can show/share/edit stale drafts under the current DAO; clear drafts at the start of a new load or on failure.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71eb87c7ec

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +314 to 316
if (drafts.needsAuth) {
openLogin();
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate the insufficient-VP modal on a saved draft

When a connected wallet has no Better Auth session and clicks Publish with voting power below the threshold, handlePublishClick calls handleSaveDraft({ navigateToDrafts: false }) and then immediately opens InsufficientVPModal, whose copy says the draft has been saved and whose “View draft” action routes to the drafts tab. This new auth guard only opens the login modal and returns without saving, so that flow can show a false saved-draft confirmation and send the user to an empty/missing draft; make the publish path wait for authentication/save success before opening the insufficient-VP modal.

Useful? React with 👍 / 👎.

…-auth

- the three connect-wallet buttons called RainbowKit's openConnectModal
  directly, so the custom LoginModal never appeared; they now go through
  useLogin().openLogin
- the better-auth client baseURL was missing the service's /api/auth base
  path, so every auth call (get-session, siwe/nonce, ...) 404'd through
  the /api/user proxy
- the modal shows the ANTICAPTURE wordmark per the Figma (whitelabel keeps
  the neutral mark); the wordmark SVG now uses currentColor, with the
  watermark call-site compensating for its previously baked-in opacity

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7335cac67

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


const dao = daoId as GetDraftProposalsPathParamsDaoEnumKey;
const userId = session?.user.id ?? null;
const needsAuth = !isSessionPending && !userId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow session-only users into proposal creation

Because drafts are now keyed to the Better Auth session, an email/Google user can be authenticated while useAccount().isConnected remains false, but the primary New Proposal CTA still only routes when isConnected and otherwise opens RainbowKit directly (apps/dashboard/features/governance/components/governance-overview/GovernanceSection.tsx:351-356). In that session-only case the user cannot reach the editor to create or save a User API draft from the CTA; gate that flow on the auth session/login state rather than wagmi connection alone.

Useful? React with 👍 / 👎.

Comment on lines +453 to 456
const ownsDraft = Boolean(ownedDraft);
const isRecipient = Boolean(draftId) && !drafts.isLoading && !ownsDraft;
// Editor only for a new proposal or an owned draft (no load-dependent flash).
const canShowEditor = !draftId || ownsDraft;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor shared ownership when the drafts list fails

When the private listDrafts request fails but the public getDraft() request still returns shared.isOwner === true, this ownership is only saved into currentDraftId; the editor gate below still depends solely on finding the row in drafts.getDraft(draftId). In that error context an owner opening their own share link is classified as a recipient and cannot edit/update the draft, so include the server isOwner result in the ownership state used by canShowEditor.

Useful? React with 👍 / 👎.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants