diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eb1963cb..4bd99c45 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.37", + "urql": "^4.0.6" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", @@ -23549,6 +23550,18 @@ "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", "dev": true }, + "node_modules/urql": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/urql/-/urql-4.0.6.tgz", + "integrity": "sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==", + "dependencies": { + "@urql/core": "^4.2.0", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 548526af..6ba61c63 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.0.5", - "ua-parser-js": "^1.0.37" + "ua-parser-js": "^1.0.37", + "urql": "^4.0.6" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", diff --git a/frontend/src/atoms.ts b/frontend/src/atoms.ts index 7ecef58a..d715015d 100644 --- a/frontend/src/atoms.ts +++ b/frontend/src/atoms.ts @@ -15,8 +15,9 @@ import { AnyVariables, CombinedError, OperationContext } from "@urql/core"; import { atom, WritableAtom } from "jotai"; import { useHydrateAtoms } from "jotai/utils"; -import { AtomWithQuery, atomWithQuery, clientAtom } from "jotai-urql"; +import { AtomWithQuery, clientAtom } from "jotai-urql"; import type { ReactElement } from "react"; +import { useQuery } from "urql"; import { graphql } from "./gql"; import { client } from "./graphql"; @@ -73,51 +74,15 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ ` ... on User { id } - - ... on Anonymous { - id - } } } `); -const currentViewerAtom = atomWithQuery({ query: CURRENT_VIEWER_QUERY }); - -export const currentUserIdAtom: GqlAtom = mapQueryAtom( - currentViewerAtom, - (data) => { - if (data.viewer.__typename === "User") { - return data.viewer.id; - } - return null; - }, -); - -const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ ` - query CurrentViewerSessionQuery { - viewerSession { - __typename - ... on BrowserSession { - id - } - - ... on Anonymous { - id - } - } - } -`); - -const currentViewerSessionAtom = atomWithQuery({ - query: CURRENT_VIEWER_SESSION_QUERY, -}); - -export const currentBrowserSessionIdAtom: GqlAtom = mapQueryAtom( - currentViewerSessionAtom, - (data) => { - if (data.viewerSession.__typename === "BrowserSession") { - return data.viewerSession.id; - } - return null; - }, -); +export const useCurrentUserId = (): string | null => { + const [result] = useQuery({ query: CURRENT_VIEWER_QUERY }); + if (result.error) throw result.error; + if (!result.data) throw new Error(); // Suspense mode is enabled + return result.data.viewer.__typename === "User" + ? result.data.viewer.id + : null; +}; diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index 79e99620..90eb74a0 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -18,7 +18,6 @@ import { atomFamily } from "jotai/utils"; import { atomWithMutation } from "jotai-urql"; import { useCallback } from "react"; -import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms"; import { FragmentType, graphql, useFragment } from "../gql"; import { parseUserAgent, @@ -74,21 +73,12 @@ export const useEndBrowserSession = ( ): (() => Promise) => { const endSession = useSetAtom(endBrowserSessionFamily(sessionId)); - // Pull those atoms to reset them when the current session is ended - const currentUserId = useSetAtom(currentUserIdAtom); - const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom); - const onSessionEnd = useCallback(async (): Promise => { await endSession(); if (isCurrent) { - currentBrowserSessionId({ - requestPolicy: "network-only", - }); - currentUserId({ - requestPolicy: "network-only", - }); + window.location.reload(); } - }, [isCurrent, endSession, currentBrowserSessionId, currentUserId]); + }, [isCurrent, endSession]); return onSessionEnd; }; diff --git a/frontend/src/components/Layout/Layout.test.tsx b/frontend/src/components/Layout/Layout.test.tsx index 0a5cf0de..2c9b3809 100644 --- a/frontend/src/components/Layout/Layout.test.tsx +++ b/frontend/src/components/Layout/Layout.test.tsx @@ -15,28 +15,17 @@ // @vitest-environment happy-dom import { render } from "@testing-library/react"; -import { describe, expect, it, vi, afterAll, beforeEach } from "vitest"; +import { describe, expect, it } from "vitest"; -import { currentUserIdAtom, GqlResult } from "../../atoms"; import { WithLocation } from "../../test-utils/WithLocation"; import Layout from "./Layout"; describe("", () => { - beforeEach(() => { - vi.spyOn(currentUserIdAtom, "read").mockResolvedValue( - "abc123" as unknown as GqlResult, - ); - }); - - afterAll(() => { - vi.restoreAllMocks(); - }); - it("renders app navigation correctly", async () => { const component = render( - + , ); diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx index 26773c89..9ddbb2f4 100644 --- a/frontend/src/components/Layout/Layout.tsx +++ b/frontend/src/components/Layout/Layout.tsx @@ -15,37 +15,25 @@ import { useAtomValue } from "jotai"; import { useTranslation } from "react-i18next"; -import { currentUserIdAtom } from "../../atoms"; -import { isErr, unwrapErr, unwrapOk } from "../../result"; import { appConfigAtom, routeAtom } from "../../routing"; import Footer from "../Footer"; -import GraphQLError from "../GraphQLError"; import NavBar from "../NavBar"; import NavItem from "../NavItem"; -import NotLoggedIn from "../NotLoggedIn"; import UserGreeting from "../UserGreeting"; import styles from "./Layout.module.css"; -const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => { +const Layout: React.FC<{ + userId: string; + children?: React.ReactNode; +}> = ({ userId, children }) => { const route = useAtomValue(routeAtom); const appConfig = useAtomValue(appConfigAtom); - const result = useAtomValue(currentUserIdAtom); const { t } = useTranslation(); - if (isErr(result)) return ; - // Hide the nav bar & user greeting on the verify-email page const shouldHideNavBar = route.type === "verify-email"; - const userId = unwrapOk(result); - if (userId === null) - return ( -
- -
- ); - return (
{shouldHideNavBar ? null : ( diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index a64f704b..8384e408 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -13,10 +13,8 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n": + "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n": types.CurrentViewerQueryDocument, - "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n": - types.CurrentViewerSessionQueryDocument, "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": types.BrowserSession_SessionFragmentDoc, "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": @@ -79,6 +77,8 @@ const documents = { types.SessionsOverviewQueryDocument, "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n": types.VerifyEmailQueryDocument, + "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n": + types.CurrentViewerSessionQueryDocument, }; /** @@ -99,14 +99,8 @@ export function graphql(source: string): unknown; * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n", -): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n", -): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"]; + source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n", +): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -293,6 +287,12 @@ export function graphql( export function graphql( source: "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n", ): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n", +): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 57a4a846..ffe38e9d 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1111,21 +1111,7 @@ export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>; export type CurrentViewerQueryQuery = { __typename?: "Query"; - viewer: - | { __typename: "Anonymous"; id: string } - | { __typename: "User"; id: string }; -}; - -export type CurrentViewerSessionQueryQueryVariables = Exact<{ - [key: string]: never; -}>; - -export type CurrentViewerSessionQueryQuery = { - __typename?: "Query"; - viewerSession: - | { __typename: "Anonymous"; id: string } - | { __typename: "BrowserSession"; id: string } - | { __typename: "Oauth2Session" }; + viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string }; }; export type BrowserSession_SessionFragment = { @@ -1680,6 +1666,18 @@ export type VerifyEmailQueryQuery = { | null; }; +export type CurrentViewerSessionQueryQueryVariables = Exact<{ + [key: string]: never; +}>; + +export type CurrentViewerSessionQueryQuery = { + __typename?: "Query"; + viewerSession: + | { __typename: "Anonymous" } + | { __typename: "BrowserSession"; id: string } + | { __typename: "Oauth2Session" }; +}; + export const BrowserSession_SessionFragmentDoc = { kind: "Document", definitions: [ @@ -2090,19 +2088,6 @@ export const CurrentViewerQueryDocument = { ], }, }, - { - kind: "InlineFragment", - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "Anonymous" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - ], - }, - }, ], }, }, @@ -2114,60 +2099,6 @@ export const CurrentViewerQueryDocument = { CurrentViewerQueryQuery, CurrentViewerQueryQueryVariables >; -export const CurrentViewerSessionQueryDocument = { - kind: "Document", - definitions: [ - { - kind: "OperationDefinition", - operation: "query", - name: { kind: "Name", value: "CurrentViewerSessionQuery" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { - kind: "Field", - name: { kind: "Name", value: "viewerSession" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "__typename" } }, - { - kind: "InlineFragment", - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "BrowserSession" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - ], - }, - }, - { - kind: "InlineFragment", - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "Anonymous" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode< - CurrentViewerSessionQueryQuery, - CurrentViewerSessionQueryQueryVariables ->; export const EndBrowserSessionDocument = { kind: "Document", definitions: [ @@ -4435,3 +4366,44 @@ export const VerifyEmailQueryDocument = { VerifyEmailQueryQuery, VerifyEmailQueryQueryVariables >; +export const CurrentViewerSessionQueryDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "query", + name: { kind: "Name", value: "CurrentViewerSessionQuery" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "viewerSession" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "__typename" } }, + { + kind: "InlineFragment", + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "BrowserSession" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + CurrentViewerSessionQueryQuery, + CurrentViewerSessionQueryQueryVariables +>; diff --git a/frontend/src/graphql.ts b/frontend/src/graphql.ts index ab10fc3c..f8737914 100644 --- a/frontend/src/graphql.ts +++ b/frontend/src/graphql.ts @@ -132,6 +132,7 @@ const exchanges = [ export const client = createClient({ url: appConfig.graphqlEndpoint, + suspense: true, // Add the devtools exchange in development exchanges: import.meta.env.DEV ? [devtoolsExchange, ...exchanges] : exchanges, }); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2dad19f5..a2fe8cdb 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -18,32 +18,49 @@ import { DevTools } from "jotai-devtools"; import { Suspense, StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; +import { Provider as UrqlProvider } from "urql"; -import { HydrateAtoms } from "./atoms"; +import { HydrateAtoms, useCurrentUserId } from "./atoms"; +import ErrorBoundary from "./components/ErrorBoundary"; import Layout from "./components/Layout"; import LoadingScreen from "./components/LoadingScreen"; import LoadingSpinner from "./components/LoadingSpinner"; +import NotLoggedIn from "./components/NotLoggedIn"; +import { client } from "./graphql"; import i18n from "./i18n"; import { Router } from "./routing"; import "./main.css"; +const App: React.FC = () => { + const userId = useCurrentUserId(); + if (userId === null) return ; + + return ( + + }> + + + + ); +}; + createRoot(document.getElementById("root") as HTMLElement).render( - - {import.meta.env.DEV && } - - }> - - - - }> - - - - - - - - + + + + {import.meta.env.DEV && } + + }> + + + + + + + + + + , ); diff --git a/frontend/src/pages/BrowserSession.tsx b/frontend/src/pages/BrowserSession.tsx index fbb85c11..6f2e2731 100644 --- a/frontend/src/pages/BrowserSession.tsx +++ b/frontend/src/pages/BrowserSession.tsx @@ -12,17 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; -import { atomWithQuery } from "jotai-urql"; +import { useQuery } from "urql"; -import { mapQueryAtom } from "../atoms"; import ErrorBoundary from "../components/ErrorBoundary"; import GraphQLError from "../components/GraphQLError"; import NotFound from "../components/NotFound"; import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail"; import { graphql } from "../gql"; -import { isErr, unwrapErr, unwrapOk } from "../result"; const QUERY = graphql(/* GraphQL */ ` query BrowserSessionQuery($id: ID!) { @@ -33,25 +29,15 @@ const QUERY = graphql(/* GraphQL */ ` } `); -const browserSessionFamily = atomFamily((id: string) => { - const browserSessionQueryAtom = atomWithQuery({ - query: QUERY, - getVariables: () => ({ id }), - }); - - const browserSessionAtom = mapQueryAtom( - browserSessionQueryAtom, - (data) => data?.browserSession, - ); - - return browserSessionAtom; -}); - const BrowserSession: React.FC<{ id: string }> = ({ id }) => { - const result = useAtomValue(browserSessionFamily(id)); - if (isErr(result)) return ; + const [result] = useQuery({ + query: QUERY, + variables: { id }, + }); + if (result.error) return ; + if (!result.data) throw new Error(); // Suspense mode is enabled - const browserSession = unwrapOk(result); + const browserSession = result.data.browserSession; if (!browserSession) return ; return ( diff --git a/frontend/src/pages/BrowserSessionList.tsx b/frontend/src/pages/BrowserSessionList.tsx index f778ce2e..6cc08428 100644 --- a/frontend/src/pages/BrowserSessionList.tsx +++ b/frontend/src/pages/BrowserSessionList.tsx @@ -12,27 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; - -import { currentUserIdAtom } from "../atoms"; import List from "../components/BrowserSessionList"; -import ErrorBoundary from "../components/ErrorBoundary"; -import GraphQLError from "../components/GraphQLError"; -import NotLoggedIn from "../components/NotLoggedIn"; -import { isErr, unwrapErr, unwrapOk } from "../result"; -const BrowserSessionList: React.FC = () => { - const result = useAtomValue(currentUserIdAtom); - if (isErr(result)) return ; - - const userId = unwrapOk(result); - if (userId === null) return ; - - return ( - - - - ); +const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => { + return ; }; export default BrowserSessionList; diff --git a/frontend/src/pages/OAuth2Client.tsx b/frontend/src/pages/OAuth2Client.tsx index 9840f326..931e33c3 100644 --- a/frontend/src/pages/OAuth2Client.tsx +++ b/frontend/src/pages/OAuth2Client.tsx @@ -12,17 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; -import { atomWithQuery } from "jotai-urql"; +import { useQuery } from "urql"; -import { mapQueryAtom } from "../atoms"; import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail"; import ErrorBoundary from "../components/ErrorBoundary"; import GraphQLError from "../components/GraphQLError"; import NotFound from "../components/NotFound"; import { graphql } from "../gql"; -import { isErr, unwrapErr, unwrapOk } from "../result"; const QUERY = graphql(/* GraphQL */ ` query OAuth2ClientQuery($id: ID!) { @@ -32,25 +28,15 @@ const QUERY = graphql(/* GraphQL */ ` } `); -const oauth2ClientFamily = atomFamily((id: string) => { - const oauth2ClientQueryAtom = atomWithQuery({ - query: QUERY, - getVariables: () => ({ id }), - }); - - const oauth2ClientAtom = mapQueryAtom( - oauth2ClientQueryAtom, - (data) => data?.oauth2Client, - ); - - return oauth2ClientAtom; -}); - const OAuth2Client: React.FC<{ id: string }> = ({ id }) => { - const result = useAtomValue(oauth2ClientFamily(id)); - if (isErr(result)) return ; + const [result] = useQuery({ + query: QUERY, + variables: { id }, + }); + if (result.error) return ; + if (!result.data) throw new Error(); // Suspense mode is enabled - const oauth2Client = unwrapOk(result); + const oauth2Client = result.data.oauth2Client; if (!oauth2Client) return ; return ( diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 38473236..53787dd5 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -12,22 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; - -import { currentUserIdAtom } from "../atoms"; import ErrorBoundary from "../components/ErrorBoundary"; -import GraphQLError from "../components/GraphQLError"; -import NotLoggedIn from "../components/NotLoggedIn"; import UserProfile from "../components/UserProfile"; -import { isErr, unwrapErr, unwrapOk } from "../result"; - -const Profile: React.FC = () => { - const result = useAtomValue(currentUserIdAtom); - if (isErr(result)) return ; - - const userId = unwrapOk(result); - if (userId === null) return ; +const Profile: React.FC<{ userId: string }> = ({ userId }) => { return ( diff --git a/frontend/src/pages/SessionDetail.tsx b/frontend/src/pages/SessionDetail.tsx index f4de2f0d..dd880cba 100644 --- a/frontend/src/pages/SessionDetail.tsx +++ b/frontend/src/pages/SessionDetail.tsx @@ -12,27 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; - -import { currentUserIdAtom } from "../atoms"; -import ErrorBoundary from "../components/ErrorBoundary"; -import GraphQLError from "../components/GraphQLError"; -import NotLoggedIn from "../components/NotLoggedIn"; import UserSessionDetail from "../components/SessionDetail"; -import { isErr, unwrapErr, unwrapOk } from "../result"; -const SessionDetail: React.FC<{ deviceId: string }> = ({ deviceId }) => { - const result = useAtomValue(currentUserIdAtom); - if (isErr(result)) return ; - - const userId = unwrapOk(result); - if (userId === null) return ; - - return ( - - - - ); +const SessionDetail: React.FC<{ userId: string; deviceId: string }> = ({ + userId, + deviceId, +}) => { + return ; }; export default SessionDetail; diff --git a/frontend/src/pages/SessionsOverview.tsx b/frontend/src/pages/SessionsOverview.tsx index 34f630c7..1e767bfa 100644 --- a/frontend/src/pages/SessionsOverview.tsx +++ b/frontend/src/pages/SessionsOverview.tsx @@ -12,16 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; -import { atomWithQuery } from "jotai-urql"; +import { useQuery } from "urql"; -import { mapQueryAtom } from "../atoms"; import ErrorBoundary from "../components/ErrorBoundary"; import GraphQLError from "../components/GraphQLError"; import NotLoggedIn from "../components/NotLoggedIn"; import UserSessionsOverview from "../components/UserSessionsOverview"; import { graphql } from "../gql"; -import { isErr, unwrapErr, unwrapOk } from "../result"; const QUERY = graphql(/* GraphQL */ ` query SessionsOverviewQuery { @@ -36,23 +33,13 @@ const QUERY = graphql(/* GraphQL */ ` } `); -const sessionsOverviewQueryAtom = atomWithQuery({ - query: QUERY, -}); - -const sessionsOverviewAtom = mapQueryAtom(sessionsOverviewQueryAtom, (data) => { - if (data.viewer?.__typename === "User") { - return data.viewer; - } - - return null; -}); - const SessionsOverview: React.FC = () => { - const result = useAtomValue(sessionsOverviewAtom); - if (isErr(result)) return ; + const [result] = useQuery({ query: QUERY }); + if (result.error) return ; + if (!result.data) throw new Error(); // Suspense mode is enabled - const data = unwrapOk(result); + const data = + result.data.viewer.__typename === "User" ? result.data.viewer : null; if (data === null) return ; return ( diff --git a/frontend/src/pages/VerifyEmail.tsx b/frontend/src/pages/VerifyEmail.tsx index 3922bf54..05648aef 100644 --- a/frontend/src/pages/VerifyEmail.tsx +++ b/frontend/src/pages/VerifyEmail.tsx @@ -12,17 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { useAtomValue } from "jotai"; -import { atomFamily } from "jotai/utils"; -import { atomWithQuery } from "jotai-urql"; import { useTranslation } from "react-i18next"; +import { useQuery } from "urql"; -import { mapQueryAtom } from "../atoms"; import ErrorBoundary from "../components/ErrorBoundary"; import GraphQLError from "../components/GraphQLError"; import VerifyEmailComponent from "../components/VerifyEmail"; import { graphql } from "../gql"; -import { isErr, unwrapErr, unwrapOk } from "../result"; const QUERY = graphql(/* GraphQL */ ` query VerifyEmailQuery($id: ID!) { @@ -32,27 +28,14 @@ const QUERY = graphql(/* GraphQL */ ` } `); -const verifyEmailFamily = atomFamily((id: string) => { - const verifyEmailQueryAtom = atomWithQuery({ - query: QUERY, - getVariables: () => ({ id }), - }); - - const verifyEmailAtom = mapQueryAtom( - verifyEmailQueryAtom, - (data) => data?.userEmail, - ); - - return verifyEmailAtom; -}); - const VerifyEmail: React.FC<{ id: string }> = ({ id }) => { - const result = useAtomValue(verifyEmailFamily(id)); + const [result] = useQuery({ query: QUERY, variables: { id } }); const { t } = useTranslation(); - if (isErr(result)) return ; + if (result.error) return ; + if (!result.data) throw new Error(); // Suspense mode is enabled - const email = unwrapOk(result); + const email = result.data.userEmail; if (email == null) return <>{t("frontend.verify_email.unknown_email")}; return ( diff --git a/frontend/src/routing/Router.tsx b/frontend/src/routing/Router.tsx index e13b72c2..879dd200 100644 --- a/frontend/src/routing/Router.tsx +++ b/frontend/src/routing/Router.tsx @@ -55,7 +55,7 @@ const unknownRoute = (route: never): never => { throw new Error(`Invalid route: ${JSON.stringify(route)}`); }; -const Router: React.FC = () => { +const Router: React.FC<{ userId: string }> = ({ userId }) => { const [route, redirecting] = useRouteWithRedirect(); const { t } = useTranslation(); @@ -65,13 +65,13 @@ const Router: React.FC = () => { switch (route.type) { case "profile": - return ; + return ; case "sessions-overview": return ; case "session": - return ; + return ; case "browser-session-list": - return ; + return ; case "client": return ; case "browser-session": diff --git a/frontend/src/utils/session/useCurrentBrowserSessionId.ts b/frontend/src/utils/session/useCurrentBrowserSessionId.ts index 019fad37..490ce20d 100644 --- a/frontend/src/utils/session/useCurrentBrowserSessionId.ts +++ b/frontend/src/utils/session/useCurrentBrowserSessionId.ts @@ -12,11 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CombinedError } from "@urql/core"; -import { useAtomValue } from "jotai"; +import { useQuery } from "urql"; -import { currentBrowserSessionIdAtom } from "../../atoms"; -import { isOk, unwrapOk, unwrapErr, isErr } from "../../result"; +import { graphql } from "../../gql"; + +const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ ` + query CurrentViewerSessionQuery { + viewerSession { + __typename + ... on BrowserSession { + id + } + } + } +`); /** * Query the current browser session id @@ -24,16 +33,10 @@ import { isOk, unwrapOk, unwrapErr, isErr } from "../../result"; * throws error when error result */ export const useCurrentBrowserSessionId = (): string | null => { - const currentSessionIdResult = useAtomValue(currentBrowserSessionIdAtom); - - if (isErr(currentSessionIdResult)) { - // eslint-disable-next-line no-throw-literal - throw unwrapErr(currentSessionIdResult) as Error; - } - - if (isOk(currentSessionIdResult)) { - return unwrapOk(currentSessionIdResult); - } - - return null; + const [result] = useQuery({ query: CURRENT_VIEWER_SESSION_QUERY }); + if (result.error) throw result.error; + if (!result.data) throw new Error(); // Suspense mode is enabled + return result.data.viewer.__typename === "User" + ? result.data.viewer.id + : null; };