From e6de0fb95dcace6397e8ac98b469e0c40b74c203 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:49:21 -0600 Subject: [PATCH] [UI/UX] Split login and register into separate menus (#6851) * [UI/UX] Split login and register into separate menus * Resize the container for the "Login" and "Register" buttons * Make container width dynamic Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> * Make logo position dynamic * Apply suggestions - Consolidate code in `LoginPhase` - Use `truncateString` utility function in `form-modal-ui-handler.ts` - Move login form to match location of register form - Swap `x` values of username and download buttons * Apply suggestions --------- Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> --- src/enums/ui-mode.ts | 1 + src/phases/login-phase.ts | 185 +++++----- src/ui/handlers/form-modal-ui-handler.ts | 68 ++-- src/ui/handlers/login-form-ui-handler.ts | 340 ++---------------- .../handlers/login-or-register-ui-handler.ts | 72 ++++ ...ogin-register-info-container-ui-handler.ts | 233 ++++++++++++ src/ui/handlers/oauth-providers-ui-handler.ts | 107 ++++++ .../handlers/registration-form-ui-handler.ts | 131 +++---- src/ui/ui.ts | 2 + 9 files changed, 641 insertions(+), 498 deletions(-) create mode 100644 src/ui/handlers/login-or-register-ui-handler.ts create mode 100644 src/ui/handlers/login-register-info-container-ui-handler.ts create mode 100644 src/ui/handlers/oauth-providers-ui-handler.ts diff --git a/src/enums/ui-mode.ts b/src/enums/ui-mode.ts index 75c07a5f63c..d4f27c66255 100644 --- a/src/enums/ui-mode.ts +++ b/src/enums/ui-mode.ts @@ -31,6 +31,7 @@ export enum UiMode { POKEDEX, POKEDEX_SCAN, POKEDEX_PAGE, + LOGIN_OR_REGISTER, LOGIN_FORM, REGISTRATION_FORM, LOADING, diff --git a/src/phases/login-phase.ts b/src/phases/login-phase.ts index 0aaf97e234b..aebdcdae152 100644 --- a/src/phases/login-phase.ts +++ b/src/phases/login-phase.ts @@ -10,7 +10,13 @@ import i18next, { t } from "i18next"; export class LoginPhase extends Phase { public readonly phaseName = "LoginPhase"; - private showText: boolean; + + /** + * Whether to load the "login or register" text. + * Only `true` the first time the phase runs, the text stays on screen after that. + * @defaultValue `true` + */ + private readonly showText: boolean; constructor(showText = true) { super(); @@ -18,103 +24,114 @@ export class LoginPhase extends Phase { this.showText = showText; } - start(): void { + public override async start(): Promise { + const { gameData, ui } = globalScene; + super.start(); const hasSession = !!getCookie(sessionIdKey); - globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); - executeIf(bypassLogin || hasSession, updateUserInfo).then(response => { - const success = response ? response[0] : false; - const statusCode = response ? response[1] : null; - if (!success) { - if (!statusCode || statusCode === 400) { - if (this.showText) { - globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount")); - } + ui.setMode(UiMode.LOADING, { buttonActions: [] }); - globalScene.playSound("ui/menu_open"); + const response = await executeIf(bypassLogin || hasSession, updateUserInfo); + const success = response?.[0] ?? false; + const statusCode = response ? response[1] : null; - const loadData = () => { - updateUserInfo().then(success => { - if (!success[0]) { - removeCookie(sessionIdKey); - globalScene.reset(true, true); - return; - } - globalScene.gameData.loadSystem().then(() => this.end()); - }); - }; + if (!success) { + this.checkStatus(statusCode); + return; + } - globalScene.ui.setMode(UiMode.LOGIN_FORM, { - buttonActions: [ - () => { - globalScene.ui.playSelect(); - loadData(); - }, - () => { - globalScene.playSound("ui/menu_open"); - globalScene.ui.setMode(UiMode.REGISTRATION_FORM, { - buttonActions: [ - () => { - globalScene.ui.playSelect(); - updateUserInfo().then(success => { - if (!success[0]) { - removeCookie(sessionIdKey); - globalScene.reset(true, true); - return; - } - this.end(); - }); - }, - () => { - globalScene.phaseManager.unshiftNew("LoginPhase", false); - this.end(); - }, - ], - }); - }, - () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); - const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID; - const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`; - window.open(discordUrl, "_self"); - }, - () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`); - const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; - const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; - window.open(googleUrl, "_self"); - }, - ], - }); - } else if (statusCode === 401) { - removeCookie(sessionIdKey); - globalScene.reset(true, true); - } else { - globalScene.phaseManager.unshiftNew("UnavailablePhase"); - super.end(); - } - return null; - } - globalScene.gameData.loadSystem().then(success => { - if (success || bypassLogin) { - this.end(); - } else { - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText(t("menu:failedToLoadSaveData")); - } - }); - }); + await gameData.loadSystem(); + if (success || bypassLogin) { + await this.end(); + return; + } + ui.setMode(UiMode.MESSAGE); + ui.showText(t("menu:failedToLoadSaveData")); } - end(): void { + public override async end(): Promise { globalScene.ui.setMode(UiMode.MESSAGE); if (!globalScene.gameData.gender) { globalScene.phaseManager.unshiftNew("SelectGenderPhase"); } - handleTutorial(Tutorial.Intro).then(() => super.end()); + await handleTutorial(Tutorial.Intro); + super.end(); + } + + private checkStatus(statusCode: number | null): void { + if (!statusCode || statusCode === 400) { + this.showLoginRegister(); + return; + } + + if (statusCode === 401) { + removeCookie(sessionIdKey); + globalScene.reset(true, true); + return; + } + + globalScene.phaseManager.unshiftNew("UnavailablePhase"); + super.end(); + } + + private showLoginRegister(): void { + const { gameData, phaseManager, ui } = globalScene; + + const backButton = () => { + phaseManager.unshiftNew("LoginPhase", false); + this.end(); + }; + + const checkUserInfo = async (): Promise => { + ui.playSelect(); + const success = await updateUserInfo(); + if (!success[0]) { + removeCookie(sessionIdKey); + globalScene.reset(true, true); + return false; + } + return true; + }; + + const loginButton = async () => { + const success = await checkUserInfo(); + if (!success) { + return; + } + await gameData.loadSystem(); + this.end(); + }; + + const registerButton = async () => { + const success = await checkUserInfo(); + if (!success) { + return; + } + this.end(); + }; + + const goToLoginButton = () => { + globalScene.playSound("ui/menu_open"); + + ui.setMode(UiMode.LOGIN_FORM, { buttonActions: [loginButton, backButton] }); + }; + + const goToRegistrationButton = () => { + globalScene.playSound("ui/menu_open"); + + ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [registerButton, backButton] }); + }; + + if (this.showText) { + ui.showText(i18next.t("menu:logInOrCreateAccount")); + } + + globalScene.playSound("ui/menu_open"); + + ui.setMode(UiMode.LOGIN_OR_REGISTER, { buttonActions: [goToLoginButton, goToRegistrationButton] }); } } diff --git a/src/ui/handlers/form-modal-ui-handler.ts b/src/ui/handlers/form-modal-ui-handler.ts index a4771721995..b6db5d41ce8 100644 --- a/src/ui/handlers/form-modal-ui-handler.ts +++ b/src/ui/handlers/form-modal-ui-handler.ts @@ -5,7 +5,8 @@ import type { ModalConfig } from "#ui/modal-ui-handler"; import { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextInputObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; -import { fixedInt } from "#utils/common"; +import { fixedInt, truncateString } from "#utils/common"; +import type Phaser from "phaser"; import type InputText from "phaser3-rex-plugins/plugins/inputtext"; export interface FormModalConfig extends ModalConfig { @@ -24,30 +25,35 @@ export abstract class FormModalUiHandler extends ModalUiHandler { /** * Get configuration for all fields that should be part of the modal + * @remarks * Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view * @returns array of {@linkcode InputFieldConfig} */ abstract getInputFieldConfigs(): InputFieldConfig[]; - getHeight(config?: ModalConfig): number { + public override getHeight(config?: FormModalConfig): number { return ( 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) - + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + + (config?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28 ); } - getReadableErrorMessage(error: string): string { - if (error?.indexOf("connection refused") > -1) { + public getReadableErrorMessage(error: string): string { + if (!error) { + return ""; + } + + if (error.includes("connection refused")) { return "Could not connect to the server"; } return error; } - setup(): void { + public override setup(): void { super.setup(); const config = this.getInputFieldConfigs(); @@ -58,16 +64,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler { this.updateFields(config, hasTitle); } - this.errorMessage = addTextObject( - 10, - (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), - "", - TextStyle.TOOLTIP_CONTENT, - { - fontSize: "42px", - wordWrap: { width: 850 }, - }, - ) + const errorMessageY = (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(); + const errorMessageOptions: Phaser.Types.GameObjects.Text.TextStyle = { fontSize: "42px", wordWrap: { width: 850 } }; + this.errorMessage = addTextObject(10, errorMessageY, "", TextStyle.TOOLTIP_CONTENT, errorMessageOptions) .setColor(getTextColor(TextStyle.SUMMARY_PINK)) .setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)) .setVisible(false); @@ -75,21 +74,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler { } protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) { - const inputContainers = (this.inputContainers = new Array(fieldsConfig.length)); - const inputs = (this.inputs = new Array(fieldsConfig.length)); - const formLabels = (this.formLabels = new Array(fieldsConfig.length)); + this.inputContainers = new Array(fieldsConfig.length); + this.inputs = new Array(fieldsConfig.length); + this.formLabels = new Array(fieldsConfig.length); for (const [f, config] of fieldsConfig.entries()) { + const labelY = (hasTitle ? 31 : 5) + 20 * f; // The Pokédex Scan Window uses width `300` instead of `160` like the other forms // Therefore, the label does not need to be shortened - const label = addTextObject( - 10, - (hasTitle ? 31 : 5) + 20 * f, - config.label.length > 25 && this.getWidth() < 200 ? config.label.slice(0, 20) + "..." : config.label, - TextStyle.TOOLTIP_CONTENT, - ); + const labelContent = this.getWidth() < 200 ? truncateString(config.label, 25) : config.label; + const label = addTextObject(10, labelY, labelContent, TextStyle.TOOLTIP_CONTENT); label.name = "formLabel" + f; - formLabels[f] = label; + this.formLabels[f] = label; this.modalContainer.add(label); const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5; @@ -109,14 +105,14 @@ export abstract class FormModalUiHandler extends ModalUiHandler { inputContainer.add([inputBg, input]); - inputContainers[f] = inputContainer; + this.inputContainers[f] = inputContainer; this.modalContainer.add(inputContainer); - inputs[f] = input; + this.inputs[f] = input; } } - override show(args: any[]): boolean { + public override show(args: any[]): boolean { if (super.show(args)) { for (const ic of this.inputContainers) { ic.setActive(true).setVisible(true); @@ -129,7 +125,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler { // Auto focus the first input field after a short delay, to prevent accidental inputs setTimeout(() => { - this.inputs[0].setFocus(); + this.inputs[0]?.setFocus(); }, 50); // #region: Override button pointerDown @@ -170,7 +166,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler { return false; } - processInput(button: Button): boolean { + public override processInput(button: Button): boolean { if (button === Button.SUBMIT && this.submitAction) { this.submitAction(); return true; @@ -179,13 +175,13 @@ export abstract class FormModalUiHandler extends ModalUiHandler { return false; } - sanitizeInputs(): void { + public sanitizeInputs(): void { for (const input of this.inputs) { input.text = input.text.trim(); } } - updateContainer(config?: ModalConfig): void { + public override updateContainer(config?: ModalConfig): void { super.updateContainer(config); this.errorMessage @@ -193,21 +189,21 @@ export abstract class FormModalUiHandler extends ModalUiHandler { .setVisible(!!this.errorMessage.text); } - hide(): void { + public hide(): void { this.modalContainer.setVisible(false).setActive(false); for (const ic of this.inputContainers) { ic.setVisible(false).setActive(false); } } - unhide(): void { + public unhide(): void { this.modalContainer.setActive(true).setVisible(true); for (const ic of this.inputContainers) { ic.setActive(true).setVisible(true); } } - clear(): void { + public override clear(): void { super.clear(); this.modalContainer.setVisible(false); diff --git a/src/ui/handlers/login-form-ui-handler.ts b/src/ui/handlers/login-form-ui-handler.ts index 6c4a7cfeaa6..98358c26ba4 100644 --- a/src/ui/handlers/login-form-ui-handler.ts +++ b/src/ui/handlers/login-form-ui-handler.ts @@ -1,152 +1,60 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; -import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { languageOptions } from "#system/settings-language"; -import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; -import { addTextObject } from "#ui/text"; -import { addWindow } from "#ui/ui-theme"; -import { fixedInt } from "#utils/common"; +import { OAuthProvidersUiHandler } from "#ui/oauth-providers-ui-handler"; import i18next from "i18next"; -import JSZip from "jszip"; -interface BuildInteractableImageOpts { - scale?: number; - x?: number; - y?: number; - origin?: { x: number; y: number }; -} +const ERR_USERNAME: string = "invalid username"; +const ERR_PASSWORD: string = "invalid password"; +const ERR_ACCOUNT_EXIST: string = "account doesn't exist"; +const ERR_PASSWORD_MATCH: string = "password doesn't match"; -/** - * The maximum number of saves that are allowed to show up in the username panel pefore - * the `P02: Too many saves` popup is displayed. - * - * @privateRemarks - * This limitation is in place to allow for the password reset helpers to get - * enough information in one screenshot. If the user has too many saves, this - * complicates the interaction as it would require scrolling, which will - * make tickets take longer to resolve. - */ -const MAX_SAVES_FOR_USERNAME_PANEL = 7; - -export class LoginFormUiHandler extends FormModalUiHandler { - private readonly ERR_USERNAME: string = "invalid username"; - private readonly ERR_PASSWORD: string = "invalid password"; - private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; - private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; - private readonly ERR_NO_SAVES: string = "No save files found"; - private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found"; - - private googleImage: Phaser.GameObjects.Image; - private discordImage: Phaser.GameObjects.Image; - private usernameInfoImage: Phaser.GameObjects.Image; - private saveDownloadImage: Phaser.GameObjects.Image; - private changeLanguageImage: Phaser.GameObjects.Image; - private externalPartyContainer: Phaser.GameObjects.Container; - private infoContainer: Phaser.GameObjects.Container; - private externalPartyBg: Phaser.GameObjects.NineSlice; - private externalPartyTitle: Phaser.GameObjects.Text; +export class LoginFormUiHandler extends OAuthProvidersUiHandler { constructor(mode: UiMode | null = null) { super(mode); } - setup(): void { - super.setup(); - - this.buildExternalPartyContainer(); - this.buildInfoContainer(); - } - - private buildExternalPartyContainer() { - this.externalPartyContainer = globalScene.add.container(0, 0); - this.externalPartyContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width / 2, globalScene.scaledCanvas.height / 2), - Phaser.Geom.Rectangle.Contains, - ); - this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL).setOrigin(0.5, 0); - this.externalPartyBg = addWindow(0, 0, 0, 0); - - this.googleImage = this.buildInteractableImage("google", "google-icon"); - this.discordImage = this.buildInteractableImage("discord", "discord-icon"); - - this.externalPartyContainer - .add([this.externalPartyBg, this.externalPartyTitle, this.googleImage, this.discordImage]) - .setVisible(false); - this.getUi().add(this.externalPartyContainer); - } - - private buildInfoContainer() { - this.infoContainer = globalScene.add.container(0, 0); - - this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", { - x: 20, - scale: 0.5, - }); - - this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", { - x: 0, - scale: 0.75, - }); - - this.changeLanguageImage = this.buildInteractableImage("language_icon", "change-language-icon", { - x: 40, - scale: 0.5, - }); - - this.infoContainer - .add([this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage]) - .setVisible(false) - .disableInteractive(); - this.getUi().add(this.infoContainer); - } - - override getModalTitle(_config?: ModalConfig): string { - let key = "menu:login"; + public override getModalTitle(): string { if (import.meta.env.VITE_SERVER_URL === "https://apibeta.pokerogue.net") { - key = "menu:loginBeta"; + return i18next.t("menu:loginBeta"); } - return i18next.t(key); + return i18next.t("menu:login"); } - override getWidth(_config?: ModalConfig): number { + public override getWidth(): number { return 160; } - override getMargin(_config?: ModalConfig): [number, number, number, number] { - return [0, 0, 48, 0]; + public override getMargin(): [number, number, number, number] { + return [0, 20, 48, 0]; } - override getButtonLabels(_config?: ModalConfig): string[] { - return [i18next.t("menu:login"), i18next.t("menu:register")]; + public override getButtonLabels(): string[] { + return [i18next.t("menu:login"), i18next.t("menu:goBack")]; } - override getReadableErrorMessage(error: string): string { - const colonIndex = error?.indexOf(":"); - if (colonIndex > 0) { - error = error.slice(0, colonIndex); + public override getReadableErrorMessage(error: string): string { + if (!error) { + return ""; } + switch (error) { - case this.ERR_USERNAME: + case ERR_USERNAME: return i18next.t("menu:invalidLoginUsername"); - case this.ERR_PASSWORD: + case ERR_PASSWORD: return i18next.t("menu:invalidLoginPassword"); - case this.ERR_ACCOUNT_EXIST: + case ERR_ACCOUNT_EXIST: return i18next.t("menu:accountNonExistent"); - case this.ERR_PASSWORD_MATCH: + case ERR_PASSWORD_MATCH: return i18next.t("menu:unmatchingPassword"); - case this.ERR_NO_SAVES: - return "P01: " + i18next.t("menu:noSaves"); - case this.ERR_TOO_MANY_SAVES: - return "P02: " + i18next.t("menu:tooManySaves"); } return super.getReadableErrorMessage(error); } - override getInputFieldConfigs(): InputFieldConfig[] { + public override getInputFieldConfigs(): InputFieldConfig[] { const inputFieldConfigs: InputFieldConfig[] = []; inputFieldConfigs.push( { label: i18next.t("menu:username") }, @@ -158,12 +66,13 @@ export class LoginFormUiHandler extends FormModalUiHandler { return inputFieldConfigs; } - override show(args: any[]): boolean { + public override show(args: any[]): boolean { if (!super.show(args)) { return false; } const config = args[0] as ModalConfig; - this.processExternalProvider(config); + this.processExternalProvider(); + this.showInfoContainer(config); const originalLoginAction = this.submitAction; this.submitAction = () => { if (globalScene.tweens.getTweensOf(this.modalContainer).length > 0) { @@ -173,7 +82,7 @@ export class LoginFormUiHandler extends FormModalUiHandler { this.submitAction = originalLoginAction; this.sanitizeInputs(); globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); - const onFail = error => { + const onFail = (error: string | null) => { globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() })); globalScene.ui.playError(); }; @@ -199,199 +108,4 @@ export class LoginFormUiHandler extends FormModalUiHandler { return true; } - - override clear() { - super.clear(); - this.externalPartyContainer.setVisible(false).setActive(false); - this.infoContainer.setVisible(false).setActive(false); - this.setMouseCursorStyle("default"); //reset cursor - - [ - this.discordImage, - this.googleImage, - this.usernameInfoImage, - this.saveDownloadImage, - this.changeLanguageImage, - ].forEach(img => { - img.off("pointerdown"); - }); - } - - override destroy() { - super.destroy(); - this.externalPartyContainer.destroy(); - this.infoContainer.destroy(); - } - - /** - * Show a panel with all usernames found in localStorage - * - * @remarks - * Up to {@linkcode MAX_SAVES_FOR_USERNAME_PANEL} usernames are shown, otherwise P02 is triggered - * @param onFail - Callback function for failure - */ - private showUsernames(config: ModalConfig) { - if (globalScene.tweens.getTweensOf(this.infoContainer).length === 0) { - const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage - const keyToFind = "data_"; - const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0); - if (dataKeys.length === 0) { - this.onFail(this.ERR_NO_SAVES, config); - return; - } - if (dataKeys.length > MAX_SAVES_FOR_USERNAME_PANEL) { - this.onFail(this.ERR_TOO_MANY_SAVES, config); - return; - } - const options: OptionSelectItem[] = []; - const handler = () => { - globalScene.ui.revertMode(); - this.infoContainer.disableInteractive(); - return true; - }; - for (const key of dataKeys) { - options.push({ - label: key.replace(keyToFind, ""), - handler, - }); - } - globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { - options, - delay: 1000, - }); - this.infoContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width, globalScene.game.canvas.height), - Phaser.Geom.Rectangle.Contains, - ); - } - } - - /** - * - */ - private onFail(error: string, config: ModalConfig) { - const ui = globalScene.ui; - ui.setMode(UiMode.LOADING, { buttonActions: [] }); - ui.setModeForceTransition(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() })); - ui.playError(); - } - - /** - * Collect the user's save files from localStorage and download them as a zip file - * - * @remarks - * Used as the `pointerDown` callback for the save download image - * @param config - The modal configuration - */ - private async downloadSaves(config: ModalConfig): Promise { - // find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip - const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage - const keyToFind = "data_"; - const sessionKeyToFind = "sessionData"; - const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0); - const sessionKeys = localStorageKeys.filter(ls => ls.indexOf(sessionKeyToFind) >= 0); - if (dataKeys.length <= 0 && sessionKeys.length <= 0) { - this.onFail(this.ERR_NO_SAVES, config); - return; - } - const zip = new JSZip(); - // Bang is safe here because of the filter above - for (const dataKey of dataKeys) { - zip.file(dataKey + ".prsv", localStorage.getItem(dataKey)!); - } - for (const sessionKey of sessionKeys) { - zip.file(sessionKey + ".prsv", localStorage.getItem(sessionKey)!); - } - const content = await zip.generateAsync({ type: "blob" }); - const url = URL.createObjectURL(content); - const a = document.createElement("a"); - a.href = url; - a.download = "pokerogue_saves.zip"; - a.click(); - URL.revokeObjectURL(url); - } - - private processExternalProvider(config: ModalConfig): void { - this.externalPartyTitle - .setText(i18next.t("menu:orUse") ?? "") - .setX(20 + this.externalPartyTitle.text.length) - .setVisible(true); - - const externalPartyContainer = this.externalPartyContainer - .setPositionRelative(this.modalContainer, 175, 0) - .setVisible(true); - - const externalPartyBg = this.externalPartyBg.setSize(this.externalPartyTitle.text.length + 50, this.modalBg.height); - this.getUi().moveTo(externalPartyContainer, this.getUi().length - 1); - - const externalPartyIconWidth = externalPartyBg.width / 3.1; - this.discordImage; - const infoContainer = this.infoContainer.setPosition(5, -76).setVisible(true); - this.getUi().moveTo(infoContainer, this.getUi().length - 1); - - this.discordImage // formatting - .setPosition(externalPartyIconWidth, externalPartyBg.height - 40) - .on("pointerdown", () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); - const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID; - const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`; - window.open(discordUrl, "_self"); - }); - - this.googleImage // formatting - .setPosition(externalPartyIconWidth, externalPartyBg.height - 60) - .on("pointerdown", () => { - const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`); - const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; - const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; - window.open(googleUrl, "_self"); - }); - - this.usernameInfoImage // formatting - .setPositionRelative(infoContainer, 0, 0) - .on("pointerdown", () => this.showUsernames(config)); - - this.saveDownloadImage // formatting - .setPositionRelative(infoContainer, 20, 0) - .on("pointerdown", () => this.downloadSaves(config)); - - this.changeLanguageImage.setPositionRelative(infoContainer, 40, 0).on("pointerdown", () => { - globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { - options: languageOptions, - maxOptions: 7, - delay: 1000, - }); - }); - - this.externalPartyContainer.setAlpha(0); - globalScene.tweens.add({ - targets: this.externalPartyContainer, - duration: fixedInt(1000), - ease: "Sine.easeInOut", - y: "-=24", - alpha: 1, - }); - - this.infoContainer.setAlpha(0); - globalScene.tweens.add({ - targets: this.infoContainer, - duration: fixedInt(1000), - ease: "Sine.easeInOut", - y: "-=24", - alpha: 1, - }); - } - - private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) { - const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts; - const img = globalScene.add - .image(x, y, texture) - .setName(name) - .setOrigin(origin.x, origin.y) - .setScale(scale) - .setInteractive(); - this.addInteractionHoverEffect(img); - - return img; - } } diff --git a/src/ui/handlers/login-or-register-ui-handler.ts b/src/ui/handlers/login-or-register-ui-handler.ts new file mode 100644 index 00000000000..b5733a9eaee --- /dev/null +++ b/src/ui/handlers/login-or-register-ui-handler.ts @@ -0,0 +1,72 @@ +import { globalScene } from "#app/global-scene"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { LoginRegisterInfoContainerUiHandler } from "#ui/login-register-info-container-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; +import i18next from "i18next"; +import type Phaser from "phaser"; + +export class LoginOrRegisterUiHandler extends LoginRegisterInfoContainerUiHandler { + private logo: Phaser.GameObjects.Image; + + public override getModalTitle(): string { + return ""; + } + + public override getWidth(): number { + const buttonWidth = this.buttonLabels.reduce((sum, label) => sum + label.width, 0) / 6; + return buttonWidth + 50; + } + + public override getHeight(): number { + return 32; + } + + public override getMargin(): [number, number, number, number] { + return [0, 0, 30, 0]; + } + + public override getButtonLabels(): string[] { + return [i18next.t("menu:login"), i18next.t("menu:register")]; + } + + // TODO: use mixins so it's not necessary to inherit from `FormModalUiHandler` + public override getInputFieldConfigs(): InputFieldConfig[] { + return []; + } + + public override setup(): void { + super.setup(); + + // logo width is 150 + this.logo = globalScene.add // + .image(-((150 - this.getWidth()) / 2), -52, "logo") + .setOrigin(0); + + this.modalContainer.add(this.logo); + } + + public override show(args: [ModalConfig, ...any[]]): boolean { + this.logo // + .setVisible(true) + .setActive(true); + + const config = args[0]; + this.showInfoContainer(config); + + return super.show(args); + } + + public override clear(): void { + super.clear(); + + this.logo // + .setVisible(false) + .setActive(false); + } + + public override destroy(): void { + super.destroy(); + + this.logo.destroy(); + } +} diff --git a/src/ui/handlers/login-register-info-container-ui-handler.ts b/src/ui/handlers/login-register-info-container-ui-handler.ts new file mode 100644 index 00000000000..556ab94e15f --- /dev/null +++ b/src/ui/handlers/login-register-info-container-ui-handler.ts @@ -0,0 +1,233 @@ +import { globalScene } from "#app/global-scene"; +import { UiMode } from "#enums/ui-mode"; +import { languageOptions } from "#system/settings-language"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; +import { fixedInt } from "#utils/common"; +import i18next from "i18next"; +import JSZip from "jszip"; + +interface BuildInteractableImageOpts { + scale?: number; + x?: number; + y?: number; + origin?: { x: number; y: number }; +} + +/** + * The maximum number of saves that are allowed to show up in the username panel pefore + * the `P02: Too many saves` popup is displayed. + * + * @privateRemarks + * This limitation is in place to allow for the password reset helpers to get + * enough information in one screenshot. If the user has too many saves, this + * complicates the interaction as it would require scrolling, which will + * make tickets take longer to resolve. + */ +const MAX_SAVES_FOR_USERNAME_PANEL = 7; + +const ERR_NO_SAVES: string = "No save files found"; +const ERR_TOO_MANY_SAVES: string = "Too many save files found"; + +// TODO: use mixins +export abstract class LoginRegisterInfoContainerUiHandler extends FormModalUiHandler { + private usernameInfoImage: Phaser.GameObjects.Image; + private saveDownloadImage: Phaser.GameObjects.Image; + private changeLanguageImage: Phaser.GameObjects.Image; + private infoContainer: Phaser.GameObjects.Container; + + public override getReadableErrorMessage(error: string): string { + if (!error) { + return ""; + } + + const colonIndex = error.indexOf(":"); + if (colonIndex > 0) { + error = error.slice(0, colonIndex); + } + + switch (error) { + case ERR_NO_SAVES: + return "P01: " + i18next.t("menu:noSaves"); + case ERR_TOO_MANY_SAVES: + return "P02: " + i18next.t("menu:tooManySaves"); + } + + return super.getReadableErrorMessage(error); + } + + public override setup(): void { + super.setup(); + this.buildInfoContainer(); + } + + public override clear(): void { + super.clear(); + this.infoContainer // + .setVisible(false) + .setActive(false); + this.setMouseCursorStyle("default"); // reset cursor + } + + public override destroy(): void { + super.destroy(); + this.infoContainer.destroy(); + } + + private buildInfoContainer() { + this.usernameInfoImage = this.buildInteractableImage("settings_icon", "username-info-icon", { x: 0, scale: 0.5 }); + this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", { x: 20, scale: 0.75 }); + this.changeLanguageImage = this.buildInteractableImage("language_icon", "change-language-icon", { + x: 40, + scale: 0.5, + }); + + this.infoContainer = globalScene.add + .container(0, 0) + .add([this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage]) + .setVisible(false) + .disableInteractive(); + + this.getUi().add(this.infoContainer); + } + + protected showInfoContainer(config: ModalConfig) { + this.infoContainer // + .setPosition(5, -76) + .setVisible(true); + this.getUi().moveTo(this.infoContainer, this.getUi().length - 1); + + this.usernameInfoImage // + .setPositionRelative(this.infoContainer, 0, 0) + .on("pointerdown", () => this.showUsernames(config)); + + this.saveDownloadImage // + .setPositionRelative(this.infoContainer, 20, 0) + .on("pointerdown", () => this.downloadSaves(config)); + + this.changeLanguageImage // + .setPositionRelative(this.infoContainer, 40, 0) + .on("pointerdown", () => { + globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { + options: languageOptions, + maxOptions: 7, + delay: 1000, + }); + }); + + this.infoContainer.setAlpha(0); + globalScene.tweens.add({ + targets: this.infoContainer, + duration: fixedInt(1000), + ease: "Sine.easeInOut", + y: "-=24", + alpha: 1, + }); + } + + /** + * Show a panel with all usernames found in localStorage + * @remarks + * Up to {@linkcode MAX_SAVES_FOR_USERNAME_PANEL} usernames are shown, otherwise P02 is triggered + * @param config - The modal configuration + */ + private showUsernames(config: ModalConfig): void { + if (globalScene.tweens.getTweensOf(this.infoContainer).length > 0) { + return; + } + + const localStorageKeys = Object.keys(localStorage); + const keyToFind = "data_"; + const dataKeys = localStorageKeys.filter(ls => ls.includes(keyToFind)); + + if (dataKeys.length === 0) { + this.onFail(ERR_NO_SAVES, config); + return; + } + if (dataKeys.length > MAX_SAVES_FOR_USERNAME_PANEL) { + this.onFail(ERR_TOO_MANY_SAVES, config); + return; + } + + const options: OptionSelectItem[] = []; + const handler = () => { + globalScene.ui.revertMode(); + this.infoContainer.disableInteractive(); + return true; + }; + + for (const key of dataKeys) { + options.push({ label: key.replace(keyToFind, ""), handler }); + } + + globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { options, delay: 1000 }); + this.infoContainer.setInteractive( + new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width, globalScene.game.canvas.height), + Phaser.Geom.Rectangle.Contains, + ); + } + + /** + * Collect the user's save files from localStorage and download them as a zip file + * @remarks + * Used as the `pointerDown` callback for the save download image + * @param config - The modal configuration + */ + private async downloadSaves(config: ModalConfig): Promise { + // find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip + const localStorageKeys = Object.keys(localStorage); + const keyToFind = "data_"; + const sessionKeyToFind = "sessionData"; + const dataKeys = localStorageKeys.filter(ls => ls.includes(keyToFind)); + const sessionKeys = localStorageKeys.filter(ls => ls.includes(sessionKeyToFind)); + + if (dataKeys.length <= 0 && sessionKeys.length <= 0) { + this.onFail(ERR_NO_SAVES, config); + return; + } + + const zip = new JSZip(); + // Bang is safe here because of the filter above + for (const dataKey of dataKeys) { + zip.file(dataKey + ".prsv", localStorage.getItem(dataKey)!); + } + for (const sessionKey of sessionKeys) { + zip.file(sessionKey + ".prsv", localStorage.getItem(sessionKey)!); + } + + const content = await zip.generateAsync({ type: "blob" }); + const url = URL.createObjectURL(content); + const a = document.createElement("a"); + + a.href = url; + a.download = "pokerogue_saves.zip"; + a.click(); + + URL.revokeObjectURL(url); + } + + private onFail(error: string, config: ModalConfig): void { + const ui = globalScene.ui; + ui.setMode(UiMode.LOADING, { buttonActions: [] }); + ui.setModeForceTransition(UiMode.LOGIN_OR_REGISTER, Object.assign(config, { errorMessage: error?.trim() })); + ui.playError(); + } + + protected buildInteractableImage( + texture: string, + name: string, + opts: BuildInteractableImageOpts = {}, + ): Phaser.GameObjects.Image { + const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts; + const img = globalScene.add + .image(x, y, texture) + .setName(name) + .setOrigin(origin.x, origin.y) + .setScale(scale) + .setInteractive(); + this.addInteractionHoverEffect(img); + + return img; + } +} diff --git a/src/ui/handlers/oauth-providers-ui-handler.ts b/src/ui/handlers/oauth-providers-ui-handler.ts new file mode 100644 index 00000000000..8839f6880a1 --- /dev/null +++ b/src/ui/handlers/oauth-providers-ui-handler.ts @@ -0,0 +1,107 @@ +import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; +import { LoginRegisterInfoContainerUiHandler } from "#ui/login-register-info-container-ui-handler"; +import { addTextObject } from "#ui/text"; +import { addWindow } from "#ui/ui-theme"; +import { fixedInt } from "#utils/common"; +import i18next from "i18next"; + +// TODO: use mixins +export abstract class OAuthProvidersUiHandler extends LoginRegisterInfoContainerUiHandler { + private discordImage: Phaser.GameObjects.Image; + private googleImage: Phaser.GameObjects.Image; + + private externalPartyContainer: Phaser.GameObjects.Container; + private externalPartyBg: Phaser.GameObjects.NineSlice; + private externalPartyTitle: Phaser.GameObjects.Text; + + public override setup(): void { + super.setup(); + this.buildExternalPartyContainer(); + } + + public override clear(): void { + super.clear(); + + this.externalPartyContainer // + .setVisible(false) + .setActive(false); + + [this.discordImage, this.googleImage].forEach(img => { + img.off("pointerdown"); + }); + } + + public override destroy(): void { + super.destroy(); + this.externalPartyContainer.destroy(); + } + + private buildExternalPartyContainer(): void { + const { height, width } = globalScene.scaledCanvas; + + this.externalPartyContainer = globalScene.add + .container(0, 0) + .setInteractive(new Phaser.Geom.Rectangle(0, 0, width / 2, height / 2), Phaser.Geom.Rectangle.Contains); + this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL) // + .setOrigin(0.5, 0); + this.externalPartyBg = addWindow(0, 0, 0, 0); + + this.discordImage = this.buildInteractableImage("discord", "discord-icon"); + this.googleImage = this.buildInteractableImage("google", "google-icon"); + + this.externalPartyContainer + .add([this.externalPartyBg, this.externalPartyTitle, this.discordImage, this.googleImage]) + .setVisible(false); + this.getUi().add(this.externalPartyContainer); + } + + protected processExternalProvider(): void { + const titleX = 22; + this.externalPartyTitle + .setText(i18next.t("menu:orUse")) + .setX(titleX + this.externalPartyTitle.text.length) + .setVisible(true); + + this.externalPartyContainer // + .setPositionRelative(this.modalContainer, 175, 0) + .setVisible(true); + + const bgWidth = this.externalPartyTitle.text.length + 50; + this.externalPartyBg.setSize(bgWidth, this.modalBg.height); + this.getUi().moveTo(this.externalPartyContainer, this.getUi().length - 1); + + const externalPartyIconWidth = this.externalPartyBg.width / 3.1; + + const getRedirectUri = (service: string): string => { + return encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/${service}/callback`); + }; + + this.discordImage // + .setPosition(externalPartyIconWidth, this.externalPartyBg.height - 40) + .on("pointerdown", () => { + const redirectUri = getRedirectUri("discord"); + const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID; + const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`; + window.open(discordUrl, "_self"); + }); + + this.googleImage // + .setPosition(externalPartyIconWidth, this.externalPartyBg.height - 60) + .on("pointerdown", () => { + const redirectUri = getRedirectUri("google"); + const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; + const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; + window.open(googleUrl, "_self"); + }); + + this.externalPartyContainer.setAlpha(0); + globalScene.tweens.add({ + targets: this.externalPartyContainer, + duration: fixedInt(1000), + ease: "Sine.easeInOut", + y: "-=24", + alpha: 1, + }); + } +} diff --git a/src/ui/handlers/registration-form-ui-handler.ts b/src/ui/handlers/registration-form-ui-handler.ts index e5220351497..250a243e494 100644 --- a/src/ui/handlers/registration-form-ui-handler.ts +++ b/src/ui/handlers/registration-form-ui-handler.ts @@ -3,33 +3,33 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; import i18next from "i18next"; +import { LoginRegisterInfoContainerUiHandler } from "./login-register-info-container-ui-handler"; -export class RegistrationFormUiHandler extends FormModalUiHandler { - getModalTitle(_config?: ModalConfig): string { +export class RegistrationFormUiHandler extends LoginRegisterInfoContainerUiHandler { + public override getModalTitle(): string { return i18next.t("menu:register"); } - getWidth(_config?: ModalConfig): number { + public override getWidth(): number { return 160; } - getMargin(_config?: ModalConfig): [number, number, number, number] { - return [0, 0, 48, 0]; + public override getMargin(): [number, number, number, number] { + return [0, 20, 48, 0]; } - getButtonTopMargin(): number { + public override getButtonTopMargin(): number { return 12; } - getButtonLabels(_config?: ModalConfig): string[] { - return [i18next.t("menu:register"), i18next.t("menu:backToLogin")]; + public override getButtonLabels(): string[] { + return [i18next.t("menu:register"), i18next.t("menu:goBack")]; } - getReadableErrorMessage(error: string): string { + public override getReadableErrorMessage(error: string): string { const colonIndex = error?.indexOf(":"); if (colonIndex > 0) { error = error.slice(0, colonIndex); @@ -46,7 +46,7 @@ export class RegistrationFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } - override getInputFieldConfigs(): InputFieldConfig[] { + public override getInputFieldConfigs(): InputFieldConfig[] { const inputFieldConfigs: InputFieldConfig[] = []; inputFieldConfigs.push({ label: i18next.t("menu:username") }); inputFieldConfigs.push({ @@ -60,7 +60,7 @@ export class RegistrationFormUiHandler extends FormModalUiHandler { return inputFieldConfigs; } - setup(): void { + public override setup(): void { super.setup(); const label = addTextObject(10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, { @@ -71,60 +71,61 @@ export class RegistrationFormUiHandler extends FormModalUiHandler { this.modalContainer.add(label); } - show(args: any[]): boolean { - if (super.show(args)) { - const config = args[0] as ModalConfig; - - const originalRegistrationAction = this.submitAction; - this.submitAction = () => { - if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { - // Prevent overlapping overrides on action modification - this.submitAction = originalRegistrationAction; - this.sanitizeInputs(); - globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); - const onFail = error => { - globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() })); - globalScene.ui.playError(); - }; - if (!this.inputs[0].text) { - return onFail(i18next.t("menu:emptyUsername")); - } - if (!this.inputs[1].text) { - return onFail(this.getReadableErrorMessage("invalid password")); - } - if (this.inputs[1].text !== this.inputs[2].text) { - return onFail(i18next.t("menu:passwordNotMatchingConfirmPassword")); - } - const [usernameInput, passwordInput] = this.inputs; - pokerogueApi.account - .register({ - username: usernameInput.text, - password: passwordInput.text, - }) - .then(registerError => { - if (!registerError) { - pokerogueApi.account - .login({ - username: usernameInput.text, - password: passwordInput.text, - }) - .then(loginError => { - if (!loginError) { - originalRegistrationAction?.(); - } else { - onFail(loginError); - } - }); - } else { - onFail(registerError); - } - }); - } - }; - - return true; + public override show(args: [ModalConfig, ...any[]]): boolean { + if (!super.show(args)) { + return false; } - return false; + const config = args[0]; + this.showInfoContainer(config); + + const originalRegistrationAction = this.submitAction; + this.submitAction = () => { + if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { + // Prevent overlapping overrides on action modification + this.submitAction = originalRegistrationAction; + this.sanitizeInputs(); + globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); + const onFail = error => { + globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() })); + globalScene.ui.playError(); + }; + if (!this.inputs[0].text) { + return onFail(i18next.t("menu:emptyUsername")); + } + if (!this.inputs[1].text) { + return onFail(this.getReadableErrorMessage("invalid password")); + } + if (this.inputs[1].text !== this.inputs[2].text) { + return onFail(i18next.t("menu:passwordNotMatchingConfirmPassword")); + } + const [usernameInput, passwordInput] = this.inputs; + pokerogueApi.account + .register({ + username: usernameInput.text, + password: passwordInput.text, + }) + .then(registerError => { + if (registerError) { + onFail(registerError); + } else { + pokerogueApi.account + .login({ + username: usernameInput.text, + password: passwordInput.text, + }) + .then(loginError => { + if (loginError) { + onFail(loginError); + } else { + originalRegistrationAction?.(); + } + }); + } + }); + } + }; + + return true; } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 29a3acd983e..4ba03d564e8 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -26,6 +26,7 @@ import { GamepadBindingUiHandler } from "#ui/gamepad-binding-ui-handler"; import { KeyboardBindingUiHandler } from "#ui/keyboard-binding-ui-handler"; import { LoadingModalUiHandler } from "#ui/loading-modal-ui-handler"; import { LoginFormUiHandler } from "#ui/login-form-ui-handler"; +import { LoginOrRegisterUiHandler } from "#ui/login-or-register-ui-handler"; import { MenuUiHandler } from "#ui/menu-ui-handler"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; @@ -163,6 +164,7 @@ export class UI extends Phaser.GameObjects.Container { new PokedexUiHandler(), new PokedexScanUiHandler(UiMode.TEST_DIALOGUE), new PokedexPageUiHandler(), + new LoginOrRegisterUiHandler(), new LoginFormUiHandler(), new RegistrationFormUiHandler(), new LoadingModalUiHandler(),