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 7a181c4cc..27560c073 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 @@ -1,14 +1,12 @@ import { useMemo } from 'react'; -import { useRouter } from 'next/navigation'; - import { zodResolver } from '@hookform/resolvers/zod'; -import { flushSync } from 'react-dom'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { renderSVG } from 'uqr'; import { z } from 'zod'; +import { downloadFile } from '@documenso/lib/client-only/download-file'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -54,14 +52,16 @@ export const EnableAuthenticatorAppDialog = ({ open, onOpenChange, }: EnableAuthenticatorAppDialogProps) => { - const router = useRouter(); const { toast } = useToast(); const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } = trpc.twoFactorAuthentication.setup.useMutation(); - const { mutateAsync: enableTwoFactorAuthentication, data: enableTwoFactorAuthenticationData } = - trpc.twoFactorAuthentication.enable.useMutation(); + const { + mutateAsync: enableTwoFactorAuthentication, + data: enableTwoFactorAuthenticationData, + isLoading: isEnableTwoFactorAuthenticationDataLoading, + } = trpc.twoFactorAuthentication.enable.useMutation(); const setupTwoFactorAuthenticationForm = useForm({ defaultValues: { @@ -115,6 +115,19 @@ export const EnableAuthenticatorAppDialog = ({ } }; + const downloadRecoveryCodes = () => { + if (enableTwoFactorAuthenticationData && enableTwoFactorAuthenticationData.recoveryCodes) { + const blob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], { + type: 'text/plain', + }); + + downloadFile({ + filename: 'documenso-2FA-recovery-codes.txt', + data: blob, + }); + } + }; + const onEnableTwoFactorAuthenticationFormSubmit = async ({ token, }: TEnableTwoFactorAuthenticationForm) => { @@ -136,14 +149,6 @@ export const EnableAuthenticatorAppDialog = ({ } }; - const onCompleteClick = () => { - flushSync(() => { - onOpenChange(false); - }); - - router.refresh(); - }; - return ( @@ -270,9 +275,16 @@ export const EnableAuthenticatorAppDialog = ({ )} -
- + +
diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 18714332a..376a8939c 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -5,6 +5,7 @@ import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { z } from 'zod'; +import { downloadFile } from '@documenso/lib/client-only/download-file'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -42,8 +43,11 @@ export type ViewRecoveryCodesDialogProps = { export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => { const { toast } = useToast(); - const { mutateAsync: viewRecoveryCodes, data: viewRecoveryCodesData } = - trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); + const { + mutateAsync: viewRecoveryCodes, + data: viewRecoveryCodesData, + isLoading: isViewRecoveryCodesDataLoading, + } = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation(); const viewRecoveryCodesForm = useForm({ defaultValues: { @@ -62,6 +66,19 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode return 'view'; }, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]); + const downloadRecoveryCodes = () => { + if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) { + const blob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], { + type: 'text/plain', + }); + + downloadFile({ + filename: 'documenso-2FA-recovery-codes.txt', + data: blob, + }); + } + }; + const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => { try { await viewRecoveryCodes({ password }); @@ -139,8 +156,17 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode )} -
+
+ +
)) diff --git a/packages/lib/client-only/download-file.ts b/packages/lib/client-only/download-file.ts new file mode 100644 index 000000000..36351bedc --- /dev/null +++ b/packages/lib/client-only/download-file.ts @@ -0,0 +1,19 @@ +export type DownloadFileOptions = { + filename: string; + data: Blob; +}; + +export const downloadFile = ({ filename, data }: DownloadFileOptions) => { + if (typeof window === 'undefined') { + throw new Error('downloadFile can only be called in browser environments'); + } + + const link = window.document.createElement('a'); + + link.href = window.URL.createObjectURL(data); + link.download = filename; + + link.click(); + + window.URL.revokeObjectURL(link.href); +}; diff --git a/packages/lib/client-only/download-pdf.ts b/packages/lib/client-only/download-pdf.ts index ec7d0c252..0f757c98d 100644 --- a/packages/lib/client-only/download-pdf.ts +++ b/packages/lib/client-only/download-pdf.ts @@ -1,6 +1,7 @@ import type { DocumentData } from '@documenso/prisma/client'; import { getFile } from '../universal/upload/get-file'; +import { downloadFile } from './download-file'; type DownloadPDFProps = { documentData: DocumentData; @@ -14,16 +15,12 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps) type: 'application/pdf', }); - const link = window.document.createElement('a'); - const [baseTitle] = fileName?.includes('.pdf') ? fileName.split('.pdf') : [fileName ?? 'document']; - link.href = window.URL.createObjectURL(blob); - link.download = `${baseTitle}_signed.pdf`; - - link.click(); - - window.URL.revokeObjectURL(link.href); + downloadFile({ + filename: baseTitle, + data: blob, + }); };