allow up to 7 usernames in panel before P02

This commit is contained in:
Sirz Benjie 2025-09-18 12:37:32 -05:00
parent 207808f37d
commit 9937cca8f8
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E

View File

@ -20,6 +20,18 @@ interface BuildInteractableImageOpts {
origin?: { 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;
export class LoginFormUiHandler extends FormModalUiHandler { export class LoginFormUiHandler extends FormModalUiHandler {
private readonly ERR_USERNAME: string = "invalid username"; private readonly ERR_USERNAME: string = "invalid username";
private readonly ERR_PASSWORD: string = "invalid password"; private readonly ERR_PASSWORD: string = "invalid password";
@ -54,21 +66,16 @@ export class LoginFormUiHandler extends FormModalUiHandler {
new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width / 2, globalScene.scaledCanvas.height / 2), new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width / 2, globalScene.scaledCanvas.height / 2),
Phaser.Geom.Rectangle.Contains, Phaser.Geom.Rectangle.Contains,
); );
this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL); this.externalPartyTitle = addTextObject(0, 4, "", TextStyle.SETTINGS_LABEL).setOrigin(0.5, 0);
this.externalPartyTitle.setOrigin(0.5, 0);
this.externalPartyBg = addWindow(0, 0, 0, 0); this.externalPartyBg = addWindow(0, 0, 0, 0);
this.externalPartyContainer.add(this.externalPartyBg);
this.externalPartyContainer.add(this.externalPartyTitle);
this.googleImage = this.buildInteractableImage("google", "google-icon"); this.googleImage = this.buildInteractableImage("google", "google-icon");
this.discordImage = this.buildInteractableImage("discord", "discord-icon"); this.discordImage = this.buildInteractableImage("discord", "discord-icon");
this.externalPartyContainer.add(this.googleImage); this.externalPartyContainer
this.externalPartyContainer.add(this.discordImage); .add([this.externalPartyBg, this.externalPartyTitle, this.googleImage, this.discordImage])
.setVisible(false);
this.getUi().add(this.externalPartyContainer); this.getUi().add(this.externalPartyContainer);
this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage);
this.externalPartyContainer.setVisible(false);
} }
private buildInfoContainer() { private buildInfoContainer() {
@ -89,12 +96,11 @@ export class LoginFormUiHandler extends FormModalUiHandler {
scale: 0.5, scale: 0.5,
}); });
this.infoContainer.add(this.usernameInfoImage); this.infoContainer
this.infoContainer.add(this.saveDownloadImage); .add([this.usernameInfoImage, this.saveDownloadImage, this.changeLanguageImage])
this.infoContainer.add(this.changeLanguageImage); .setVisible(false)
.disableInteractive();
this.getUi().add(this.infoContainer); this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false);
this.infoContainer.disableInteractive();
} }
override getModalTitle(_config?: ModalConfig): string { override getModalTitle(_config?: ModalConfig): string {
@ -142,60 +148,61 @@ export class LoginFormUiHandler extends FormModalUiHandler {
override getInputFieldConfigs(): InputFieldConfig[] { override getInputFieldConfigs(): InputFieldConfig[] {
const inputFieldConfigs: InputFieldConfig[] = []; const inputFieldConfigs: InputFieldConfig[] = [];
inputFieldConfigs.push({ label: i18next.t("menu:username") }); inputFieldConfigs.push(
inputFieldConfigs.push({ { label: i18next.t("menu:username") },
label: i18next.t("menu:password"), {
isPassword: true, label: i18next.t("menu:password"),
}); isPassword: true,
},
);
return inputFieldConfigs; return inputFieldConfigs;
} }
override show(args: any[]): boolean { override show(args: any[]): boolean {
if (super.show(args)) { if (!super.show(args)) {
const config = args[0] as ModalConfig; return false;
this.processExternalProvider(config);
const originalLoginAction = this.submitAction;
this.submitAction = _ => {
if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
// Prevent overlapping overrides on action modification
this.submitAction = originalLoginAction;
this.sanitizeInputs();
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
const onFail = error => {
globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
globalScene.ui.playError();
};
if (!this.inputs[0].text) {
return onFail(i18next.t("menu:emptyUsername"));
}
const [usernameInput, passwordInput] = this.inputs;
pokerogueApi.account
.login({
username: usernameInput.text,
password: passwordInput.text,
})
.then(error => {
if (!error && originalLoginAction) {
originalLoginAction();
} else {
onFail(error);
}
});
}
};
return true;
} }
const config = args[0] as ModalConfig;
this.processExternalProvider(config);
const originalLoginAction = this.submitAction;
this.submitAction = _ => {
if (globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
// Prevent overlapping overrides on action modification
this.submitAction = originalLoginAction;
this.sanitizeInputs();
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
const onFail = error => {
globalScene.ui.setMode(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() }));
globalScene.ui.playError();
};
if (!this.inputs[0].text) {
return onFail(i18next.t("menu:emptyUsername"));
}
return false; const [usernameInput, passwordInput] = this.inputs;
pokerogueApi.account
.login({
username: usernameInput.text,
password: passwordInput.text,
})
.then(error => {
if (!error && originalLoginAction) {
originalLoginAction();
} else {
onFail(error);
}
});
}
};
return true;
} }
override clear() { override clear() {
super.clear(); super.clear();
this.externalPartyContainer.setVisible(false); this.externalPartyContainer.setVisible(false).setActive(false);
this.infoContainer.setVisible(false); this.infoContainer.setVisible(false).setActive(false);
this.setMouseCursorStyle("default"); //reset cursor this.setMouseCursorStyle("default"); //reset cursor
[ [
@ -204,109 +211,150 @@ export class LoginFormUiHandler extends FormModalUiHandler {
this.usernameInfoImage, this.usernameInfoImage,
this.saveDownloadImage, this.saveDownloadImage,
this.changeLanguageImage, this.changeLanguageImage,
].forEach(img => img.off("pointerdown")); ].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<void> {
// 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 { private processExternalProvider(config: ModalConfig): void {
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? ""); this.externalPartyTitle
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length); .setText(i18next.t("menu:orUse") ?? "")
this.externalPartyTitle.setVisible(true); .setX(20 + this.externalPartyTitle.text.length)
this.externalPartyContainer.setPositionRelative(this.modalContainer, 175, 0); .setVisible(true);
this.externalPartyContainer.setVisible(true);
this.externalPartyBg.setSize(this.externalPartyTitle.text.length + 50, this.modalBg.height);
this.getUi().moveTo(this.externalPartyContainer, this.getUi().length - 1);
this.googleImage.setPosition(this.externalPartyBg.width / 3.1, this.externalPartyBg.height - 60);
this.discordImage.setPosition(this.externalPartyBg.width / 3.1, this.externalPartyBg.height - 40);
this.infoContainer.setPosition(5, -76); const externalPartyContainer = this.externalPartyContainer
this.infoContainer.setVisible(true); .setPositionRelative(this.modalContainer, 175, 0)
this.getUi().moveTo(this.infoContainer, this.getUi().length - 1); .setVisible(true);
this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0);
this.saveDownloadImage.setPositionRelative(this.infoContainer, 20, 0);
this.changeLanguageImage.setPositionRelative(this.infoContainer, 40, 0);
this.discordImage.on("pointerdown", () => { const externalPartyBg = this.externalPartyBg.setSize(this.externalPartyTitle.text.length + 50, this.modalBg.height);
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); this.getUi().moveTo(externalPartyContainer, this.getUi().length - 1);
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.on("pointerdown", () => { const externalPartyIconWidth = externalPartyBg.width / 3.1;
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`); this.discordImage;
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID; const infoContainer = this.infoContainer.setPosition(5, -76).setVisible(true);
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`; this.getUi().moveTo(infoContainer, this.getUi().length - 1);
window.open(googleUrl, "_self");
});
const onFail = error => { this.discordImage // formatting
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); .setPosition(externalPartyIconWidth, externalPartyBg.height - 40)
globalScene.ui.setModeForceTransition(UiMode.LOGIN_FORM, Object.assign(config, { errorMessage: error?.trim() })); .on("pointerdown", () => {
globalScene.ui.playError(); 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.usernameInfoImage.on("pointerdown", () => { this.googleImage // formatting
if (globalScene.tweens.getTweensOf(this.infoContainer).length === 0) { .setPosition(externalPartyIconWidth, externalPartyBg.height - 60)
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage .on("pointerdown", () => {
const keyToFind = "data_"; const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0); const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
if (dataKeys.length > 0 && dataKeys.length <= 2) { const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
const options: OptionSelectItem[] = []; window.open(googleUrl, "_self");
for (const key of dataKeys) { });
options.push({
label: key.replace(keyToFind, ""),
handler: () => {
globalScene.ui.revertMode();
this.infoContainer.disableInteractive();
return true;
},
});
}
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,
);
} else {
if (dataKeys.length > 2) {
return onFail(this.ERR_TOO_MANY_SAVES);
}
return onFail(this.ERR_NO_SAVES);
}
}
});
this.saveDownloadImage.on("pointerdown", async () => { this.usernameInfoImage // formatting
// find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip .setPositionRelative(infoContainer, 0, 0)
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage .on("pointerdown", () => this.showUsernames(config));
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) {
const zip = new JSZip();
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);
} else {
return onFail(this.ERR_NO_SAVES);
}
});
this.changeLanguageImage.on("pointerdown", () => { 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, { globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, {
options: languageOptions, options: languageOptions,
maxOptions: 7, maxOptions: 7,
@ -335,11 +383,12 @@ export class LoginFormUiHandler extends FormModalUiHandler {
private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) { private buildInteractableImage(texture: string, name: string, opts: BuildInteractableImageOpts = {}) {
const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts; const { scale = 0.07, x = 0, y = 0, origin = { x: 0, y: 0 } } = opts;
const img = globalScene.add.image(x, y, texture); const img = globalScene.add
img.setName(name); .image(x, y, texture)
img.setOrigin(origin.x, origin.y); .setName(name)
img.setScale(scale); .setOrigin(origin.x, origin.y)
img.setInteractive(); .setScale(scale)
.setInteractive();
this.addInteractionHoverEffect(img); this.addInteractionHoverEffect(img);
return img; return img;