import { html, LitElement, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import type { UserMeResponse } from "../../core/ApiSchemas"; import { setMarketingConsent } from "../Api"; import { translateText } from "../Utils"; /** * A small, non-blocking prompt docked in the top-right corner that asks a * logged-in player whether they want marketing emails. * * Client-driven consent: it reads the player's consent state from the * `userMeResponse` document event (dispatched after login / on load) and only * appears when the player has not decided yet (`no_response`) and has an email * on file (`hasEmail`). The player's choice is recorded via POST * /marketing/consent; once answered it does not reappear. Players with no email * are handled in account settings ("link an email to subscribe"), not here. */ @customElement("marketing-consent-toast") export class MarketingConsentToast extends LitElement { @state() private visible = false; @state() private busy = false; @state() private errored = false; // Set once the player acts on the prompt this session (answers or dismisses) // so it doesn't reappear until the next load. Not persisted — a dismissal // leaves the server decision at no_response, so it asks again next visit. private answered = false; private onUserMeResponse = (event: Event) => { if (this.answered || this.visible) return; const detail = (event as CustomEvent).detail; if (detail === false) return; const consent = detail.player.marketingConsent; if (consent?.consented === "no_response" && consent.hasEmail) { this.visible = true; } }; createRenderRoot() { return this; } connectedCallback() { super.connectedCallback(); document.addEventListener("userMeResponse", this.onUserMeResponse); } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener("userMeResponse", this.onUserMeResponse); } // Record an explicit decision ("Yes please" -> approved, "No thanks" -> // denied). Awaits the write and disables the buttons while it's in flight, so // a failure surfaces (the toast stays, with an error) instead of a silent // false success that would re-nag on the next load. private async decide(consented: boolean) { if (this.busy) return; this.busy = true; this.errored = false; const ok = await setMarketingConsent(consented); this.busy = false; if (ok) { this.answered = true; this.visible = false; } else { this.errored = true; } } // A subtle close (the X) is not an objection: it records nothing and leaves // the server decision at no_response, only hiding the prompt for this session // so it asks again on a later visit. Only "No thanks" records a denial. private dismiss() { if (this.busy) return; this.answered = true; this.visible = false; } render() { if (!this.visible) return nothing; return html` `; } }