fix(studio): clarify SMTP password field UX when a password is already saved (#50794)

## What
The custom SMTP password field showed a "Reveal" and "Copy" button next
to text saying "this password cannot be viewed once saved" —
contradictory, since those buttons implied there was something to reveal
or copy. In reality the backend never returns the saved password, so the
field was always blank and those buttons acted on an empty string.

## Why
Reported in FE-3765: users found it confusing whether saving other
fields would blank out their password, and the Reveal/Copy buttons
appeared broken.

## Fix
- Removed the non-functional Reveal/Copy buttons from the password
input.
- When a password is already saved, the field now shows a
`••••••••••••••••` placeholder and copy reading "Stored password is
hidden. Enter a new password to replace it." — matching the existing
`STORED_SECRET_PLACEHOLDER` pattern already used in the Replication
destination forms (BigQuery, ClickHouse, Snowflake, etc).
- No behavior change: leaving the field blank on save still preserves
the existing password (unchanged logic).

## Testing
- Manually verified in the running app.
- Added a component test (`SmtpForm.test.tsx`) covering both the
"password already saved" and "fresh setup" states.
- `tsc --noEmit`, `eslint`, and `prettier --check` all pass with no new
errors/warnings.

Fixes
[FE-3765](https://linear.app/supabase/issue/FE-3765/custom-smtp-password-field-ux-issues)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Updates**
* Configured SMTP passwords are masked, with a notice that entering a
new password will replace the stored one.
* For new SMTP setups, the password field prompts for the SMTP server
password and does not show the stored-password notice.
* The SMTP password field no longer provides controls to reveal or copy
the password.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Monica Khoury authored and GitHub committed 2026-09-23 16:40:53 +03:00
1 parent f3094a29ce
commit e143c94e5f
3 files changed
+92 -4

No files matched your search

@@ -1,3 +1,5 @@
export const STORED_SECRET_PLACEHOLDER = '••••••••••••••••'
export const defaultDisabledSmtpFormValues = {
SMTP_ADMIN_EMAIL: null,
SMTP_SENDER_NAME: null,
@@ -0,0 +1,69 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { SmtpForm } from './SmtpForm'
import type { components } from '@/data/api'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
type AuthConfigResponse = components['schemas']['GoTrueConfigResponse']
vi.mock('@/lib/constants', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/constants')>()
return { ...actual, IS_PLATFORM: true }
})
vi.mock('@/hooks/misc/useSelectedProject', () => ({
useSelectedProjectQuery: () => ({ data: { ref: 'default', inserted_at: null } }),
}))
vi.mock('@/hooks/misc/useCheckPermissions', () => ({
useAsyncCheckPermissions: () => ({ can: true, isLoading: false, isSuccess: true }),
}))
const SMTP_CONFIGURED_CONFIG = {
SMTP_ADMIN_EMAIL: 'admin@example.com',
SMTP_SENDER_NAME: 'Example',
SMTP_USER: 'smtp-user',
SMTP_HOST: 'smtp.example.com',
SMTP_PASS: 'stored-password-marker',
SMTP_PORT: '587',
SMTP_MAX_FREQUENCY: 60,
}
function mockAuthConfig(overrides: Partial<AuthConfigResponse>) {
addAPIMock({
method: 'get',
path: '/platform/auth/:ref/config',
response: () => HttpResponse.json<AuthConfigResponse>({ ...overrides } as AuthConfigResponse),
})
}
describe('SmtpForm', () => {
test('shows a hidden-password placeholder when a password is already saved', async () => {
mockAuthConfig(SMTP_CONFIGURED_CONFIG)
customRender(<SmtpForm />)
expect(
await screen.findByText('Stored password is hidden. Enter a new password to replace it.')
).toBeInTheDocument()
expect(screen.getByPlaceholderText('••••••••••••••••')).toBeInTheDocument()
})
test('does not show the hidden-password copy or placeholder for a fresh, never-configured setup', async () => {
mockAuthConfig({})
customRender(<SmtpForm />)
await userEvent.click(await screen.findByLabelText('Toggle SMTP'))
expect(await screen.findByText('Password for your SMTP server.')).toBeInTheDocument()
expect(screen.queryByPlaceholderText('••••••••••••••••')).not.toBeInTheDocument()
expect(
screen.queryByText('Stored password is hidden. Enter a new password to replace it.')
).not.toBeInTheDocument()
})
})
@@ -20,7 +20,6 @@ import {
Switch,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import * as z from 'zod'
@@ -29,7 +28,7 @@ import { urlRegex } from '../Auth.constants'
import { AUTH_TEMPLATE_RESET_TYPES } from '../EmailTemplates/EmailTemplates.constants'
import { isBeforeFreeTierTemplateBlockCutoff } from '../EmailTemplates/EmailTemplates.utils'
import { SmtpDisableConfirmationDialog } from './SmtpDisableConfirmationDialog'
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
import { defaultDisabledSmtpFormValues, STORED_SECRET_PLACEHOLDER } from './SmtpForm.constants'
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
import { AlertError } from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
@@ -474,10 +473,28 @@ export const SmtpForm = () => {
render={({ field }) => (
<FormItemLayout
label="Password"
description="Password for your SMTP server. For security reasons, this password cannot be viewed once saved."
description={
isSmtpEnabled(authConfig)
? 'Stored password is hidden. Enter a new password to replace it.'
: 'Password for your SMTP server.'
}
>
<FormControl>
<PasswordInput {...field} reveal copy disabled={!canUpdateConfig} />
<Input
{...field}
type="password"
autoComplete="new-password"
data-1p-ignore
data-lpignore="true"
data-form-type="other"
data-bwignore
placeholder={
isSmtpEnabled(authConfig)
? STORED_SECRET_PLACEHOLDER
: undefined
}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}