From af4fcb8f064a5f8bb6db5180ebe23c506e80e87a Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:49:11 -0300 Subject: [PATCH] feat: manage account buttons --- src/locales/en/translation.json | 13 +- src/locales/pt-BR/translation.json | 13 +- src/main/events/index.ts | 1 + src/main/events/misc/open-manage-account.ts | 25 ++ src/main/services/hydra-api.ts | 14 +- src/preload/index.ts | 3 + src/renderer/src/declaration.d.ts | 2 + ...privacy.css.ts => settings-account.css.ts} | 0 .../src/pages/settings/settings-account.tsx | 217 ++++++++++++++++++ .../src/pages/settings/settings-privacy.tsx | 139 ----------- src/renderer/src/pages/settings/settings.tsx | 6 +- src/types/index.ts | 2 + 12 files changed, 286 insertions(+), 149 deletions(-) create mode 100644 src/main/events/misc/open-manage-account.ts rename src/renderer/src/pages/settings/{settings-privacy.css.ts => settings-account.css.ts} (100%) create mode 100644 src/renderer/src/pages/settings/settings-account.tsx delete mode 100644 src/renderer/src/pages/settings/settings-privacy.tsx diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4e3dcb37..e55954bf 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -280,7 +280,18 @@ "launch_minimized": "Launch Hydra minimized", "disable_nsfw_alert": "Disable NSFW alert", "seed_after_download_complete": "Seed after download complete", - "show_hidden_achievement_description": "Show hidden achievements description before unlocking them" + "show_hidden_achievement_description": "Show hidden achievements description before unlocking them", + "account": "Account", + "no_users_blocked": "You have no blocked users", + "subscription": "Hydra Cloud subscription", + "subscription_active_until": "Your Hydra Cloud is active until {{date}}", + "subscription_not_active": "You don't have an active Hydra Cloud subscription", + "manage_account": "Manage account", + "manage_subscription": "Manage subscription", + "update_email": "Update email", + "update_password": "Update password", + "current_email": "Current email:", + "no_associated_email": "You don't have an associated email yet" }, "notifications": { "download_complete": "Download complete", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 2a80084f..24ab4653 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -268,7 +268,18 @@ "launch_minimized": "Iniciar o Hydra minimizado", "disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado", "seed_after_download_complete": "Semear após a conclusão do download", - "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las" + "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las", + "account": "Conta", + "no_users_blocked": "Você não bloqueou nenhum usuário", + "subscription": "Assinatura Hydra Cloud", + "subscription_active_until": "Seu Hydra Cloud ficará ativo até {{date}}", + "subscription_not_active": "Você não possui uma assinatura Hydra Cloud ativa", + "manage_account": "Gerenciar conta", + "manage_subscription": "Gerenciar assinatura", + "update_email": "Atualizar email", + "update_password": "Atualizar senha", + "current_email": "Email atual:", + "no_associated_email": "Você ainda não associou nenhum email a sua conta" }, "notifications": { "download_complete": "Download concluído", diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 25882c3f..f0f882ca 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -30,6 +30,7 @@ import "./library/remove-game-from-library"; import "./library/select-game-wine-prefix"; import "./library/reset-game-achievements"; import "./misc/open-checkout"; +import "./misc/open-manage-account"; import "./misc/open-external"; import "./misc/show-open-dialog"; import "./misc/get-features"; diff --git a/src/main/events/misc/open-manage-account.ts b/src/main/events/misc/open-manage-account.ts new file mode 100644 index 00000000..2946b04b --- /dev/null +++ b/src/main/events/misc/open-manage-account.ts @@ -0,0 +1,25 @@ +import { shell } from "electron"; +import { registerEvent } from "../register-event"; +import { HydraApi, logger } from "@main/services"; +import { ManageAccountPage } from "@types"; + +const openManageAccount = async ( + _event: Electron.IpcMainInvokeEvent, + page: ManageAccountPage +) => { + try { + const { accessToken } = await HydraApi.refreshToken(); + + const params = new URLSearchParams({ + token: accessToken, + }); + + shell.openExternal( + `${import.meta.env.MAIN_VITE_AUTH_URL}/${page}?${params.toString()}` + ); + } catch (err) { + logger.error("Failed to open manage account", err); + } +}; + +registerEvent("openManageAccount", openManageAccount); diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 63dd9b16..4ab20e80 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -215,16 +215,20 @@ export class HydraApi { } } + public static async refreshToken() { + return this.instance + .post<{ accessToken: string; expiresIn: number }>(`/auth/refresh`, { + refreshToken: this.userAuth.refreshToken, + }) + .then((response) => response.data); + } + private static async revalidateAccessTokenIfExpired() { const now = new Date(); if (this.userAuth.expirationTimestamp < now.getTime()) { try { - const response = await this.instance.post(`/auth/refresh`, { - refreshToken: this.userAuth.refreshToken, - }); - - const { accessToken, expiresIn } = response.data; + const { accessToken, expiresIn } = await this.refreshToken(); const tokenExpirationTimestamp = now.getTime() + diff --git a/src/preload/index.ts b/src/preload/index.ts index 316397d2..24e6cf39 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -14,6 +14,7 @@ import type { CatalogueSearchPayload, SeedingStatus, GameAchievement, + ManageAccountPage, } from "@types"; import type { CatalogueCategory } from "@shared"; import type { AxiosProgressEvent } from "axios"; @@ -226,6 +227,8 @@ contextBridge.exposeInMainWorld("electron", { isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"), openExternal: (src: string) => ipcRenderer.invoke("openExternal", src), openCheckout: () => ipcRenderer.invoke("openCheckout"), + openManageAccount: (page: ManageAccountPage) => + ipcRenderer.invoke("openManageAccount", page), showOpenDialog: (options: Electron.OpenDialogOptions) => ipcRenderer.invoke("showOpenDialog", options), showItemInFolder: (path: string) => diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 88f3297f..33fcb7da 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -29,6 +29,7 @@ import type { UserAchievement, ComparedAchievements, CatalogueSearchPayload, + ManageAccountPage, } from "@types"; import type { AxiosProgressEvent } from "axios"; import type disk from "diskusage"; @@ -187,6 +188,7 @@ declare global { /* Misc */ openExternal: (src: string) => Promise; openCheckout: () => Promise; + openManageAccount: (page: ManageAccountPage) => Promise; getVersion: () => Promise; isStaging: () => Promise; ping: () => string; diff --git a/src/renderer/src/pages/settings/settings-privacy.css.ts b/src/renderer/src/pages/settings/settings-account.css.ts similarity index 100% rename from src/renderer/src/pages/settings/settings-privacy.css.ts rename to src/renderer/src/pages/settings/settings-account.css.ts diff --git a/src/renderer/src/pages/settings/settings-account.tsx b/src/renderer/src/pages/settings/settings-account.tsx new file mode 100644 index 00000000..16ed40e4 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-account.tsx @@ -0,0 +1,217 @@ +import { Button, SelectField } from "@renderer/components"; +import { SPACING_UNIT, vars } from "@renderer/theme.css"; +import { Controller, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +import * as styles from "./settings-account.css"; +import { useDate, useToast, useUserDetails } from "@renderer/hooks"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { + CloudIcon, + KeyIcon, + MailIcon, + XCircleFillIcon, +} from "@primer/octicons-react"; +import { settingsContext } from "@renderer/context"; + +interface FormValues { + profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE"; +} + +export function SettingsAccount() { + const { t } = useTranslation("settings"); + + const [isUnblocking, setIsUnblocking] = useState(false); + + const { showSuccessToast } = useToast(); + + const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext); + + const { formatDate } = useDate(); + + const { + control, + formState: { isSubmitting }, + setValue, + handleSubmit, + } = useForm(); + + const { patchUser, userDetails } = useUserDetails(); + + const { unblockUser } = useUserDetails(); + + useEffect(() => { + if (userDetails?.profileVisibility) { + setValue("profileVisibility", userDetails.profileVisibility); + } + }, [userDetails, setValue]); + + const visibilityOptions = [ + { value: "PUBLIC", label: t("public") }, + { value: "FRIENDS", label: t("friends_only") }, + { value: "PRIVATE", label: t("private") }, + ]; + + const onSubmit = async (values: FormValues) => { + await patchUser(values); + showSuccessToast(t("changes_saved")); + }; + + const handleUnblockClick = useCallback( + (id: string) => { + setIsUnblocking(true); + + unblockUser(id) + .then(() => { + fetchBlockedUsers(); + showSuccessToast(t("user_unblocked")); + }) + .finally(() => { + setIsUnblocking(false); + }); + }, + [unblockUser, fetchBlockedUsers, t, showSuccessToast] + ); + + return ( +
+ { + const handleChange = ( + event: React.ChangeEvent + ) => { + field.onChange(event); + handleSubmit(onSubmit)(); + }; + + return ( + <> + ({ + key: visiblity.value, + value: visiblity.value, + label: visiblity.label, + }))} + disabled={isSubmitting} + /> + + {t("profile_visibility_description")} + + ); + }} + /> + +

+ {t("manage_account")} +

+ + {userDetails?.email ? ( +
+

{t("current_email")}

+

{userDetails.email}

+
+ ) : ( +

{t("no_associated_email")}

+ )} + +
+ + + +
+ +

+ {t("subscription")} +

+ {userDetails?.subscription?.expiresAt ? ( +

+ {t("subscription_active_until", { + date: formatDate(userDetails?.subscription?.expiresAt), + })} +

+ ) : ( +

{t("subscription_not_active")}

+ )} + +
+ +
+ +

+ {t("blocked_users")} +

+ +
    + {blockedUsers.length > 0 ? ( + blockedUsers.map((user) => { + return ( +
  • +
    + {user.displayName} + {user.displayName} +
    + + +
  • + ); + }) + ) : ( + {t("no_users_blocked")} + )} +
+ + ); +} diff --git a/src/renderer/src/pages/settings/settings-privacy.tsx b/src/renderer/src/pages/settings/settings-privacy.tsx deleted file mode 100644 index b93d1d07..00000000 --- a/src/renderer/src/pages/settings/settings-privacy.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { SelectField } from "@renderer/components"; -import { SPACING_UNIT } from "@renderer/theme.css"; -import { Controller, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; - -import * as styles from "./settings-privacy.css"; -import { useToast, useUserDetails } from "@renderer/hooks"; -import { useCallback, useContext, useEffect, useState } from "react"; -import { XCircleFillIcon } from "@primer/octicons-react"; -import { settingsContext } from "@renderer/context"; - -interface FormValues { - profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE"; -} - -export function SettingsPrivacy() { - const { t } = useTranslation("settings"); - - const [isUnblocking, setIsUnblocking] = useState(false); - - const { showSuccessToast } = useToast(); - - const { blockedUsers, fetchBlockedUsers } = useContext(settingsContext); - - const { - control, - formState: { isSubmitting }, - setValue, - handleSubmit, - } = useForm(); - - const { patchUser, userDetails } = useUserDetails(); - - const { unblockUser } = useUserDetails(); - - useEffect(() => { - if (userDetails?.profileVisibility) { - setValue("profileVisibility", userDetails.profileVisibility); - } - }, [userDetails, setValue]); - - const visibilityOptions = [ - { value: "PUBLIC", label: t("public") }, - { value: "FRIENDS", label: t("friends_only") }, - { value: "PRIVATE", label: t("private") }, - ]; - - const onSubmit = async (values: FormValues) => { - await patchUser(values); - showSuccessToast(t("changes_saved")); - }; - - const handleUnblockClick = useCallback( - (id: string) => { - setIsUnblocking(true); - - unblockUser(id) - .then(() => { - fetchBlockedUsers(); - showSuccessToast(t("user_unblocked")); - }) - .finally(() => { - setIsUnblocking(false); - }); - }, - [unblockUser, fetchBlockedUsers, t, showSuccessToast] - ); - - return ( -
- { - const handleChange = ( - event: React.ChangeEvent - ) => { - field.onChange(event); - handleSubmit(onSubmit)(); - }; - - return ( - <> - ({ - key: visiblity.value, - value: visiblity.value, - label: visiblity.label, - }))} - disabled={isSubmitting} - /> - - {t("profile_visibility_description")} - - ); - }} - /> - -

- {t("blocked_users")} -

- -
    - {blockedUsers.map((user) => { - return ( -
  • -
    - {user.displayName} - {user.displayName} -
    - - -
  • - ); - })} -
- - ); -} diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index dffdfbae..5fba6c5d 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -11,7 +11,7 @@ import { SettingsContextConsumer, SettingsContextProvider, } from "@renderer/context"; -import { SettingsPrivacy } from "./settings-privacy"; +import { SettingsAccount } from "./settings-account"; import { useUserDetails } from "@renderer/hooks"; import { useMemo } from "react"; @@ -28,7 +28,7 @@ export default function Settings() { "Real-Debrid", ]; - if (userDetails) return [...categories, t("privacy")]; + if (userDetails) return [...categories, t("account")]; return categories; }, [userDetails, t]); @@ -53,7 +53,7 @@ export default function Settings() { return ; } - return ; + return ; }; return ( diff --git a/src/types/index.ts b/src/types/index.ts index 345893a5..66c458b5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -416,6 +416,8 @@ export interface CatalogueSearchPayload { developers: string[]; } +export type ManageAccountPage = "update-email" | "update-password"; + export * from "./steam.types"; export * from "./real-debrid.types"; export * from "./ludusavi.types";