feat: marketing email consent UI (post-login prompt + account settings)#4554
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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. ChangesMarketing consent feature
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
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. Comment |
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>
1e777c8 to
9f67115
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/client/MarketingConsentToast.test.ts (1)
79-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd 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 callingdecide(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
📒 Files selected for processing (8)
index.htmlresources/lang/en.jsonsrc/client/AccountModal.tssrc/client/Api.tssrc/client/Main.tssrc/client/MarketingConsentToast.tssrc/core/ApiSchemas.tstests/client/MarketingConsentToast.test.ts
…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>
|
Addressed the review feedback in 1. Dismiss (X) conflated with "No thanks" → both recorded a denial. 2. 3. Added tests covering dismiss (no write), denial, approval, and write failure. |
evanpelle
left a comment
There was a problem hiding this comment.
- 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:
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>
|
@evanpelle Fixed in fbb7cd8. The consent Settings tab is now only added when |
…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>
|
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 |
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>
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>



Resolves #4029
Description:
Client-side marketing-email consent capture — the client half of #4029. It consumes the already-merged API (
GET /users/@memarketing-consent state +POST /marketing/consent); no backend changes.Three surfaces:
no_response) and a verified email is on file. Yes / No thanks / dismiss all record a decision, and it never re-nags.new-associationpath — 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), reusingo-buttonand the account modal's existing email field/handlers.Changes:
<marketing-consent-toast>(src/client/MarketingConsentToast.ts), mounted inindex.html, registered inMain.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: optionalplayer.marketingConsent { consented, hasEmail }onUserMeResponse.en.json:marketing_consent.*+account_modal.marketing_*/tab_settings.Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
Iamlewis