diff --git a/resources/images/DiscordIcon.svg b/resources/images/DiscordIcon.svg
deleted file mode 100644
index 4cadbc7f7..000000000
--- a/resources/images/DiscordIcon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/resources/images/DiscordLogo.svg b/resources/images/DiscordLogo.svg
new file mode 100644
index 000000000..836ab0ccc
--- /dev/null
+++ b/resources/images/DiscordLogo.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/images/EmailIcon.svg b/resources/images/EmailIcon.svg
new file mode 100644
index 000000000..4466b000d
--- /dev/null
+++ b/resources/images/EmailIcon.svg
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/resources/images/LoggedOutIcon.svg b/resources/images/LoggedOutIcon.svg
new file mode 100644
index 000000000..17e55d184
--- /dev/null
+++ b/resources/images/LoggedOutIcon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/lang/en.json b/resources/lang/en.json
index 6e78650a5..1b5accb22 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -134,6 +134,17 @@
"enables_title": "Enable Settings",
"start": "Start Game"
},
+ "token_login_modal": {
+ "title": "Logging in...",
+ "logging_in": "Logging in...",
+ "success": "Successfully logged in as {email}!"
+ },
+ "account_modal": {
+ "title": "Account",
+ "logged_in_as": "Logged in as {email}",
+ "logged_in_with_discord": "Logged in with Discord",
+ "recovery_email_sent": "Recovery email sent to {email}"
+ },
"map": {
"map": "Map",
"world": "World",
diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts
new file mode 100644
index 000000000..3e3c56e16
--- /dev/null
+++ b/src/client/AccountModal.ts
@@ -0,0 +1,316 @@
+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`
+
+ ${this.renderInner()}
+
+ `;
+ }
+
+ private renderInner() {
+ if (this.loggedInDiscord) {
+ return this.renderLoggedInDiscord();
+ } else if (this.loggedInEmail) {
+ return this.renderLoggedInEmail();
+ } else {
+ return this.renderLoginOptions();
+ }
+ }
+
+ private renderLoggedInDiscord() {
+ return html`
+
+
+
+ Logged in with Discord as ${this.loggedInDiscord}
+
+
+ ${this.logoutButton()}
+
+ `;
+ }
+
+ private renderLoggedInEmail(): TemplateResult {
+ return html`
+
+
+
+ Logged in as ${this.loggedInEmail}
+
+
+ ${this.logoutButton()}
+
+ `;
+ }
+
+ private logoutButton(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private renderLoginOptions() {
+ return html`
+
+
+
+ Choose your login method
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ 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;
+
+ @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() {
+ 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`
+
+
+
+
+ `;
+ }
+
+ private renderIcon() {
+ if (this.loggedInDiscord) {
+ return html`
`;
+ } else if (this.loggedInEmail) {
+ return html`
`;
+ }
+ return html`
`;
+ }
+
+ private open() {
+ this.recoveryModal?.open();
+ }
+}
diff --git a/src/client/Cosmetics.ts b/src/client/Cosmetics.ts
index e3d602455..0b2f919a7 100644
--- a/src/client/Cosmetics.ts
+++ b/src/client/Cosmetics.ts
@@ -1,6 +1,7 @@
import { UserMeResponse } from "../core/ApiSchemas";
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
import { getApiBase, getAuthHeader } from "./jwt";
+import { getPersistentID } from "./Main";
export async function fetchPatterns(
userMe: UserMeResponse | null,
@@ -44,11 +45,11 @@ export async function handlePurchase(pattern: Pattern) {
headers: {
"Content-Type": "application/json",
authorization: getAuthHeader(),
+ "X-Persistent-Id": getPersistentID(),
},
body: JSON.stringify({
priceId: pattern.product.priceId,
- successUrl: `${window.location.origin}#purchase-completed=true&pattern=${pattern.name}`,
- cancelUrl: `${window.location.origin}#purchase-completed=false`,
+ hostname: window.location.origin,
}),
},
);
diff --git a/src/client/DarkModeButton.ts b/src/client/DarkModeButton.ts
index 5284ca323..56fe34adc 100644
--- a/src/client/DarkModeButton.ts
+++ b/src/client/DarkModeButton.ts
@@ -35,7 +35,7 @@ export class DarkModeButton extends LitElement {
return html`