`;
}
return html``;
}
// Show the Google link state: a confirmation line when a Google account is
// already linked, otherwise the button to link one.
private renderGoogleLink(): TemplateResult {
const google = this.userMeResponse?.user?.google;
if (google) {
const label = google.email
? translateText("account_modal.linked_to_google_email", {
email: google.email,
})
: translateText("account_modal.linked_to_google");
return html`
${label}
`;
}
return this.renderLinkGoogleButton();
}
// Shown when logged in without a Google identity yet. Lets the user attach
// Google to their existing account (we never auto-merge by email).
private renderLinkGoogleButton(): TemplateResult {
if (this.userMeResponse?.user?.google) return html``;
return html`
`;
}
private async viewGame(gameId: string): Promise {
this.close();
const encodedGameId = encodeURIComponent(gameId);
const newUrl = `/${ClientEnv.workerPath(gameId)}/game/${encodedGameId}`;
history.pushState({ join: gameId }, "", newUrl);
window.dispatchEvent(
new CustomEvent("join-changed", { detail: { gameId: encodedGameId } }),
);
}
private renderLogoutButton(): TemplateResult {
return html`
`;
}
private renderLoginOptions() {
return html`
${translateText("account_modal.sign_in_desc")}
${this.renderCurrency()}
${translateText("account_modal.or")}
`;
}
private handleEmailInput(e: Event) {
const target = e.target as HTMLInputElement;
this.email = target.value;
}
private async handleSubmit() {
if (!this.email) {
alert(translateText("account_modal.enter_email_address"));
return;
}
const success = await sendMagicLink(this.email);
if (success) {
alert(
translateText("account_modal.recovery_email_sent", {
email: this.email,
}),
);
} else {
alert(translateText("account_modal.failed_to_send_recovery_email"));
}
}
private handleDiscordLogin() {
discordLogin();
}
private handleGoogleLogin() {
googleLogin();
}
private async handleLinkGoogle(): Promise {
// On success linkGoogle navigates to Google; the result comes back as a
// `link=...` router arg handled in handleLinkResult. A false return means we
// couldn't start it.
const started = await linkGoogle();
if (!started) {
alert(translateText("account_modal.link_google_failed"));
}
}
// The Google link callback returns us to #modal=account&link=, so the
// router reopens this modal with a `link` arg. Surface the outcome, then strip
// the one-shot param from the URL so a refresh/re-open doesn't replay it.
private handleLinkResult(args?: Record): void {
const link = typeof args?.link === "string" ? args.link : undefined;
if (link === undefined) return;
// replaceState doesn't fire hashchange, so removing the param won't re-route.
const params = new URLSearchParams(window.location.hash.slice(1));
params.delete("link");
const rest = params.toString();
history.replaceState(
null,
"",
rest ? `#${rest}` : window.location.pathname + window.location.search,
);
// Defer so the modal paints before the (blocking) alert. "cancel" needs no
// feedback — the user chose to back out.
if (link === "google") {
setTimeout(
() => alert(translateText("account_modal.link_google_success")),
0,
);
} else if (link === "already_linked") {
setTimeout(
() => alert(translateText("account_modal.link_google_already_linked")),
0,
);
} else if (link === "error") {
setTimeout(
() => alert(translateText("account_modal.link_google_error")),
0,
);
}
}
protected onOpen(args?: Record): void {
this.isLoadingUser = true;
this.handleLinkResult(args);
if (SUBSCRIPTIONS_ENABLED) {
void fetchCosmetics().then((cosmetics) => {
this.cosmetics = cosmetics;
this.requestUpdate();
});
}
void getUserMe()
.then((userMe) => {
if (userMe) {
this.userMeResponse = userMe;
if (this.userMeResponse?.player?.publicId) {
this.loadPlayerProfile(this.userMeResponse.player.publicId);
}
}
this.isLoadingUser = false;
this.requestUpdate();
})
.catch((err) => {
console.warn("Failed to fetch user info in AccountModal.open():", err);
this.isLoadingUser = false;
this.requestUpdate();
});
this.requestUpdate();
}
protected onClose(): void {
this.dispatchEvent(
new CustomEvent("close", { bubbles: true, composed: true }),
);
}
private async handleLogout() {
await logOut();
this.close();
// Refresh the page after logout to update the UI state
window.location.reload();
}
private async loadPlayerProfile(publicId: string): Promise {
try {
const data = await fetchPlayerById(publicId);
if (!data) {
this.requestUpdate();
return;
}
this.recentGames = data.games;
this.statsTree = data.stats;
this.requestUpdate();
} catch (err) {
console.warn("Failed to load player data:", err);
this.requestUpdate();
}
}
}