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..323bc7198 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
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
@@ -41,6 +41,7 @@ export type ViewRecoveryCodesDialogProps = {
export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => {
const { toast } = useToast();
+ const [recoveryCodesUrl, setRecoveryCodesUrl] = useState('');
const { mutateAsync: viewRecoveryCodes, data: viewRecoveryCodesData } =
trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
@@ -62,6 +63,16 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
return 'view';
}, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]);
+ useEffect(() => {
+ if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) {
+ const textBlob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], {
+ type: 'text/plain',
+ });
+ if (recoveryCodesUrl) URL.revokeObjectURL(recoveryCodesUrl);
+ setRecoveryCodesUrl(URL.createObjectURL(textBlob));
+ }
+ }, [viewRecoveryCodesData]);
+
const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => {
try {
await viewRecoveryCodes({ password });
@@ -139,8 +150,11 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode