From ce683aa0e3883e673a301c27a38d94583750e2cf Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:50:19 +0200 Subject: [PATCH] Recovery Codes: improve code format (#50646) ## Problem 1. Recovery are displayed as returned by the backend image 2. Recovery codes are not displayed even if present when more than 1 MFA is set up ## Solution 1. Format them as uppercased groups of 4 characters image 3. Fix the condition check to display recovery codes ## How to test - Generate or regenerate your recovery codes: check the format is correct - If you haven't already, add a 2nd MFA: check recovery codes are still displayed ## Summary by CodeRabbit - **Improvements** - Recovery codes are now displayed in uppercase with clear hyphen-separated groups. - Copied recovery codes use the same formatted presentation for easier sharing and entry. - The recovery codes section is available whenever recovery codes are enabled, regardless of the number of authenticator apps configured. - Codes that do not match the expected format remain unchanged. - **Tests** - Added coverage to verify consistent recovery code formatting and clipboard behavior. --- .../GenerateRecoveryCodesModal.test.tsx | 27 ++++++++++++++----- .../TOTPFactors/RecoveryCodesModal.tsx | 14 ++++++---- .../RecoveryCodesModal.utils.test.ts | 12 +++++++++ .../TOTPFactors/RecoveryCodesModal.utils.ts | 11 ++++++++ .../RegenerateRecoveryCodesModal.test.tsx | 27 ++++++++++++++----- .../interfaces/Account/TOTPFactors/index.tsx | 2 +- 6 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.utils.test.ts create mode 100644 apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.utils.ts diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx index 4ac820f96a1..562ca916e2a 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/GenerateRecoveryCodesModal.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, expect, test, vi } from 'vitest' import { GenerateRecoveryCodesModal } from './GenerateRecoveryCodesModal' +import { formatRecoveryCode } from './RecoveryCodesModal.utils' import { auth } from '@/lib/gotrue' import { customRender } from '@/tests/lib/custom-render' @@ -16,7 +17,18 @@ vi.mock('ui', async (importOriginal) => ({ copyToClipboard: mockCopyToClipboard, })) -const codes = Array.from(Array(10).keys()).map((i) => `code_${i}`) +const codes = [ + 'wto24t5xbeulvjmi', + 'ade6in2sufndbbwd', + 'oge3npsrhrr66k25', + 'pxbkfvxlbi6ggvnf', + 'kc3oaioqdjn4htc3', + '363gseoknplambiy', + '3urgae2p2jegem4m', + 'tbxog2guayp6uvud', + 'rpcvcf4owbclxrfp', + 'kyyfsp3eqjydj53t', +] describe('GenerateRecoveryCodesModal', () => { test('generate the recovery codes and allow users to copy them', async () => { @@ -34,8 +46,8 @@ describe('GenerateRecoveryCodesModal', () => { // Codes are generated await screen.findByText('Save your recovery codes') - await screen.findByText('code_0') - await screen.findByText('code_9') + await screen.findByText('WTO2-4T5X-BEUL-VJMI') + await screen.findByText('KYYF-SP3E-QJYD-J53T') // Users have to copy the codes to close the modal, next click should fail if they managed to close it expect(await screen.findAllByRole('button', { name: 'Close' })).toHaveLength(1) @@ -44,7 +56,10 @@ describe('GenerateRecoveryCodesModal', () => { await waitFor(() => expect(screen.getByRole('checkbox', { name: 'I have copied the codes' })).toBeChecked() ) - expect(mockCopyToClipboard).toHaveBeenCalledWith(codes.join('\n'), expect.any(Function)) + expect(mockCopyToClipboard).toHaveBeenCalledWith( + codes.map((code) => formatRecoveryCode(code)).join('\n'), + expect.any(Function) + ) // We should have 2 close buttons (header icon and a standard button) expect(await screen.findAllByRole('button', { name: 'Close' })).toHaveLength(2) @@ -82,7 +97,7 @@ describe('GenerateRecoveryCodesModal', () => { fireEvent.click(await screen.findByRole('button', { name: 'Generate recovery codes' })) // Codes are generated await screen.findByText('Save your recovery codes') - await screen.findByText('code_0') - await screen.findByText('code_9') + await screen.findByText('WTO2-4T5X-BEUL-VJMI') + await screen.findByText('KYYF-SP3E-QJYD-J53T') }) }) diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.tsx index 4efec68cf56..c16cb52bfa3 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/RecoveryCodesModal.tsx @@ -14,6 +14,7 @@ import { DialogTitle, } from 'ui' +import { formatRecoveryCode } from './RecoveryCodesModal.utils' import { recoveryCodeKeys } from '@/data/recovery-codes/keys' interface RecoveryCodesModalProps @@ -82,10 +83,13 @@ export const RecoveryCodesModal = ({