Files
OpenFrontIO/src/client/AccountModal.ts
T
evanpelle acd65e3371 bugfix: hide account button and token modal login on game start (#1986)
## Description:

The account button persisted after game starting

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

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

evan
2025-09-01 11:34:42 -07:00

329 lines
9.0 KiB
TypeScript

import { html, LitElement, TemplateResult } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { UserMeResponse } from "../core/ApiSchemas";
import "./components/Difficulties";
import "./components/PatternButton";
import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt";
import { translateText } from "./Utils";
@customElement("account-modal")
export class AccountModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
@state() private email: string = "";
private loggedInEmail: string | null = null;
private loggedInDiscord: string | null = null;
constructor() {
super();
}
createRenderRoot() {
return this;
}
render() {
return html`
<o-modal
id="account-modal"
title="${translateText("account_modal.title") || "Account"}"
>
${this.renderInner()}
</o-modal>
`;
}
private renderInner() {
if (this.loggedInDiscord) {
return this.renderLoggedInDiscord();
} else if (this.loggedInEmail) {
return this.renderLoggedInEmail();
} else {
return this.renderLoginOptions();
}
}
private renderLoggedInDiscord() {
return html`
<div class="p-6">
<div class="mb-4">
<p class="text-white text-center mb-4">
Logged in with Discord as ${this.loggedInDiscord}
</p>
</div>
${this.logoutButton()}
</div>
`;
}
private renderLoggedInEmail(): TemplateResult {
return html`
<div class="p-6">
<div class="mb-4">
<p class="text-white text-center mb-4">
Logged in as ${this.loggedInEmail}
</p>
</div>
${this.logoutButton()}
</div>
`;
}
private logoutButton(): TemplateResult {
return html`
<button
@click="${this.handleLogout}"
class="px-6 py-3 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors duration-200"
>
Log Out
</button>
`;
}
private renderLoginOptions() {
return html`
<div class="p-6">
<div class="mb-6">
<h3 class="text-lg font-medium text-white mb-4 text-center">
Choose your login method
</h3>
<!-- Discord Login Button -->
<div class="mb-6">
<button
@click="${this.handleDiscordLogin}"
class="w-full px-6 py-3 text-sm font-medium text-white bg-[#5865F2] border border-transparent rounded-md hover:bg-[#4752C4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-colors duration-200 flex items-center justify-center space-x-2"
>
<img
src="/images/DiscordLogo.svg"
alt="Discord"
class="w-5 h-5"
/>
<span
>${translateText("main.login_discord") ||
"Login with Discord"}</span
>
</button>
</div>
<!-- Divider -->
<div class="relative mb-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-gray-800 text-gray-300">or</span>
</div>
</div>
<!-- Email Recovery -->
<div class="mb-4">
<label
for="email"
class="block text-sm font-medium text-white mb-2"
>
Recover account by email
</label>
<input
type="email"
id="email"
name="email"
.value="${this.email}"
@input="${this.handleEmailInput}"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black"
placeholder="Enter your email address"
required
/>
</div>
</div>
<div class="flex justify-end space-x-3">
<button
@click="${this.close}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Cancel
</button>
<button
@click="${this.handleSubmit}"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Submit
</button>
</div>
</div>
`;
}
private handleEmailInput(e: Event) {
const target = e.target as HTMLInputElement;
this.email = target.value;
}
private async handleSubmit() {
if (!this.email) {
alert("Please enter an email address");
return;
}
try {
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/magic-link`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
redirectDomain: window.location.origin,
email: this.email,
}),
});
if (response.ok) {
alert(
translateText("account_modal.recovery_email_sent", {
email: this.email,
}),
);
this.close();
} else {
console.error(
"Failed to send recovery email:",
response.status,
response.statusText,
);
alert("Failed to send recovery email. Please try again.");
}
} catch (error) {
console.error("Error sending recovery email:", error);
alert("Error sending recovery email. Please try again.");
}
}
private handleDiscordLogin() {
discordLogin();
}
public async open() {
const userMe = await getUserMe();
if (userMe) {
this.loggedInEmail = userMe.user.email ?? null;
this.loggedInDiscord = userMe.user.discord?.global_name ?? null;
}
this.modalEl?.open();
this.requestUpdate();
}
public close() {
this.modalEl?.close();
}
private async handleLogout() {
await logOut();
this.close();
// Refresh the page after logout to update the UI state
window.location.reload();
}
}
@customElement("account-button")
export class AccountButton extends LitElement {
@state() private loggedInEmail: string | null = null;
@state() private loggedInDiscord: string | null = null;
private isVisible = true;
@query("account-modal") private recoveryModal: AccountModal;
constructor() {
super();
document.addEventListener("userMeResponse", (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail) {
const userMeResponse = customEvent.detail as UserMeResponse;
if (userMeResponse.user.email) {
this.loggedInEmail = userMeResponse.user.email;
this.requestUpdate();
} else if (userMeResponse.user.discord) {
this.loggedInDiscord = userMeResponse.user.discord.id;
this.requestUpdate();
}
} else {
// Clear the logged in states when user logs out
this.loggedInEmail = null;
this.loggedInDiscord = null;
this.requestUpdate();
}
});
}
createRenderRoot() {
return this;
}
render() {
if (!this.isVisible) {
return html``;
}
let buttonTitle = "";
if (this.loggedInEmail) {
buttonTitle = translateText("account_modal.logged_in_as", {
email: this.loggedInEmail,
});
} else if (this.loggedInDiscord) {
buttonTitle = translateText("account_modal.logged_in_with_discord");
}
return html`
<div class="fixed top-4 right-4 z-[9999]">
<button
@click="${this.open}"
class="w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-3xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
title="${buttonTitle}"
>
${this.renderIcon()}
</button>
</div>
<account-modal></account-modal>
`;
}
private renderIcon() {
if (this.loggedInDiscord) {
return html`<img
src="/images/DiscordLogo.svg"
alt="Discord"
class="w-6 h-6"
/>`;
} else if (this.loggedInEmail) {
return html`<img
src="/images/EmailIcon.svg"
alt="Email"
class="w-6 h-6"
/>`;
}
return html`<img
src="/images/LoggedOutIcon.svg"
alt="Logged Out"
class="w-6 h-6"
/>`;
}
private open() {
this.recoveryModal?.open();
}
public close() {
this.isVisible = false;
this.recoveryModal?.close();
this.requestUpdate();
}
}