Skip to content

Feat/503 consent pr2 - #566

Open
nhyiramante1 wants to merge 8 commits into
mainfrom
feat/503-consent-pr2
Open

Feat/503 consent pr2#566
nhyiramante1 wants to merge 8 commits into
mainfrom
feat/503-consent-pr2

Conversation

@nhyiramante1

@nhyiramante1 nhyiramante1 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements PR 2 of #503: users can make and manage their logging-consent choice.

  • Adds a required first-run consent gate for authenticated users.
  • Adds the standalone Account & privacy page for changing consent, erasing logged activity, and deleting an account.
  • Threads consentUpdatedAt through the authenticated session so the gate is shown only until a choice is persisted.
  • Updates the active auth state directly from the consent-save response, so a transient session-refresh failure cannot strand a user behind the gate.
  • Synchronizes consent changes across open same-origin tabs. A receiving tab fails closed at none until its authoritative session refresh completes.
  • Adds an Account entry through the user identity chip, preserving the registry/Labs navbar layout.

Account-page behavior

The Account & privacy entry opens a standalone page in the user’s browser. On Office and Google Docs add-in surfaces, that browser may not share the host webview’s storage, so the user may need to sign in again.

The UI states this explicitly. We intentionally do not pass bearer tokens in URLs; an in-app Account screen could be the long-term solution for seamless session continuity.

Validation

  • Frontend: 108 tests passing
  • Backend: 102 tests passing
  • Frontend typecheck and production build passing
  • Backend production build passing
  • Lint has only two pre-existing warnings in pages/draft/index.tsx
  • Browser QA completed for:
    • first-run gate completion
    • consentUpdatedAt session propagation
    • cross-tab consent synchronization

Follow-ups

  • Resync a mounted Account-page chooser when consent changes in another Account tab.
  • Remove the internal alwaysAllow flag from the client session response in a small backend-focused PR.
  • Address the local HTTP/HTTPS trusted-origin fallback separately.

Note:

  • The visual-regression check currently fails only for the demo-page screenshot; functional E2E tests pass. This appears unrelated to the Account/privacy changes.

nhyiramante1 and others added 8 commits July 27, 2026 13:54
…done snapshot)

NOT ready to merge. Half-done work parked to finish Mon 2026-07-27.

Includes:
- onboarding consent gate (no Skip; visibility derived from hasSetConsent)
- shared useConsent save hook (gate + account page, one code path)
- standalone account & privacy page wiring + navbar Account entry
- cross-tab consent sync (consentSync.ts + provider listener)
- device-auth tweaks

Open review items — recorded, deliberately NOT addressed yet:
- in-flight consent choice bug still unfixed
- reconsider whole cross-tab sync vs. dropping target="_blank" on the
  Account link (navigate in-context instead) — reviewer's simpler interim
- broadcastConsentChange: `storage = localStorage` default arg evaluates
  before the try, so catch doesn't cover a throwing global
- LIVE-verify Better Auth returns consentUpdatedAt on get-session (merge prereq)

Still to do Mon: finish code review, e2e + browser QA (two-tab sync,
get-session network check, cross-user gate lifecycle).

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

# Conflicts:
#	backend/.gitignore
#	frontend/src/components/navbar/index.tsx
#	frontend/src/pages/app/index.tsx
…st-effort

Two failure paths found reviewing the onboarding gate:

- The gate's chooser stayed live while the save was in flight. Continue commits
  the level as it read at click time, so a pick made mid-request rendered as
  selected while the server recorded the earlier one -- and on success the gate
  unmounts, hiding the divergence. Disable it while saving, matching the
  account page.

- broadcastConsentChange resolved localStorage in a default parameter, which is
  evaluated outside the function's try. Merely touching localStorage throws in a
  partitioned context (Office task-pane iframe, Safari private mode), and that
  throw escaped into useConsent.save's catch -- rolling the level back and
  reporting failure after the POST had already succeeded. Resolve storage
  inside the try so the broadcast stays best-effort.

Two regression tests cover the storage cases: a throwing setItem and a throwing
localStorage getter.

Co-Authored-By: Claude <noreply@anthropic.com>
…entity chip

The nav strip switches in-app pages; Account is an outbound link to
/account.html, so it was a category error there. It also competed for the
strip's ~300px budget with the Labs overflow button that main's page registry
introduced -- and the hunk anchored on a `{tabs.map(...)}` block that registry
rewrite deleted, so it was a hard rebase conflict besides.

Account affordances belong next to identity, so the footer's existing
"User: {name}" chip becomes the link rather than adding a fifth nav item or a
new icon. Target stays /account.html: same-origin, so the persisted token is
shared and there is no second sign-in. Only the entry point moves.

The chip picks up hover, focus-visible, and cursor affordances it did not need
as plain text.

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

# Conflicts:
#	frontend/src/components/navbar/index.tsx
…nt dip

Cross-tab sync fails closed by dropping the receiving tab to `none` until its
user refresh lands. That made consent < 'usage' a state every broadcast passes
through -- including a consent *raise* -- and the PostHog bridge treated it the
same as a withdrawal: opt_out_capturing() plus reset(). reset() is about
identity, not capture; it rotates the anonymous distinct_id and drops feature
flags and session recording. So every cross-tab consent change silently churned
a person profile for a user who withdrew nothing.

Opting out is what fails closed, and it stays unconditional. reset() now waits
for the provisional window to end. Gating it on "authenticated with an id"
instead was rejected: that reads as identity-still-present and so would skip the
reset on a genuine withdrawal, which is exactly when dropping the locally stored
analytics identity matters most.

So the reconcile now reports when it is in flight. invalidateConsent becomes
reconcileConsentFromOtherTab -- fail closed and re-fetch as one function, so the
provisional window has a defined end that consentPending brackets exactly; two
separate calls left no way to tell a transient `none` from a chosen one. The
in-flight count is a counter rather than a boolean so overlapping pings don't
let the first to finish present the second's provisional level as authoritative.

Tests cover the drop-then-adopt path, the failed refresh (stays `none`, stops
being pending), overlapping reconciles, and applyConsentSnapshot. jsdom and
@testing-library/react arrived with main, so the earlier "no component tests"
constraint no longer applies.

Co-Authored-By: Claude <noreply@anthropic.com>
@nhyiramante1
nhyiramante1 requested a review from kcarnold July 27, 2026 21:47
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.

1 participant