feat: Add Google and Discord login functionality

feat: Add link to Discord in menu UI

feat: Add Discord and Google login functionality

Add container around discord and google icons

refactor: Update environment variable names for Discord and Google client IDs

feat: Add "Or use" translation for login options in multiple languages

feat: Update menu UI translations for multiple languages

Code review fixes

refactor: Update Discord and Google client IDs in environment variables
This commit is contained in:
Frederico Santos 2024-06-01 17:23:01 +01:00
parent c71e5372d4
commit ee5d689575
30 changed files with 205 additions and 7 deletions

4
.env
View File

@ -1,3 +1,5 @@
VITE_BYPASS_LOGIN=0
VITE_BYPASS_TUTORIAL=0
VITE_SERVER_URL=http://localhost:8001
VITE_SERVER_URL=http://localhost:8001
VITE_DISCORD_CLIENT_ID=1248062921129459756
VITE_GOOGLE_CLIENT_ID=955345393540-2k6lfftf0fdnb0krqmpthjnqavfvvf73.apps.googleusercontent.com

View File

@ -1,3 +1,5 @@
VITE_BYPASS_LOGIN=1
VITE_BYPASS_TUTORIAL=0
VITE_SERVER_URL=http://localhost:8001
VITE_SERVER_URL=http://localhost:8001
VITE_DISCORD_CLIENT_ID=1234567890
VITE_GOOGLE_CLIENT_ID=1234567890

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/images/ui/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

View File

@ -4,6 +4,8 @@ import * as Utils from "./utils";
export interface UserInfo {
username: string;
lastSessionSlot: integer;
discordId: string;
googleId: string;
}
export let loggedInUser: UserInfo = null;
@ -17,7 +19,7 @@ export function initLoggedInUser(): void {
export function updateUserInfo(): Promise<[boolean, integer]> {
return new Promise<[boolean, integer]>(resolve => {
if (bypassLogin) {
loggedInUser = { username: "Guest", lastSessionSlot: -1 };
loggedInUser = { username: "Guest", lastSessionSlot: -1, discordId: "", googleId: "" };
let lastSessionSlot = -1;
for (let s = 0; s < 5; s++) {
if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) {

View File

@ -153,6 +153,8 @@ export class LoadingScene extends SceneBase {
this.loadImage("select_gen_cursor_highlight", "ui");
this.loadImage("saving_icon", "ui");
this.loadImage("discord", "ui");
this.loadImage("google", "ui");
this.loadImage("default_bg", "arenas");
// Load arena images

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Wähle einen Slot zum Exportieren.",
"importData": "Daten importieren",
"exportData": "Daten exportieren",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "Abbrechen",
"losingProgressionWarning": "Du wirst jeglichen Fortschritt seit Anfang dieses Kampfes verlieren. Fortfahren?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Benutzername",
"password": "Passwort",
"login": "Anmelden",
"orUse": "Or use",
"register": "Registrieren",
"emptyUsername": "Benutzername darf nicht leer sein.",
"invalidLoginUsername": "Der eingegebene Benutzername ist ungültig.",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Select a slot to export from.",
"importData": "Import Data",
"exportData": "Export Data",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "Cancel",
"losingProgressionWarning": "You will lose any progress since the beginning of the battle. Proceed?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Username",
"password": "Password",
"login": "Login",
"orUse": "Or use",
"register": "Register",
"emptyUsername": "Username must not be empty",
"invalidLoginUsername": "The provided username is invalid",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Selecciona una ranura para exportar.",
"importData": "Importar Datos",
"exportData": "Exportar Datos",
"linkDiscord": "Conectar Discord",
"unlinkDiscord": "Desconectar Discord",
"linkGoogle": "Conectar Google",
"unlinkGoogle": "Desconectar Google",
"cancel": "Cancelar",
"losingProgressionWarning": "Perderás cualquier progreso desde el inicio de la batalla. ¿Continuar?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Usuario",
"password": "Contraseña",
"login": "Iniciar Sesión",
"orUse": "O usa",
"register": "Registrarse",
"emptyUsername": "El usuario no puede estar vacío",
"invalidLoginUsername": "El usuario no es válido",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Sélectionnez lemplacement depuis lequel exporter les données.",
"importData": "Importer données",
"exportData": "Exporter données",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "Retour",
"losingProgressionWarning": "Vous allez perdre votre progression depuis le début du combat. Continuer ?"
} as const;

View File

@ -12,6 +12,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Nom dutilisateur",
"password": "Mot de passe",
"login": "Connexion",
"orUse": "Ou utilisez",
"register": "Sinscrire",
"emptyUsername": "Le nom dutilisateur est manquant",
"invalidLoginUsername": "Le nom dutilisateur nest pas valide",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Seleziona uno slot da cui esportare.",
"importData": "Importa Dati",
"exportData": "Esporta Dati",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "Annulla",
"losingProgressionWarning": "Perderai tutti i progressi dall'inizio della battaglia. Procedere?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Nome utente",
"password": "Password",
"login": "Accedi",
"orUse": "Or use",
"register": "Registrati",
"emptyUsername": "Nome utente mancante!",
"invalidLoginUsername": "Nome utente non valido!",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "내보낼 슬롯을 골라주세요.",
"importData": "데이터 불러오기",
"exportData": "데이터 내보내기",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "취소",
"losingProgressionWarning": "전투 시작으로부터의 진행 상황을 잃게 됩니다. 계속하시겠습니까?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "이름",
"password": "비밀번호",
"login": "로그인",
"orUse": "Or use",
"register": "등록",
"emptyUsername": "이름은 비워둘 수 없습니다",
"invalidLoginUsername": "사용할 수 없는 이름입니다",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "Selecione um slot para exportar.",
"importData": "Importar dados",
"exportData": "Exportar dados",
"linkDiscord": "Conectar Discord",
"unlinkDiscord": "Desconectar Discord",
"linkGoogle": "Conectar Google",
"unlinkGoogle": "Desconectar Google",
"cancel": "Cancelar",
"losingProgressionWarning": "Você vai perder todo o progresso desde o início da batalha. Confirmar?"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "Nome de Usuário",
"password": "Senha",
"login": "Iniciar sessão",
"orUse": "Ou use",
"register": "Registrar-se",
"emptyUsername": "Nome de usuário vazio",
"invalidLoginUsername": "Nome de usuário inválido",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "选择要导出的存档位。",
"importData": "导入数据",
"exportData": "导出数据",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "取消",
"losingProgressionWarning": "你将失去自战斗开始以来的所有进度。是否\n继续"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "用户名",
"password": "密码",
"login": "登录",
"Or use": "Or use",
"register": "注册",
"emptyUsername": "用户名不能为空",
"invalidLoginUsername": "输入的用户名无效",

View File

@ -18,6 +18,10 @@ export const menuUiHandler: SimpleTranslationEntries = {
"exportSlotSelect": "選擇要導出的存檔位。",
"importData": "導入數據",
"exportData": "導出數據",
"linkDiscord": "Link Discord",
"unlinkDiscord": "Unlink Discord",
"linkGoogle": "Link Google",
"unlinkGoogle": "Unlink Google",
"cancel": "取消",
"losingProgressionWarning": "你將失去自戰鬥開始以來的所有進度。是否\n繼續"
} as const;

View File

@ -17,6 +17,7 @@ export const menu: SimpleTranslationEntries = {
"username": "用戶名",
"password": "密碼",
"login": "登入",
"orUse": "Or use",
"register": "注冊",
"emptyUsername": "用戶名不能為空",
"invalidLoginUsername": "提供的用戶名無效",

View File

@ -130,6 +130,16 @@ export class LoginPhase extends Phase {
}
]
});
}, () => {
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/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`;
window.open(discordUrl, "_self");
}, () => {
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/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");
}
]
});

View File

@ -3,8 +3,65 @@ import { ModalConfig } from "./modal-ui-handler";
import * as Utils from "../utils";
import { Mode } from "./ui";
import i18next from "i18next";
import BattleScene from "#app/battle-scene.js";
import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme";
export default class LoginFormUiHandler extends FormModalUiHandler {
private googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image;
private externalPartyContainer: Phaser.GameObjects.Container;
private externalPartyBg: Phaser.GameObjects.NineSlice;
private externalPartyTitle: Phaser.GameObjects.Text;
constructor(scene: BattleScene, mode?: Mode) {
super(scene, mode);
}
setup(): void {
super.setup();
this.externalPartyContainer = this.scene.add.container(0, 0);
this.externalPartyContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 12, this.scene.game.canvas.height / 12), Phaser.Geom.Rectangle.Contains);
this.externalPartyTitle = addTextObject(this.scene, 0, 4, "", TextStyle.SETTINGS_LABEL);
this.externalPartyTitle.setOrigin(0.5, 0);
this.externalPartyBg = addWindow(this.scene, 0, 0, 0, 0);
this.externalPartyContainer.add(this.externalPartyBg);
this.externalPartyContainer.add(this.externalPartyTitle);
const googleImage = this.scene.add.image(0, 0, "google");
googleImage.setOrigin(0, 0);
googleImage.setScale(0.07);
googleImage.setInteractive();
googleImage.setName("google-icon");
googleImage.on("pointerdown", () => {
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/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.googleImage = googleImage;
const discordImage = this.scene.add.image(20, 0, "discord");
discordImage.setOrigin(0, 0);
discordImage.setScale(0.07);
discordImage.setInteractive();
discordImage.setName("discord-icon");
discordImage.on("pointerdown", () => {
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/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`;
window.open(discordUrl, "_self");
});
this.discordImage = discordImage;
this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage);
this.getUi().add(this.externalPartyContainer);
this.externalPartyContainer.add(this.googleImage);
this.externalPartyContainer.add(this.discordImage);
this.externalPartyContainer.setVisible(false);
}
getModalTitle(config?: ModalConfig): string {
return i18next.t("menu:login");
}
@ -22,7 +79,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
}
getButtonLabels(config?: ModalConfig): string[] {
return [ i18next.t("menu:login"), i18next.t("menu:register") ];
return [ i18next.t("menu:login"), i18next.t("menu:register")];
}
getReadableErrorMessage(error: string): string {
@ -46,8 +103,10 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
show(args: any[]): boolean {
if (super.show(args)) {
const config = args[0] as ModalConfig;
this.processExternalProvider();
const config = args[0] as ModalConfig;
const originalLoginAction = this.submitAction;
this.submitAction = (_) => {
// Prevent overlapping overrides on action modification
@ -83,4 +142,33 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
return false;
}
clear() {
super.clear();
this.externalPartyContainer.setVisible(false);
this.discordImage.off("pointerdown");
this.googleImage.off("pointerdown");
}
processExternalProvider() : void {
this.externalPartyTitle.setText(i18next.t("menu:orUse"));
this.externalPartyTitle.setX(20+this.externalPartyTitle.text.length);
this.externalPartyTitle.setVisible(true);
this.externalPartyContainer.setPositionRelative(this.modalContainer, 175, 0);
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.externalPartyContainer.setAlpha(0);
this.scene.tweens.add({
targets: this.externalPartyContainer,
duration: Utils.fixedInt(1000),
ease: "Sine.easeInOut",
y: "-=24",
alpha: 1
});
}
}

View File

@ -6,7 +6,7 @@ import { addWindow } from "./ui-theme";
import MessageUiHandler from "./message-ui-handler";
import { OptionSelectConfig, OptionSelectItem } from "./abstact-option-select-ui-handler";
import { Tutorial, handleTutorial } from "../tutorial";
import { updateUserInfo } from "../account";
import { loggedInUser, updateUserInfo } from "../account";
import i18next from "i18next";
import {Button} from "#enums/buttons";
import { GameDataType } from "#enums/game-data-type";
@ -21,7 +21,7 @@ export enum MenuOptions {
MANAGE_DATA,
COMMUNITY,
SAVE_AND_QUIT,
LOG_OUT
LOG_OUT,
}
const wikiUrl = "https://wiki.pokerogue.net/start";
@ -303,6 +303,51 @@ export default class MenuUiHandler extends MessageUiHandler {
success = true;
break;
case MenuOptions.MANAGE_DATA:
if (!bypassLogin && !this.manageDataConfig.options.some(o => o.label === i18next.t("menuUiHandler:linkDiscord") || o.label === i18next.t("menuUiHandler:unlinkDiscord"))) {
this.manageDataConfig.options.splice(this.manageDataConfig.options.length-1,0,
{
label: loggedInUser.discordId === "" ? i18next.t("menuUiHandler:linkDiscord") : i18next.t("menuUiHandler:unlinkDiscord"),
handler: () => {
if (loggedInUser?.discordId === "") {
const token = Utils.getCookie(Utils.sessionIdKey);
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/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&state=${token}`;
window.open(discordUrl, "_self");
return true;
} else {
Utils.apiPost("/auth/discord/logout", undefined, undefined, true).then(res => {
if (!res.ok) {
console.error(`Unlink failed (${res.status}: ${res.statusText})`);
}
updateUserInfo().then(() => this.scene.reset(true, true));
});
return true;
}
}
},
{
label: loggedInUser?.googleId === "" ? i18next.t("menuUiHandler:linkGoogle") : i18next.t("menuUiHandler:unlinkGoogle"),
handler: () => {
if (loggedInUser?.googleId === "") {
const token = Utils.getCookie(Utils.sessionIdKey);
const redirectUri = encodeURIComponent(`${Utils.serverUrl}/auth/google/callback`);
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&response_type=code&redirect_uri=${redirectUri}&scope=openid&state=${token}`;
window.open(googleUrl, "_self");
return true;
} else {
Utils.apiPost("/auth/google/logout", undefined, undefined, true).then(res => {
if (!res.ok) {
console.error(`Unlink failed (${res.status}: ${res.statusText})`);
}
updateUserInfo().then(() => this.scene.reset(true, true));
});
return true;
}
}
});
}
ui.setOverlayMode(Mode.MENU_OPTION_SELECT, this.manageDataConfig);
success = true;
break;

2
src/vite.env.d.ts vendored
View File

@ -5,6 +5,8 @@ interface ImportMetaEnv {
readonly VITE_BYPASS_TUTORIAL?: string;
readonly VITE_API_BASE_URL?: string;
readonly VITE_SERVER_URL?: string;
readonly VITE_DISCORD_CLIENT_ID?: string;
readonly VITE_GOOGLE_CLIENT_ID?: string;
}
interface ImportMeta {