1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-23 11:02:35 +03:00

WIP: start replacing jotai-urql with urql

This commit is contained in:
Quentin Gliech
2024-02-09 20:33:30 +01:00
parent 3d90d0861c
commit 1b75b3b185
19 changed files with 192 additions and 354 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<string | null> = 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<string | null> = 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;
};

View File

@@ -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<void>) => {
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<void> => {
await endSession();
if (isCurrent) {
currentBrowserSessionId({
requestPolicy: "network-only",
});
currentUserId({
requestPolicy: "network-only",
});
window.location.reload();
}
}, [isCurrent, endSession, currentBrowserSessionId, currentUserId]);
}, [isCurrent, endSession]);
return onSessionEnd;
};

View File

@@ -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("<Layout />", () => {
beforeEach(() => {
vi.spyOn(currentUserIdAtom, "read").mockResolvedValue(
"abc123" as unknown as GqlResult<string | null>,
);
});
afterAll(() => {
vi.restoreAllMocks();
});
it("renders app navigation correctly", async () => {
const component = render(
<WithLocation path="/">
<Layout />
<Layout userId="abc123" />
</WithLocation>,
);

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
// 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 (
<div className={styles.container}>
<NotLoggedIn />
</div>
);
return (
<div className={styles.layoutContainer}>
{shouldHideNavBar ? null : (

View File

@@ -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] ?? {};

View File

@@ -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
>;

View File

@@ -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,
});

View File

@@ -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 <NotLoggedIn />;
return (
<Layout userId={userId}>
<Suspense fallback={<LoadingSpinner />}>
<Router userId={userId} />
</Suspense>
</Layout>
);
};
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<Provider>
{import.meta.env.DEV && <DevTools />}
<HydrateAtoms>
<Suspense fallback={<LoadingScreen />}>
<I18nextProvider i18n={i18n}>
<TooltipProvider>
<Layout>
<Suspense fallback={<LoadingSpinner />}>
<Router />
</Suspense>
</Layout>
</TooltipProvider>
</I18nextProvider>
</Suspense>
</HydrateAtoms>
</Provider>
<ErrorBoundary>
<UrqlProvider value={client}>
<Provider>
{import.meta.env.DEV && <DevTools />}
<HydrateAtoms>
<Suspense fallback={<LoadingScreen />}>
<I18nextProvider i18n={i18n}>
<TooltipProvider>
<App />
</TooltipProvider>
</I18nextProvider>
</Suspense>
</HydrateAtoms>
</Provider>
</UrqlProvider>
</ErrorBoundary>
</StrictMode>,
);

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const [result] = useQuery({
query: QUERY,
variables: { id },
});
if (result.error) return <GraphQLError error={result.error} />;
if (!result.data) throw new Error(); // Suspense mode is enabled
const browserSession = unwrapOk(result);
const browserSession = result.data.browserSession;
if (!browserSession) return <NotFound />;
return (

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return (
<ErrorBoundary>
<List userId={userId} />
</ErrorBoundary>
);
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
return <List userId={userId} />;
};
export default BrowserSessionList;

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const [result] = useQuery({
query: QUERY,
variables: { id },
});
if (result.error) return <GraphQLError error={result.error} />;
if (!result.data) throw new Error(); // Suspense mode is enabled
const oauth2Client = unwrapOk(result);
const oauth2Client = result.data.oauth2Client;
if (!oauth2Client) return <NotFound />;
return (

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
const Profile: React.FC<{ userId: string }> = ({ userId }) => {
return (
<ErrorBoundary>
<UserProfile userId={userId} />

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return (
<ErrorBoundary>
<UserSessionDetail userId={userId} deviceId={deviceId} />
</ErrorBoundary>
);
const SessionDetail: React.FC<{ userId: string; deviceId: string }> = ({
userId,
deviceId,
}) => {
return <UserSessionDetail userId={userId} deviceId={deviceId} />;
};
export default SessionDetail;

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
const [result] = useQuery({ query: QUERY });
if (result.error) return <GraphQLError error={result.error} />;
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 <NotLoggedIn />;
return (

View File

@@ -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 <GraphQLError error={unwrapErr(result)} />;
if (result.error) return <GraphQLError error={result.error} />;
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 (

View File

@@ -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 <Profile />;
return <Profile userId={userId} />;
case "sessions-overview":
return <SessionsOverview />;
case "session":
return <SessionDetail deviceId={route.id} />;
return <SessionDetail userId={userId} deviceId={route.id} />;
case "browser-session-list":
return <BrowserSessionList />;
return <BrowserSessionList userId={userId} />;
case "client":
return <OAuth2Client id={route.id} />;
case "browser-session":

View File

@@ -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<CombinedError>(currentSessionIdResult) as Error;
}
if (isOk<string | null, unknown>(currentSessionIdResult)) {
return unwrapOk<string | null>(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;
};