diff --git a/apps/trust/src/components/AuditRow.tsx b/apps/trust/src/components/AuditRow.tsx index bd7a13a16..4e436cae6 100644 --- a/apps/trust/src/components/AuditRow.tsx +++ b/apps/trust/src/components/AuditRow.tsx @@ -21,7 +21,6 @@ import { downloadFile, formatError } from "@probo/helpers"; import { type PropsWithChildren, use, useState } from "react"; import { useLocation } from "react-router"; import { Viewer } from "/providers/Viewer"; -import { MagicLinkDialog } from "./MagicLinkDialog"; import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql"; const requestAccessMutation = graphql` @@ -150,15 +149,14 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) { {hasRequested ? __("Access requested") : __("Request access")} ) : ( - - - {hasRequested ? __("Access requested") : __("Request access")} - - + + {hasRequested ? __("Access requested") : __("Request access")} + )} ); diff --git a/apps/trust/src/components/DocumentRow.tsx b/apps/trust/src/components/DocumentRow.tsx index f302c1fb3..a4d69ccda 100644 --- a/apps/trust/src/components/DocumentRow.tsx +++ b/apps/trust/src/components/DocumentRow.tsx @@ -14,7 +14,6 @@ import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDow import { useMutationWithToasts } from "/hooks/useMutationWithToast"; import { downloadFile, formatError } from "@probo/helpers"; import { use, useState } from "react"; -import { MagicLinkDialog } from "./MagicLinkDialog"; import { Viewer } from "/providers/Viewer"; import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql"; @@ -135,15 +134,14 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) { {hasRequested ? __("Access requested") : __("Request access")} ) : ( - - - {hasRequested ? __("Access requested") : __("Request access")} - - + + {hasRequested ? __("Access requested") : __("Request access")} + )} ); diff --git a/apps/trust/src/components/MagicLinkDialog.tsx b/apps/trust/src/components/MagicLinkDialog.tsx deleted file mode 100644 index d79e19ac2..000000000 --- a/apps/trust/src/components/MagicLinkDialog.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { - Button, - Dialog, - DialogContent, - DialogFooter, - DialogTitle, - Field, - useDialogRef, - useToast, -} from "@probo/ui"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; -import z from "zod"; -import { useFormWithSchema } from "/hooks/useFormWithSchema"; -import { useTranslate } from "@probo/i18n"; -import type { MagicLinkDialogMutation } from "./__generated__/MagicLinkDialogMutation.graphql"; -import type { PropsWithChildren } from "react"; - -const sendMagicLinkMutation = graphql` - mutation MagicLinkDialogMutation($input: SendMagicLinkInput!) { - sendMagicLink(input: $input) { - success - } - } -`; - -const schema = z.object({ - email: z.string().email(), -}); - -type FormData = z.infer; - -export function MagicLinkDialog(props: PropsWithChildren) { - const { children } = props; - - const { __ } = useTranslate(); - const { toast } = useToast(); - const dialogRef = useDialogRef(); - - const [sendMagicLink] = useMutation( - sendMagicLinkMutation, - ); - - const { - handleSubmit: handleSubmitWrapper, - register, - formState, - } = useFormWithSchema(schema, { - defaultValues: { - email: "", - }, - }); - - const handleSubmit = ({ email }: FormData) => { - sendMagicLink({ - variables: { - input: { - email, - }, - }, - onCompleted: (_, errors) => { - if (errors?.length) { - toast({ - title: __("Error"), - description: __("Cannot send magic link"), - variant: "error", - }); - return; - } - - toast({ - title: __("Success"), - description: __( - "Magic link sent! Please check your emails to authenticate", - ), - variant: "success", - }); - dialogRef.current?.close(); - }, - onError: (error) => { - toast({ - title: __("Error"), - description: error.message, - variant: "error", - }); - }, - }); - }; - - return ( - - - - {__("Create account or sign in")} - - - - {__( - "To be able to request access to documents, you need to authenticate. Please enter your email to get a magic link.", - )} - - - - - - {__("Continue")} - - - - - ); -} diff --git a/apps/trust/src/components/OrganizationSidebar.tsx b/apps/trust/src/components/OrganizationSidebar.tsx index b62259f6d..0bedfa684 100644 --- a/apps/trust/src/components/OrganizationSidebar.tsx +++ b/apps/trust/src/components/OrganizationSidebar.tsx @@ -11,7 +11,6 @@ import { use, type PropsWithChildren } from "react"; import { domain, formatError } from "@probo/helpers"; import { AuditRowAvatar } from "./AuditRow"; import { Viewer } from "/providers/Viewer"; -import { MagicLinkDialog } from "./MagicLinkDialog"; import { graphql } from "relay-runtime"; import { useMutation } from "react-relay"; import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql"; @@ -160,15 +159,15 @@ export function OrganizationSidebar({ {__("Request access")} ) : ( - - - {__("Request access")} - - + + {__("Request access")} + )} - {/* - {__("Subscribe to updates")} - */} ); diff --git a/apps/trust/src/components/TrustCenterFileRow.tsx b/apps/trust/src/components/TrustCenterFileRow.tsx index 8fa5e69a8..2a7143bcc 100644 --- a/apps/trust/src/components/TrustCenterFileRow.tsx +++ b/apps/trust/src/components/TrustCenterFileRow.tsx @@ -15,7 +15,6 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToast"; import { downloadFile, formatError } from "@probo/helpers"; import { use, useState } from "react"; import { Viewer } from "/providers/Viewer"; -import { MagicLinkDialog } from "./MagicLinkDialog"; import type { TrustCenterFileRow_requestAccessMutation } from "./__generated__/TrustCenterFileRow_requestAccessMutation.graphql"; const requestAccessMutation = graphql` @@ -139,15 +138,14 @@ export function TrustCenterFileRow(props: { {hasRequested ? __("Access requested") : __("Request access")} ) : ( - - - {hasRequested ? __("Access requested") : __("Request access")} - - + + {hasRequested ? __("Access requested") : __("Request access")} + )} ); diff --git a/apps/trust/src/pages/auth/ConnectPage.tsx b/apps/trust/src/pages/auth/ConnectPage.tsx new file mode 100644 index 000000000..46b361789 --- /dev/null +++ b/apps/trust/src/pages/auth/ConnectPage.tsx @@ -0,0 +1,173 @@ +import { usePageTitle } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; +import { Button, Field, useToast } from "@probo/ui"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import z from "zod"; +import { graphql } from "relay-runtime"; +import { + useMutation, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import { AuthLayout } from "./AuthLayout"; +import type { ConnectPageMutation } from "./__generated__/ConnectPageMutation.graphql"; +import { useEffect, useRef, useState } from "react"; +import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql"; + +export const connectPageQuery = graphql` + query ConnectPageQuery { + currentTrustCenter @required(action: THROW) { + organization @required(action: THROW) { + name + } + } + } +`; + +const sendMagicLinkMutation = graphql` + mutation ConnectPageMutation($input: SendMagicLinkInput!) { + sendMagicLink(input: $input) { + success + } + } +`; + +const schema = z.object({ + email: z.string().email(), +}); + +type FormData = z.infer; + +const timerDurationSeconds = 60; + +export function ConnectPage(props: { + queryRef: PreloadedQuery; +}) { + const { queryRef } = props; + + const { __ } = useTranslate(); + const { toast } = useToast(); + const [magicLinkSent, setMagicLinkSent] = useState(false); + const interval = useRef(undefined); + const [timer, setTimer] = useState(timerDurationSeconds); + + const { + currentTrustCenter: { organization }, + } = usePreloadedQuery(connectPageQuery, queryRef); + + useEffect(() => { + if (!magicLinkSent && interval.current) { + clearInterval(interval.current); + interval.current = undefined; + setTimer(timerDurationSeconds); + } + if (magicLinkSent) { + clearInterval(interval.current); + interval.current = setInterval(() => { + setTimer((timer) => Math.max(timer - 1, 0)); + }, 1000); + } + + return () => { + clearInterval(interval.current); + }; + }, [magicLinkSent]); + + usePageTitle(__("Connect to Probo")); + + const { + handleSubmit: handleSubmitWrapper, + register, + formState, + } = useFormWithSchema(schema, { + defaultValues: { + email: "", + }, + }); + + const [sendMagicLink] = useMutation( + sendMagicLinkMutation, + ); + + const handleSubmit = handleSubmitWrapper(({ email }: FormData) => { + sendMagicLink({ + variables: { + input: { + email, + }, + }, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: __("Cannot send magic link"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Magic link sent!"), + variant: "success", + }); + setTimer(timerDurationSeconds); + setMagicLinkSent(true); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message, + variant: "error", + }); + }, + }); + }); + + return ( + + + + + {__(`Connect to ${organization.name}'s compliance page`)} + + + {__( + "Enter your email address to connect with a magic link and start requesting access to documents", + )} + + + + + + + {magicLinkSent && ( + + {__( + "Magic Link Sent! Check your emails and use the link to connect.", + )} + + )} + + + {magicLinkSent + ? timer === 0 + ? __("Resend Link") + : `${__("Resend Link in")} ${timer}s` + : __("Send Magic Link")} + + + + + ); +} diff --git a/apps/trust/src/pages/auth/ConnectPageLoader.tsx b/apps/trust/src/pages/auth/ConnectPageLoader.tsx new file mode 100644 index 000000000..e3d8e8511 --- /dev/null +++ b/apps/trust/src/pages/auth/ConnectPageLoader.tsx @@ -0,0 +1,32 @@ +import { useQueryLoader } from "react-relay"; +import { ConnectPage, connectPageQuery } from "./ConnectPage"; +import { Suspense, useEffect } from "react"; +import { RelayProvider } from "/providers/RelayProviders"; +import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql"; + +function ConnectPageLoader() { + const [queryRef, loadQuery] = + useQueryLoader(connectPageQuery); + + useEffect(() => { + if (!queryRef) { + loadQuery({}); + } + }); + + if (!queryRef) return null; + + return ( + + + + ); +} + +export default function () { + return ( + + + + ); +} diff --git a/apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts b/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts similarity index 68% rename from apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts rename to apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts index 21fc17849..b420a2bc8 100644 --- a/apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts +++ b/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<1d6509148442dad28e2af20068216f49>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,17 +12,17 @@ import { ConcreteRequest } from 'relay-runtime'; export type SendMagicLinkInput = { email: any; }; -export type MagicLinkDialogMutation$variables = { +export type ConnectPageMutation$variables = { input: SendMagicLinkInput; }; -export type MagicLinkDialogMutation$data = { +export type ConnectPageMutation$data = { readonly sendMagicLink: { readonly success: boolean; } | null | undefined; }; -export type MagicLinkDialogMutation = { - response: MagicLinkDialogMutation$data; - variables: MagicLinkDialogMutation$variables; +export type ConnectPageMutation = { + response: ConnectPageMutation$data; + variables: ConnectPageMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -64,7 +64,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "MagicLinkDialogMutation", + "name": "ConnectPageMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -73,20 +73,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "MagicLinkDialogMutation", + "name": "ConnectPageMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "d0e02db0cec956d7a21f5ad5a5a69d61", + "cacheID": "95987f897611118fa771a2e54cb43b66", "id": null, "metadata": {}, - "name": "MagicLinkDialogMutation", + "name": "ConnectPageMutation", "operationKind": "mutation", - "text": "mutation MagicLinkDialogMutation(\n $input: SendMagicLinkInput!\n) {\n sendMagicLink(input: $input) {\n success\n }\n}\n" + "text": "mutation ConnectPageMutation(\n $input: SendMagicLinkInput!\n) {\n sendMagicLink(input: $input) {\n success\n }\n}\n" } }; })(); -(node as any).hash = "c1ee1fe3ef7c232fcb43b5cf8e56c2de"; +(node as any).hash = "4680fea978582bb020b4613737c14d9a"; export default node; diff --git a/apps/trust/src/pages/auth/__generated__/ConnectPageQuery.graphql.ts b/apps/trust/src/pages/auth/__generated__/ConnectPageQuery.graphql.ts new file mode 100644 index 000000000..fcfb86e2f --- /dev/null +++ b/apps/trust/src/pages/auth/__generated__/ConnectPageQuery.graphql.ts @@ -0,0 +1,128 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ConnectPageQuery$variables = Record; +export type ConnectPageQuery$data = { + readonly currentTrustCenter: { + readonly organization: { + readonly name: string; + }; + }; +}; +export type ConnectPageQuery = { + response: ConnectPageQuery$data; + variables: ConnectPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "ConnectPageQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "TrustCenter", + "kind": "LinkedField", + "name": "currentTrustCenter", + "plural": false, + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "ConnectPageQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenter", + "kind": "LinkedField", + "name": "currentTrustCenter", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "c0fceafb8e47a9434fb90040efcac8d7", + "id": null, + "metadata": {}, + "name": "ConnectPageQuery", + "operationKind": "query", + "text": "query ConnectPageQuery {\n currentTrustCenter {\n organization {\n name\n id\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "bc5ac3e0b5b3560f9e9240fe79032c6b"; + +export default node; diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index 063a7d53f..0603b9b99 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -31,6 +31,10 @@ function ErrorBoundary({ error: propsError }: { error?: string }) { } const routes = [ + { + path: "/connect", + Component: lazy(() => import("./pages/auth/ConnectPageLoader.tsx")), + }, { path: "/verify-magic-link", Component: lazy(() => import("./pages/auth/VerifyMagicLinkPage.tsx")),
- {__( - "To be able to request access to documents, you need to authenticate. Please enter your email to get a magic link.", - )} -
+ {__( + "Enter your email address to connect with a magic link and start requesting access to documents", + )} +
+ {__( + "Magic Link Sent! Check your emails and use the link to connect.", + )} +