Files
supabase/apps/studio/components/interfaces/Auth/Hooks/DeleteHookConfirmationDialog.tsx
Danny White 8192d97008 feat: surface send-email hook status in template UI and hook deletion dialog (#46319)
## What kind of change does this PR introduce?

Feature. Implements
[AUTH-1215](https://linear.app/supabase/issue/AUTH-1215/improve-studio-ui-when-send-email-hook-is-active-or-deleted).
Follow-up to #45396.

## What is the current behavior?

When a send-email hook is configured, email templates are bypassed
entirely. Auth passes event metadata to the hook, not rendered HTML. The
template list and editor give no indication of this.

Deleting the send-email hook silently reverts Auth to using email
templates with no warning. For post-cutoff Free plan projects without
custom SMTP, this also locks template editing.

## What is the new behavior?

### Admonition when send-email hook is active

A new `SendEmailHookActiveAdmonition` is shown on both the template list
and individual template editor pages when `HOOK_SEND_EMAIL_ENABLED` and
`HOOK_SEND_EMAIL_URI` are set:

> **Email templates are not used**
> A Send Email hook is active. Event metadata is passed directly to your
hook, meaning these templates are bypassed entirely.

With a **Manage hook** link to the hooks page.

### AlertDialog for Send Email hook deletion

Deleting the Send Email hook now uses a dedicated
`DeleteSendEmailHookConfirmationDialog`:

- **Always:** "The {default or built-in} email templates will be used to
send auth emails."
- **Post-cutoff Free plan, no custom SMTP:** adds "Email templates
cannot be edited on the Free plan without custom SMTP."

The dialog stays open with a loading state while the deletion is
in-flight and closes on success.

| After |
| --- |
| <img width="1862" height="880" alt="CleanShot 2026-05-25 at 15 57
41@2x"
src="https://github.com/user-attachments/assets/8a441bb2-9112-4b19-bd0b-02c9d1989ec1"
/> |
| <img width="884" height="578" alt="CleanShot 2026-05-25 at 15 57
21@2x"
src="https://github.com/user-attachments/assets/44e5bd79-2bd9-44ee-8f53-5fdaeefd68c6"
/> |

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

* **New Features**
* Added a Send Email hook warning in the template editor with “Manage
hook” and “Learn more” links.
* **UI Improvements**
* Refined template editor alerts to reflect when templates are bypassed
vs blocked.
* Updated hook cards/actions to a dropdown with separate Edit and Delete
flows, including documentation links.
* **Bug Fixes**
* Improved template editor and hook deletion flows to better reflect
pending states and current authentication configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 17:09:31 +08:00

170 lines
5.6 KiB
TypeScript

import { joinSqlFragments } from '@supabase/pg-meta'
import { useParams } from 'common'
import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
cn,
} from 'ui'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
import { isBeforeFreeTierTemplateBlockCutoff } from '../EmailTemplates/EmailTemplates.utils'
import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils'
import { type Hook } from './hooks.constants'
import { getRevokePermissionStatements } from './hooks.utils'
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
import { useAuthHooksUpdateMutation } from '@/data/auth/auth-hooks-update-mutation'
import { executeSql } from '@/data/sql/execute-sql-mutation'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
interface DeleteHookConfirmationDialogProps {
hook?: Hook
onOpenChange: (open: boolean) => void
onDeleteSuccess: () => void
}
export const DeleteHookConfirmationDialog = ({
hook,
onOpenChange,
onDeleteSuccess,
}: DeleteHookConfirmationDialogProps) => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: authConfig } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthHooks, isPending: isDeletingAuthHook } = useAuthHooksUpdateMutation({
onSuccess: async () => {
if (!hook) return
const { method } = hook
if (method.type === 'postgres') {
try {
const revokeStatements = getRevokePermissionStatements(method.schema, method.functionName)
await executeSql({
projectRef,
connectionString: project!.connectionString,
sql: joinSqlFragments(revokeStatements, '\n'),
})
toast.success(`Successfully deleted ${hook.title}`)
} catch (error) {
toast.warning(
`Deleted ${hook.title}, but failed to revoke permissions on ${method.schema}.${method.functionName}. You may want to revoke them manually.`
)
}
}
onDeleteSuccess()
},
onError: (error) => {
toast.error(`Failed to delete hook: ${error.message}`)
},
})
const open = !!hook
// Whether deleting the Send Email hook will lock template editing:
// post-cutoff Free plan projects without custom SMTP.
const willLockTemplates =
!!selectedOrganization &&
selectedOrganization.plan?.id === 'free' &&
!!project?.inserted_at &&
!isBeforeFreeTierTemplateBlockCutoff(project.inserted_at) &&
!isSmtpEnabled(authConfig)
const handleDeleteSendEmailHook = async (): Promise<void> => {
if (!hook) return
updateAuthHooks({
projectRef: projectRef!,
config: {
[hook.enabledKey]: false,
[hook.uriKey]: null,
[hook.secretsKey]: null,
},
})
}
if (hook?.id === 'send-email') {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm to delete Send Email hook</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2">
<p>
The {willLockTemplates ? 'default' : 'built-in'} email templates will be used when
sending authentication-related emails.
</p>
{willLockTemplates && (
<p>Email templates cannot be edited on the Free plan without custom SMTP.</p>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="danger"
loading={isDeletingAuthHook}
onClick={(e) => {
e.preventDefault()
handleDeleteSendEmailHook()
}}
>
Delete hook
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
return (
<ConfirmationModal
visible={!!hook && hook.id !== 'send-email'}
size="small"
variant="destructive"
loading={isDeletingAuthHook}
title={`Confirm to delete ${hook?.title}`}
className={cn('md:px-0', hook?.method.type === 'postgres' && 'pb-0')}
confirmLabel="Delete"
confirmLabelLoading="Deleting"
onCancel={() => onOpenChange(false)}
onConfirm={handleDeleteSendEmailHook}
>
<div>
<p className="md:px-5 text-sm text-foreground-light">
Are you sure you want to delete the {hook?.title}?
</p>
{hook?.method.type === 'postgres' && (
<>
<p className="md:px-5 text-sm text-foreground-light">
The following statements will be executed on the {hook?.method.schema}.
{hook?.method.functionName} function:
</p>
<div className="mt-4 h-72">
<CodeEditor
isReadOnly
id="deletion-hook-editor"
language="pgsql"
value={getRevokePermissionStatements(
hook?.method.schema,
hook?.method.functionName
).join('\n\n')}
/>
</div>
</>
)}
</div>
</ConfirmationModal>
)
}