Skip to content

feat: marketing email consent UI (post-login prompt + account settings)#4554

Merged
ryanbarlow97 merged 8 commits into
mainfrom
feat/marketing-consent-client-ui
Jul 10, 2026
Merged

feat: marketing email consent UI (post-login prompt + account settings)#4554
ryanbarlow97 merged 8 commits into
mainfrom
feat/marketing-consent-client-ui

Conversation

@iiamlewis

@iiamlewis iiamlewis commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Resolves #4029

Description:

Client-side marketing-email consent capture — the client half of #4029. It consumes the already-merged API (GET /users/@me marketing-consent state + POST /marketing/consent); no backend changes.

Three surfaces:

  • Post-login prompt — a small, non-blocking toast docked top-right, shown once after login when the player's consent is undecided (no_response) and a verified email is on file. Yes / No thanks / dismiss all record a decision, and it never re-nags.
  • Account → "Account Settings" tab — a persistent on/off toggle to change or withdraw consent at any time (the GDPR change/withdraw path).
  • No-email state — when the account has no verified email, the tab offers to bind one (magic link to a plain email — the backend's new-association path — or link Google) so the player can subscribe.

Styling matches the game's existing panels (bg-surface, border-white/10, shadow-[var(--shadow-malibu-blue)], malibu-blue accent), reusing o-button and the account modal's existing email field/handlers.

Changes:

  • New <marketing-consent-toast> (src/client/MarketingConsentToast.ts), mounted in index.html, registered in Main.ts.
  • AccountModal.ts: new "Account Settings" tab (toggle + bind-email state), optimistic with revert-on-failure.
  • Api.ts: setMarketingConsent()POST /marketing/consent, invalidates cached /users/@me.
  • ApiSchemas.ts: optional player.marketingConsent { consented, hasEmail } on UserMeResponse.
  • en.json: marketing_consent.* + account_modal.marketing_* / tab_settings.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory

Please put your Discord username so you can be contacted if a bug or regression is found:

Iamlewis

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds marketing consent data, an authenticated consent API, a settings tab with email-binding support, and a login-time consent toast. The toast is loaded into the main page, translated, and covered by tests for display, decisions, dismissal, and failed persistence.

Changes

Marketing consent feature

Layer / File(s) Summary
Schema and consent API
src/core/ApiSchemas.ts, src/client/Api.ts
The user response accepts optional consent data, and setMarketingConsent posts consent decisions, handles failures, logs out on unauthorized responses, and invalidates the user cache on success.
Account settings consent flow
src/client/AccountModal.ts, resources/lang/en.json
AccountModal conditionally adds a settings tab, renders consent or email-binding controls, shares email-field markup with recovery, and persists toggle changes with optimistic updates and busy-state handling.
Consent toast and validation
src/client/components/MarketingConsentToast.ts, src/client/Main.ts, index.html, resources/lang/en.json, tests/client/MarketingConsentToast.test.ts
A startup-loaded toast responds to user events, displays eligible consent prompts, handles decisions and dismissal, and tests visibility, successful actions, dismissal, and failed writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Page
  participant MarketingConsentToast
  participant Api
  participant Server

  Page->>MarketingConsentToast: dispatch userMeResponse
  MarketingConsentToast->>MarketingConsentToast: check unanswered consent and email
  MarketingConsentToast->>MarketingConsentToast: render consent dialog
  MarketingConsentToast->>Api: setMarketingConsent(true or false)
  Api->>Server: POST /marketing/consent
  Server-->>Api: success or failure
  Api-->>MarketingConsentToast: true or false
  MarketingConsentToast->>MarketingConsentToast: hide on success or show error
Loading

Possibly related PRs

Poem

A toast appears with buttons bright,
Consent travels left to right.
Settings holds the choice in place,
Email fields share their space.
If writes fail, the prompt stays near—
Ready for another try, sincere.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR does not implement #4029's sign-up opt-in, Discord invite, or Mailchimp sync, so the linked requirements are unmet. Add the sign-up consent control, persist the opt-in in the backend, sync opted-in users to Mailchimp, and show the Discord invite, or revise the linked issue scope.
Out of Scope Changes check ⚠️ Warning The PR adds post-login toast and account-settings consent UI, which are not part of the linked sign-up, Discord, or Mailchimp scope. Limit this PR to the linked sign-up consent and Discord/Mailchimp work, or move the new post-login and account-settings flows to a separate issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: marketing email consent UI across login and account settings.
Description check ✅ Passed The description matches the marketing-consent UI, API, schema, and test changes in the patch.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Client-driven marketing-email consent, consuming the already-merged API
(GET /users/@me marketingConsent + POST /marketing/consent):

- <marketing-consent-toast>: a small, non-blocking top-right prompt shown
  once after login when consent is undecided (no_response) and a verified
  email is on file. Records the choice and never re-nags.
- Account modal "Account Settings" tab: a persistent on/off toggle to
  change or withdraw consent any time; when no email is on the account it
  offers to bind one via magic link or Google link so the player can
  subscribe.
- setMarketingConsent() API helper; optional player.marketingConsent added
  to the UserMeResponse schema.

All copy via translateText()/en.json. Adds a component test for the
toast's show/hide and record-once behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@iiamlewis iiamlewis force-pushed the feat/marketing-consent-client-ui branch from 1e777c8 to 9f67115 Compare July 9, 2026 16:35

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/client/MarketingConsentToast.test.ts (1)

79-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the "No thanks" opt-out path.

The existing test verifies setMarketingConsent(true) when "Yes please" is clicked, but neither the "No thanks" button nor the dismiss (X) button — both calling decide(false) — are tested. A regression in the opt-out path could silently break GDPR-compliant consent recording.

🧪 Suggested test for the opt-out path
   it("records the choice and does not reappear after answering", async () => {
     fireUserMe(userMe("no_response", true));
     expect(await shown()).toBe(true);

     const yes = [...el.querySelectorAll("button")].find((b) =>
       (b.textContent ?? "").includes("marketing_consent.yes"),
     );
     yes?.click();

     expect(setMarketingConsent).toHaveBeenCalledWith(true);
     expect(await shown()).toBe(false);

     // Re-firing the undecided state must not resurrect the prompt.
     fireUserMe(userMe("no_response", true));
     expect(await shown()).toBe(false);
   });
+
+  it("records denial when 'No thanks' is clicked", async () => {
+    fireUserMe(userMe("no_response", true));
+    expect(await shown()).toBe(true);
+
+    const no = [...el.querySelectorAll("button")].find((b) =>
+      (b.textContent ?? "").includes("marketing_consent.no"),
+    );
+    no?.click();
+
+    expect(setMarketingConsent).toHaveBeenCalledWith(false);
+    expect(await shown()).toBe(false);
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/client/MarketingConsentToast.test.ts` around lines 79 - 94, The current
MarketingConsentToast.test only covers the accept flow via the yes button, so
add a separate test that exercises the opt-out path in MarketingConsentToast by
clicking the "No thanks" button (or the dismiss X) and asserting
setMarketingConsent is called with false. Reuse the existing helpers like
fireUserMe, shown, and setMarketingConsent, and verify the toast stays hidden
after the choice is recorded and does not reappear when userMe is fired again.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/client/MarketingConsentToast.test.ts`:
- Around line 79-94: The current MarketingConsentToast.test only covers the
accept flow via the yes button, so add a separate test that exercises the
opt-out path in MarketingConsentToast by clicking the "No thanks" button (or the
dismiss X) and asserting setMarketingConsent is called with false. Reuse the
existing helpers like fireUserMe, shown, and setMarketingConsent, and verify the
toast stays hidden after the choice is recorded and does not reappear when
userMe is fired again.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8e5888a9-76d0-4dc2-b488-9665942601ca

📥 Commits

Reviewing files that changed from the base of the PR and between 274b516 and 1e777c8.

📒 Files selected for processing (8)
  • index.html
  • resources/lang/en.json
  • src/client/AccountModal.ts
  • src/client/Api.ts
  • src/client/Main.ts
  • src/client/MarketingConsentToast.ts
  • src/core/ApiSchemas.ts
  • tests/client/MarketingConsentToast.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
…mail field

- Toast: the X (dismiss) now records nothing and leaves consent at
  no_response so it asks again next visit; only "No thanks" records a
  denial. Previously both permanently opted the user out.
- Toast: decide() awaits setMarketingConsent and disables the buttons while
  the write is in flight; on failure the prompt stays up with an error
  instead of a silent false success that re-nagged on the next load.
- AccountModal: extract a shared renderEmailField() used by both the
  sign-in form and the Account Settings bind-email state so they stay in
  sync.
- Tests cover dismiss (no write), denial, approval, and write failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
@iiamlewis iiamlewis added the approved Approved for a PR, if you assigned to the issue. label Jul 9, 2026
@iiamlewis iiamlewis added this to the v33 milestone Jul 9, 2026
@iiamlewis

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 4e72345:

1. Dismiss (X) conflated with "No thanks" → both recorded a denial.
Fixed. The X now calls a separate dismiss() that records nothing and leaves the server decision at no_response, hiding the prompt only for the current session so it asks again on the next visit. Only "No thanks" records a denied. A player who just wants it out of the way is no longer permanently opted out.

2. void setMarketingConsent(...) swallowed failures; the busy guard did nothing.
Fixed. decide() now awaits the write and holds busy true across the request, so the buttons are genuinely disabled while it's in flight. On success it hides and marks the session answered; on failure it keeps the toast up and shows an error (marketing_consent.error) instead of a silent false success that re-nagged on the next load — matching the settings-tab setConsent pattern.

3. renderEmailBinding() duplicated the sign-in form's email field.
Fixed. Extracted a shared renderEmailField() (email input + magic-link o-button), now used by both the sign-in form and the Account Settings bind-email state, so styling/handler changes only happen in one place.

Added tests covering dismiss (no write), denial, approval, and write failure.

@iiamlewis

iiamlewis commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author
image-1783616220502 Screenshot 2026-07-09 175920

@evanpelle evanpelle 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.

  1. Settings tab is misleading when the API omits marketingConsent (medium).
    The schema comment says the optional field means "no consent UI", but renderSettingsTab doesn't honor that: with consent === undefined, hasEmail defaults to false, so a user who does have a linked email would see "Link an email to your account to subscribe" plus the email/Google binding form. Suggest hiding the panel (or the whole tab) when marketingConsent is undefined:

@ryanbarlow97

ryanbarlow97 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
image

please check ui for mobile, it's cutting off words (on 14 pro max, so even smaller screens will struggle)

Per review (evanpelle): when the API doesn't return player.marketingConsent
the schema treats it as "no consent UI", but renderSettingsTab still rendered
with hasEmail defaulting to false — showing a misleading "link an email"
prompt to users who may already have a linked email. The consent settings tab
is now only added to the modal when marketingConsent is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread resources/lang/en.json Outdated
@iiamlewis

Copy link
Copy Markdown
Contributor Author

@evanpelle Fixed in fbb7cd8. The consent Settings tab is now only added when player.marketingConsent is present; when the API omits it (the schema's "no consent UI" case) the tab is hidden entirely, so a user with a linked email no longer sees the misleading "link an email" prompt. Verified both states in the running client — tab present when consent state is returned, absent when it isn't.

Comment thread src/client/components/MarketingConsentToast.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 9, 2026
Comment thread src/client/components/MarketingConsentToast.ts
Comment thread src/client/AccountModal.ts Outdated
…ve toast

Per ryanbarlow97's review:
- en.json: drop the em-dash from the consent error string.
- Move MarketingConsentToast into src/client/components/ (fix imports,
  registration, and test path accordingly).
- Toast is now a full-width top banner on small screens and the top-right
  236px card on sm+ — fixes the cramped/word-cutoff mobile layout (the old
  wrapping button label) while keeping the top-right placement on desktop.

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

Copy link
Copy Markdown
Contributor Author

Re: mobile UI cutting off words — fixed in 898d50b. The toast is now a full-width top banner on small screens (and the top-right card on sm+). Verified at 375px and 430px (14 Pro Max): title, body, and both buttons render fully with no cutoff and no horizontal overflow. The earlier cutoff was the old "Yes, keep me posted" button label wrapping — it's now "Yes please".

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 10, 2026
Adds a unit test verifying the marketing-consent toast carries the
full-width-banner classes for small screens (left-4/right-4/w-auto) and the
sm+ top-right card overrides (sm:left-auto/sm:right-4/sm:w-[236px]), guarding
the mobile fix (verified visually on a 14 Pro Max / 430px viewport).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 10, 2026
iiamlewis and others added 2 commits July 10, 2026 11:00
Reverses the earlier hide-when-absent guard (ryanbarlow97: "we should always
show the tab — worst case it's empty"). The tab is now always present; when
the API omits marketingConsent, renderSettingsTab renders empty instead of the
bind-email prompt, so a user who may already have an email isn't shown a
misleading "link an email" state. The toggle still renders normally when
consent state is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Collapse the modalConfig settings tab to a single line, matching the other
  tab entries (fits within the 80-col limit).
- Rename the tab label from "Account Settings" to "Settings".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-project-automation github-project-automation Bot moved this from Triage to Final Review in OpenFront Release Management Jul 10, 2026
@ryanbarlow97 ryanbarlow97 added this pull request to the merge queue Jul 10, 2026
Merged via the queue into main with commit 8e5f279 Jul 10, 2026
12 checks passed
@ryanbarlow97 ryanbarlow97 deleted the feat/marketing-consent-client-ui branch July 10, 2026 10:16
@github-project-automation github-project-automation Bot moved this from Final Review to Complete in OpenFront Release Management Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Approved for a PR, if you assigned to the issue.

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

[Feature] Marketing permissions capture + Discord join prompt on sign-up

3 participants