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`