diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx index 61f280c1d..98bcacf10 100644 --- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx @@ -1,7 +1,5 @@ import { useEffect, useState } from 'react'; -import Link from 'next/link'; - import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -22,6 +20,8 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog'; + import { useRequiredDocumentAuthContext } from './document-auth-provider'; export type DocumentActionAuth2FAProps = { @@ -58,6 +58,7 @@ export const DocumentActionAuth2FA = ({ }, }); + const [is2FASetupSuccessful, setIs2FASetupSuccessful] = useState(false); const [formErrorCode, setFormErrorCode] = useState(null); const onFormSubmit = async ({ token }: T2FAAuthFormSchema) => { @@ -87,17 +88,29 @@ export const DocumentActionAuth2FA = ({ token: '', }); + setIs2FASetupSuccessful(false); setFormErrorCode(null); - }, [open, form]); - if (!user?.twoFactorEnabled) { + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + if (!user?.twoFactorEnabled && !is2FASetupSuccessful) { return (
- {recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' - ? 'You need to setup 2FA to mark this document as viewed.' - : `You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`} +

+ {recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' + ? 'You need to setup 2FA to mark this document as viewed.' + : `You need to setup 2FA to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`} +

+ + {user?.identityProvider === 'DOCUMENSO' && ( +

+ By enabling 2FA, you will be required to enter a code from your authenticator app + every time you sign in. +

+ )}
@@ -106,9 +119,7 @@ export const DocumentActionAuth2FA = ({ Close - + setIs2FASetupSuccessful(true)} />
); diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx index fb4a14699..c09a60189 100644 --- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-account.tsx @@ -49,7 +49,7 @@ export const DocumentActionAuthAccount = ({ return (
- + {actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? ( diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx index ad8199b84..da1be1f38 100644 --- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-passkey.tsx @@ -137,10 +137,7 @@ export const DocumentActionAuthPasskey = ({ ); } - if ( - passkeyData.isInitialLoading || - (passkeyData.isRefetching && passkeyData.passkeys.length === 0) - ) { + if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) { return (
@@ -171,7 +168,7 @@ export const DocumentActionAuthPasskey = ({ if (passkeyData.passkeys.length === 0) { return (
- + {recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? 'You need to setup a passkey to mark this document as viewed.' diff --git a/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx b/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx index f9eda2564..86f673db0 100644 --- a/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/document-auth-provider.tsx @@ -5,7 +5,6 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { match } from 'ts-pattern'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; -import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth'; import type { TDocumentAuthOptions, TRecipientAccessAuthTypes, @@ -175,9 +174,11 @@ export const DocumentAuthProvider = ({ // 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( - DOCUMENT_AUTH_TYPES[derivedRecipientActionAuth || '']?.isAuthRedirectRequired && - !preCalculatedActionAuthOptions, + derivedRecipientActionAuth && + derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE && + user?.email !== recipient.email, ); const refetchPasskeys = async () => { diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 2b9b9d294..70897a716 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -42,10 +42,10 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin const { mutateAsync: completeDocumentWithToken } = trpc.recipient.completeDocumentWithToken.useMutation(); - const { - handleSubmit, - formState: { isSubmitting }, - } = useForm(); + const { handleSubmit, formState } = useForm(); + + // Keep the loading state going if successful since the redirect may take some time. + const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful; const uninsertedFields = useMemo(() => { return sortFieldsByPosition(fields.filter((field) => !field.inserted)); diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx index 0a6aac5dc..ce0b66ba4 100644 --- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx @@ -41,8 +41,13 @@ export const ZEnable2FAForm = z.object({ export type TEnable2FAForm = z.infer; -export const EnableAuthenticatorAppDialog = () => { +export type EnableAuthenticatorAppDialogProps = { + onSuccess?: () => void; +}; + +export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => { const { toast } = useToast(); + const router = useRouter(); const [isOpen, setIsOpen] = useState(false); @@ -79,6 +84,7 @@ export const EnableAuthenticatorAppDialog = () => { const data = await enable2FA({ code: token }); setRecoveryCodes(data.recoveryCodes); + onSuccess?.(); toast({ title: 'Two-factor authentication enabled', @@ -89,7 +95,7 @@ export const EnableAuthenticatorAppDialog = () => { toast({ title: 'Unable to setup two-factor authentication', description: - 'We were unable to setup two-factor authentication for your account. Please ensure that you have entered your password correctly and try again.', + 'We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.', variant: 'destructive', }); } diff --git a/packages/lib/constants/document-auth.ts b/packages/lib/constants/document-auth.ts index 7881c38f5..77c8d7b58 100644 --- a/packages/lib/constants/document-auth.ts +++ b/packages/lib/constants/document-auth.ts @@ -4,21 +4,12 @@ import { DocumentAuth } from '../types/document-auth'; type DocumentAuthTypeData = { key: TDocumentAuth; value: string; - - /** - * Whether this authentication event will require the user to halt and - * redirect. - * - * Defaults to false. - */ - isAuthRedirectRequired?: boolean; }; export const DOCUMENT_AUTH_TYPES: Record = { [DocumentAuth.ACCOUNT]: { key: DocumentAuth.ACCOUNT, value: 'Require account', - isAuthRedirectRequired: true, }, [DocumentAuth.PASSKEY]: { key: DocumentAuth.PASSKEY,