From 35ad6f3abfe8ab924237b0e1c223e300635fe4d5 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 31 Aug 2025 19:09:38 -0700 Subject: [PATCH] create account on purchase (#1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: When purchasing an item, user will be logged in as their email automatically. * Users can be logged in either via discord or email (the top right button has an email or discord icon depending on which is logged in * Created AccountModal to show current login and has option to log in via Discord or send recovery email * Created TokenLoginModal which is triggered during account recovery or after purchase * Update DiscordUserSchema to * Removed choco pattern key listeners, they were causing NPEs when empty input was provided on forms Screenshot 2025-08-29 at 5 35 31 PM support email or discord identity Screenshot 2025-08-29 at 5 38 59 PM Screenshot 2025-08-29 at 5 39 51 PM Screenshot 2025-08-29 at 5 40 03 PM Screenshot 2025-08-29 at 5 40 19 PM ## 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 --- resources/images/DiscordIcon.svg | 1 - resources/images/DiscordLogo.svg | 3 + resources/images/EmailIcon.svg | 5 + resources/images/LoggedOutIcon.svg | 1 + resources/lang/en.json | 11 + src/client/AccountModal.ts | 316 +++++++++++++++++++++++++++ src/client/Cosmetics.ts | 5 +- src/client/DarkModeButton.ts | 2 +- src/client/LangSelector.ts | 6 +- src/client/Main.ts | 121 ++++++---- src/client/TerritoryPatternsModal.ts | 44 ---- src/client/TokenLoginModal.ts | 98 +++++++++ src/client/index.html | 16 +- src/client/jwt.ts | 21 ++ src/core/ApiSchemas.ts | 17 +- 15 files changed, 552 insertions(+), 115 deletions(-) delete mode 100644 resources/images/DiscordIcon.svg create mode 100644 resources/images/DiscordLogo.svg create mode 100644 resources/images/EmailIcon.svg create mode 100644 resources/images/LoggedOutIcon.svg create mode 100644 src/client/AccountModal.ts create mode 100644 src/client/TokenLoginModal.ts 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 +

+ + +
+ +
+ + +
+
+
+
+
+ or +
+
+ + +
+ + +
+
+ +
+ + +
+
+ `; + } + + 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`Discord`; + } else if (this.loggedInEmail) { + return html`Email`; + } + return html`Logged Out`; + } + + 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`