import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { type Envelope, FieldType, type Passkey, type Recipient } from '@prisma/client'; import type { SessionUser } from '@documenso/auth/server/lib/session/session'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import type { TDocumentAuthOptions, TRecipientAccessAuthTypes, TRecipientActionAuthTypes, TRecipientAuthOptions, } from '@documenso/lib/types/document-auth'; import { DocumentAuth } from '@documenso/lib/types/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { trpc } from '@documenso/trpc/react'; import type { DocumentSigningAuthDialogProps } from './document-signing-auth-dialog'; import { DocumentSigningAuthDialog } from './document-signing-auth-dialog'; type PasskeyData = { passkeys: Omit[]; isInitialLoading: boolean; isRefetching: boolean; isError: boolean; }; type SigningAuthRecipient = Pick< Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id' >; export type DocumentSigningAuthContextValue = { executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise; documentAuthOptions: Envelope['authOptions']; documentAuthOption: TDocumentAuthOptions; setDocumentAuthOptions: (_value: Envelope['authOptions']) => void; recipient: SigningAuthRecipient; recipientAuthOption: TRecipientAuthOptions; setRecipient: (_value: SigningAuthRecipient) => void; derivedRecipientAccessAuth: TRecipientAccessAuthTypes[]; derivedRecipientActionAuth: TRecipientActionAuthTypes[]; isAuthRedirectRequired: boolean; isCurrentlyAuthenticating: boolean; setIsCurrentlyAuthenticating: (_value: boolean) => void; passkeyData: PasskeyData; preferredPasskeyId: string | null; setPreferredPasskeyId: (_value: string | null) => void; user?: SessionUser | null; refetchPasskeys: () => Promise; }; const DocumentSigningAuthContext = createContext(null); export const useDocumentSigningAuthContext = () => { return useContext(DocumentSigningAuthContext); }; export const useRequiredDocumentSigningAuthContext = () => { const context = useDocumentSigningAuthContext(); if (!context) { throw new Error('Document signing auth context is required'); } return context; }; export interface DocumentSigningAuthProviderProps { documentAuthOptions: Envelope['authOptions']; recipient: SigningAuthRecipient; user?: SessionUser | null; children: React.ReactNode; } export const DocumentSigningAuthProvider = ({ documentAuthOptions: initialDocumentAuthOptions, recipient: initialRecipient, user, children, }: DocumentSigningAuthProviderProps) => { const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions); const [recipient, setRecipient] = useState(initialRecipient); const [isCurrentlyAuthenticating, setIsCurrentlyAuthenticating] = useState(false); const [preferredPasskeyId, setPreferredPasskeyId] = useState(null); const { documentAuthOption, recipientAuthOption, derivedRecipientAccessAuth, derivedRecipientActionAuth, } = useMemo( () => extractDocumentAuthMethods({ documentAuth: documentAuthOptions, recipientAuth: recipient.authOptions, }), [documentAuthOptions, recipient], ); const passkeyQuery = trpc.auth.passkey.find.useQuery( { perPage: MAXIMUM_PASSKEYS, }, { placeholderData: (previousData) => previousData, enabled: derivedRecipientActionAuth?.includes(DocumentAuth.PASSKEY) ?? false, }, ); const passkeyData: PasskeyData = { passkeys: passkeyQuery.data?.data || [], isInitialLoading: passkeyQuery.isInitialLoading, isRefetching: passkeyQuery.isRefetching, isError: passkeyQuery.isError, }; const [documentAuthDialogPayload, setDocumentAuthDialogPayload] = useState(null); /** * The pre calculated auth payload if the current user is authenticated correctly * for the `derivedRecipientActionAuth`. * * Will be `null` if the user still requires authentication, or if they don't need * authentication. */ const preCalculatedActionAuthOptions = useMemo(() => { if ( !derivedRecipientActionAuth || derivedRecipientActionAuth.length === 0 || derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) ) { return { type: DocumentAuth.EXPLICIT_NONE, }; } if ( derivedRecipientActionAuth.includes(DocumentAuth.ACCOUNT) && user?.email == recipient.email ) { return { type: DocumentAuth.ACCOUNT, }; } return null; }, [derivedRecipientActionAuth, user, recipient]); const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => { // Directly run callback if no auth required. if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) { await options.onReauthFormSubmit(); return; } // Run callback with precalculated auth options if available. if (preCalculatedActionAuthOptions) { setDocumentAuthDialogPayload(null); await options.onReauthFormSubmit(preCalculatedActionAuthOptions); return; } // Request the required auth from the user. setDocumentAuthDialogPayload({ ...options, }); }; useEffect(() => { const { passkeys } = passkeyData; if (!preferredPasskeyId && passkeys.length > 0) { setPreferredPasskeyId(passkeys[0].id); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [passkeyData.passkeys]); // Assume that a user must be logged in for any auth requirements. const isAuthRedirectRequired = Boolean( derivedRecipientActionAuth && derivedRecipientActionAuth.length > 0 && !derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) && user?.email !== recipient.email, ); const refetchPasskeys = async () => { await passkeyQuery.refetch(); }; return ( {children} {documentAuthDialogPayload && derivedRecipientActionAuth && ( setDocumentAuthDialogPayload(null)} onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit} actionTarget={documentAuthDialogPayload.actionTarget} availableAuthTypes={derivedRecipientActionAuth} /> )} ); }; type ExecuteActionAuthProcedureOptions = Omit< DocumentSigningAuthDialogProps, 'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes' >; DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';