From 787f3d985aed56a3c39347ab8cc461bcce819910 Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Tue, 4 Nov 2025 16:02:24 +0200 Subject: [PATCH 1/5] Fix error message on disconnect social auth Fix how the error message is fetched and displayed when disconnecting a social auth account. Refs. TS-2758 --- .../settings/user/Connections/Connections.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx index bdf9bea79..c0e7880b8 100644 --- a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx +++ b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx @@ -13,6 +13,7 @@ import { type OutletContextShape } from "~/root"; import { ApiAction } from "@thunderstore/ts-api-react-actions"; import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn"; import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; +import { ApiError } from "@thunderstore/thunderstore-api"; type ProvidersType = { name: string; @@ -56,11 +57,24 @@ export default function Connections() { duration: 30000, }); }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const onSubmitError = (_e: unknown) => { + + const onSubmitError = (error: unknown) => { + let message = "Error when disconnecting account."; + + if (error instanceof ApiError) { + const fieldErrors = error.getFieldErrors(); + message = + fieldErrors.non_field_errors?.[0] || + fieldErrors.detail?.[0] || + fieldErrors.root?.[0] || + error.message || + message; + } + toast.addToast({ csVariant: "danger", - children: <>Unknown error occurred. The error has been logged, + children: <>{message}, + duration: 8000, }); }; From 096b8fd66186e81727104e380fdfe37cee7c88d2 Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Fri, 14 Nov 2025 16:53:37 +0200 Subject: [PATCH 2/5] Add disabled flag to Connection.tsx Add disable flag to render a connection switch disabled. Refs. TS-2758 --- .../app/commonComponents/Connection/Connection.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.tsx b/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.tsx index 0203124e8..51d925530 100644 --- a/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.tsx +++ b/apps/cyberstorm-remix/app/commonComponents/Connection/Connection.tsx @@ -11,10 +11,12 @@ interface ConnectionProps { connection?: OAuthConnection; connectionLink: string; disconnectFunction: (data: userLinkedAccountDisconnectProviders) => void; + disabled: boolean; } export function Connection(props: ConnectionProps) { - const { connection, identifier, icon, name, connectionLink } = props; + const { connection, identifier, icon, name, connectionLink, disabled } = + props; return (
@@ -30,6 +32,7 @@ export function Connection(props: ConnectionProps) {
) : null} { if (connection) { From 0eec26d9c2b5a089de85f6e8bbdaac9e587fdbab Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Fri, 14 Nov 2025 16:55:07 +0200 Subject: [PATCH 3/5] Simplify Settings.tsx Simplify Settings parent component to be a simple component holding no logic. Create helper class for getting current tab class. Refs. TS-2758 --- .../app/settings/user/Settings.tsx | 61 ++++--------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/apps/cyberstorm-remix/app/settings/user/Settings.tsx b/apps/cyberstorm-remix/app/settings/user/Settings.tsx index 6b05b28fb..54ef6ac9e 100644 --- a/apps/cyberstorm-remix/app/settings/user/Settings.tsx +++ b/apps/cyberstorm-remix/app/settings/user/Settings.tsx @@ -1,56 +1,24 @@ +import "./Settings.css"; import { Outlet, useLocation, useOutletContext } from "react-router"; import { NewLink, Tabs } from "@thunderstore/cyberstorm"; import { PageHeader } from "~/commonComponents/PageHeader/PageHeader"; - import { type OutletContextShape } from "../../root"; -import "./Settings.css"; -import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn"; - -// export async function clientLoader() { -// const _storage = new NamespacedStorageManager(SESSION_STORAGE_KEY); -// const currentUser = getSessionCurrentUser(_storage, true, undefined, () => { -// clearSession(_storage); -// throw new Response("Your session has expired, please log in again", { -// status: 401, -// }); -// // redirect("/"); -// }); - -// if ( -// !currentUser.username || -// (currentUser.username && currentUser.username === "") -// ) { -// clearSession(_storage); -// throw new Response("Not logged in.", { status: 401 }); -// } else { -// return { -// currentUser: currentUser as typeof currentUserSchema._type, -// }; -// } -// } -// export function HydrateFallback() { -// return
Loading...
; -// } +export default function UserSettings() { + const context = useOutletContext(); + const { pathname } = useLocation(); + const currentTab = pathname.endsWith("/account/") ? "account" : "settings"; -export default function Community() { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // const { currentUser } = useLoaderData(); - const location = useLocation(); - const outletContext = useOutletContext() as OutletContextShape; - - if (!outletContext.currentUser || !outletContext.currentUser.username) - return ; - - const currentTab = location.pathname.endsWith("/account/") - ? "account" - : "settings"; + function tabClass(tab: string) { + return `tabs-item${currentTab === tab ? " tabs-item--current" : ""}`; + } return ( <> Settings +
Settings @@ -69,15 +35,14 @@ export default function Community() { primitiveType="cyberstormLink" linkId="SettingsAccount" aria-current={currentTab === "account"} - rootClasses={`tabs-item${ - currentTab === "account" ? " tabs-item--current" : "" - }`} + rootClasses={tabClass("account")} > Account +
- +
From 15ddefdd89fd08966358e3617df4387de96919ff Mon Sep 17 00:00:00 2001 From: Daniel Vainio Date: Fri, 14 Nov 2025 16:56:06 +0200 Subject: [PATCH 4/5] Update Connections.tsx logic Update logic for handling removing social auth accounts. - Use Suspend/Await pattern in component - Always fetch new user in clientLoader & update session - Revalidate when successful disconnect - Use HydrateFallback in order to properly render loading state - Keep track of disconnected social auths - Prevent user from disconnecting last social auth account - Show proper success message to user Refs. TS-2758 --- .../settings/user/Connections/Connections.tsx | 189 +++++++++++------- 1 file changed, 113 insertions(+), 76 deletions(-) diff --git a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx index c0e7880b8..ff7eee0ee 100644 --- a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx +++ b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx @@ -1,19 +1,30 @@ +import { type ReactElement, useRef, Suspense } from "react"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"; + +import { + useOutletContext, + useRevalidator, + useLoaderData, + Await, +} from "react-router"; + import { NewLink, OverwolfLogo, useToast } from "@thunderstore/cyberstorm"; +import { ApiAction } from "@thunderstore/ts-api-react-actions"; +import { ApiError } from "@thunderstore/thunderstore-api"; -import { useOutletContext } from "react-router"; import { buildAuthLoginUrl } from "cyberstorm/utils/ThunderstoreAuth"; - -import { userLinkedAccountDisconnect } from "../../../../../../packages/thunderstore-api/src"; +import { + getPublicEnvVariables, + getSessionTools, +} from "cyberstorm/security/publicEnvVariables"; import { Connection } from "~/commonComponents/Connection/Connection"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"; -import { type ReactElement } from "react"; -import { type OutletContextShape } from "~/root"; -import { ApiAction } from "@thunderstore/ts-api-react-actions"; import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn"; -import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; -import { ApiError } from "@thunderstore/thunderstore-api"; +import { type OutletContextShape } from "~/root"; + +import { userLinkedAccountDisconnect } from "../../../../../../packages/thunderstore-api/src"; type ProvidersType = { name: string; @@ -35,27 +46,52 @@ export const PROVIDERS: ProvidersType = [ { name: "Overwolf", identifier: "overwolf", icon: OverwolfLogo() }, ]; +export function HydrateFallback() { + return
Loading...
; +} + +export async function clientLoader() { + const tools = getSessionTools(); + const currentUser = tools.getSessionCurrentUser(true); + return { currentUser }; +} + export default function Connections() { const outletContext = useOutletContext() as OutletContextShape; + const { currentUser } = useLoaderData(); + const revalidator = useRevalidator(); + const toast = useToast(); + const disconnectingProviderRef = useRef(null); - if (!outletContext.currentUser || !outletContext.currentUser.username) - return ; + const onlyOneConnected = () => { + const connectedProviders = + outletContext.currentUser?.connections?.length ?? 0; + return connectedProviders === 1; + }; - const toast = useToast(); + const getConnection = (provider: ProvidersType) => + outletContext.currentUser?.connections?.find( + (c) => c.provider?.toLowerCase() === provider.identifier + ); // eslint-disable-next-line @typescript-eslint/no-unused-vars const onSubmitSuccess = (_r: unknown) => { - if (!outletContext.currentUser || !outletContext.currentUser.username) - throw new Error("User not logged in"); + const username = outletContext.currentUser?.username; + + revalidator.revalidate(); + toast.addToast({ csVariant: "success", children: ( <> - User {outletContext.currentUser.username} was disconnected from TODO + User {username} was disconnected from{" "} + {disconnectingProviderRef.current} ), duration: 30000, }); + + disconnectingProviderRef.current = null; }; const onSubmitError = (error: unknown) => { @@ -90,65 +126,66 @@ export default function Connections() { ]); return ( -
-
-
-

Connections

-

- This information will not be shared outside of Thunderstore. Read - more on our{" "} - - Privacy Policy - - . -

-
-
- {PROVIDERS.map((p) => { - if ( - !outletContext.currentUser || - !outletContext.currentUser.username - ) - throw new Error("User not logged in"); - return ( - c.provider.toLowerCase() === p.identifier - )} - connectionLink={buildAuthLoginUrl({ - type: p.identifier, - authBaseDomain: publicEnvVariables.VITE_AUTH_BASE_URL || "", - authReturnDomain: - publicEnvVariables.VITE_AUTH_RETURN_URL || "", - })} - disconnectFunction={(p) => { - if ( - !outletContext.currentUser || - !outletContext.currentUser.username - ) - throw new Error("User not logged in"); - return onSubmit({ - params: { - provider: p, - }, - config: outletContext.requestConfig, - queryParams: {}, - data: { provider: p }, - }); - }} - /> - ); - })} -
-
-
+ + Error loading user}> + {(resolvedCurrentUser) => { + outletContext.currentUser = resolvedCurrentUser; + + if (!outletContext.currentUser) { + return ; + } + + return ( +
+
+
+

Connections

+

+ This information will not be shared outside of Thunderstore. + Read more on our{" "} + + Privacy Policy + + . +

+
+
+ {PROVIDERS.map((provider) => ( + { + disconnectingProviderRef.current = provider; + return onSubmit({ + params: { provider }, + config: outletContext.requestConfig, + queryParams: {}, + data: { provider }, + }); + }} + /> + ))} +
+
+
+ ); + }} +
+
); } From 4310ad1a9c61555e96c0e7a89f140cead7224e29 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Mon, 17 Nov 2025 16:31:20 +0200 Subject: [PATCH 5/5] fix(Nimbus) Simplify Connections page data fetching and usage - Remove loaders as the data is available in the outletContext.currentUser, and it's always up-to-date, becuase settings pages path cause a revalidation in root loaders - Remove usage of Suspense/Await as it's not needed - Add usage of isHydrated to display "loading" element while client hydration is on going - Add correct nextUrl into the buildAuthLoginUrl function props --- .../settings/user/Connections/Connections.tsx | 147 ++++++++---------- 1 file changed, 62 insertions(+), 85 deletions(-) diff --git a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx index ff7eee0ee..6b88d8b89 100644 --- a/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx +++ b/apps/cyberstorm-remix/app/settings/user/Connections/Connections.tsx @@ -1,38 +1,31 @@ -import { type ReactElement, useRef, Suspense } from "react"; +import { type ReactElement, useRef } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"; -import { - useOutletContext, - useRevalidator, - useLoaderData, - Await, -} from "react-router"; +import { useOutletContext, useRevalidator } from "react-router"; import { NewLink, OverwolfLogo, useToast } from "@thunderstore/cyberstorm"; import { ApiAction } from "@thunderstore/ts-api-react-actions"; import { ApiError } from "@thunderstore/thunderstore-api"; import { buildAuthLoginUrl } from "cyberstorm/utils/ThunderstoreAuth"; -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; +import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables"; import { Connection } from "~/commonComponents/Connection/Connection"; import { NotLoggedIn } from "~/commonComponents/NotLoggedIn/NotLoggedIn"; import { type OutletContextShape } from "~/root"; import { userLinkedAccountDisconnect } from "../../../../../../packages/thunderstore-api/src"; +import { useHydrated } from "remix-utils/use-hydrated"; type ProvidersType = { name: string; identifier: "discord" | "github" | "overwolf"; icon: ReactElement; -}[]; +}; -export const PROVIDERS: ProvidersType = [ +export const PROVIDERS: ProvidersType[] = [ { name: "Discord", identifier: "discord", @@ -46,19 +39,9 @@ export const PROVIDERS: ProvidersType = [ { name: "Overwolf", identifier: "overwolf", icon: OverwolfLogo() }, ]; -export function HydrateFallback() { - return
Loading...
; -} - -export async function clientLoader() { - const tools = getSessionTools(); - const currentUser = tools.getSessionCurrentUser(true); - return { currentUser }; -} - export default function Connections() { const outletContext = useOutletContext() as OutletContextShape; - const { currentUser } = useLoaderData(); + const isHydrated = useHydrated(); const revalidator = useRevalidator(); const toast = useToast(); const disconnectingProviderRef = useRef(null); @@ -123,69 +106,63 @@ export default function Connections() { const publicEnvVariables = getPublicEnvVariables([ "VITE_AUTH_BASE_URL", "VITE_AUTH_RETURN_URL", + "VITE_BETA_SITE_URL", ]); + if (!isHydrated) { + return
Loading...
; + } + + if (!outletContext.currentUser) { + return ; + } + return ( - - Error loading user}> - {(resolvedCurrentUser) => { - outletContext.currentUser = resolvedCurrentUser; - - if (!outletContext.currentUser) { - return ; - } - - return ( -
-
-
-

Connections

-

- This information will not be shared outside of Thunderstore. - Read more on our{" "} - - Privacy Policy - - . -

-
-
- {PROVIDERS.map((provider) => ( - { - disconnectingProviderRef.current = provider; - return onSubmit({ - params: { provider }, - config: outletContext.requestConfig, - queryParams: {}, - data: { provider }, - }); - }} - /> - ))} -
-
-
- ); - }} -
-
+
+
+
+

Connections

+

+ This information will not be shared outside of Thunderstore. Read + more on our{" "} + + Privacy Policy + + . +

+
+
+ {PROVIDERS.map((provider) => ( + { + disconnectingProviderRef.current = provider; + return onSubmit({ + params: { provider }, + config: outletContext.requestConfig, + queryParams: {}, + data: { provider }, + }); + }} + /> + ))} +
+
+
); }