Files
supabase/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx
T
Gildas Garcia 3146650a5a Fix FormItemLayout usages for a11y (#49761)
Follow up of #49637. Usages that impacted tests were fixed in the
previous PR. This PR fixes the other usages so that label are correctly
linked to their inputs.

No visual changes

## How to test

1. Design system: [Form
examples](https://design-system-git-fix-form-item-layout-usages-supabase.vercel.app/design-system/docs/ui-patterns/forms):
moved `FormControl` around the `SelectTrigger` so that the label is
linked to the button (It's actually done like this in the [Select Form
example](https://design-system-git-fix-form-item-layout-usages-supabase.vercel.app/design-system/docs/components/select#form)
and Radix recommend targeting the button too in their
[documentation](https://www.radix-ui.com/primitives/docs/components/select#labelling))
2. [Access
tokens](https://studio-staging-463111oii-supabase.vercel.app/dashboard/account/tokens):
updated usage to fallback on generated ids and fixed the select just
like _1_
3. [New TOTP
factor](https://studio-staging-463111oii-supabase.vercel.app/dashboard/account/security):
updated usage to fallback on generated ids
4. _Studio/Database/Extensions_
(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/database/extensions`):
updated the extension enabling modal to fallback on generated ids
5. _Studio/Integrations/Vault
(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/integrations/vault/secrets`):
updated the secret edition modal to fallback on generated ids
6.
_Studio/Observability(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/observability`):
updated the report creation and edition modals to fallback on generated
ids
7. _Studio/SQL
Editor(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/sql/new`):
updated the query renaming modal to fallback on generated ids
8.
_Studio/Storage/Analytics(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/storage/analytics`):
updated the table creation sheet to fallback on generated ids (you must
have a bucket first)
9.
_Studio/Workers(`https://studio-staging-463111oii-supabase.vercel.app/dashboard/project/[PROJECT]/workers`):
updated the worker creation modal to fallback on generated ids (you must
have a bucket first)
10. Updated
[Signup](https://studio-staging-463111oii-supabase.vercel.app/dashboard/sign-up?returnTo=%2Fnew),
[Signin](https://studio-staging-463111oii-supabase.vercel.app/dashboard/sign-in)
and [SSO
Signin](https://studio-staging-463111oii-supabase.vercel.app/dashboard/sign-in-sso)
forms to fallback on generated ids

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

- **Improvements**
- Standardized form field presentation across access tokens,
authentication, reports, integrations, database extensions, SQL editor,
storage, and worker deployment workflows.
- Updated password fields and visibility toggles for more consistent
input behavior.
- Refined token expiration selection, verification code entry, and
dropdown layouts.
- Preserved existing labels, validation, options, and form functionality
while simplifying the interface structure.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 15:22:55 +02:00

132 lines
4.0 KiB
TypeScript

import HCaptcha from '@hcaptcha/react-hcaptcha'
import { zodResolver } from '@hookform/resolvers/zod'
import { useQueryClient } from '@tanstack/react-query'
import { Lock } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { useForm, type SubmitHandler } from 'react-hook-form'
import { toast } from 'sonner'
import { Button, Form, FormControl, FormField, Input } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import z from 'zod'
import { LastSignInWrapper } from './LastSignInWrapper'
import { useLastSignIn } from '@/hooks/misc/useLastSignIn'
import { BASE_PATH } from '@/lib/constants'
import { captureCriticalError } from '@/lib/error-reporting'
import { auth, buildPathWithParams } from '@/lib/gotrue'
const schema = z.object({
email: z.string().min(1, 'Email is required').email('Must be a valid email'),
})
const formId = 'sso-sign-in-form'
export const SignInSSOForm = () => {
const queryClient = useQueryClient()
const captchaRef = useRef<HCaptcha>(null)
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const [_, setLastSignInUsed] = useLastSignIn()
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { email: '' },
})
const isSubmitting = form.formState.isSubmitting
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ email }) => {
const toastId = toast.loading('Signing in...')
let token = captchaToken
if (!token) {
const captchaResponse = await captchaRef.current?.execute({ async: true })
token = captchaResponse?.response ?? null
}
// redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
const redirectTo = buildPathWithParams(
`${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}/sign-in-mfa?method=sso`
)
const { data, error } = await auth.signInWithSSO({
domain: email.split('@')[1],
options: {
captchaToken: token ?? undefined,
redirectTo,
},
})
if (!error) {
await queryClient.resetQueries()
setLastSignInUsed('sso')
if (data) {
// redirect to SSO identity provider page
window.location.href = data.url
}
} else {
setCaptchaToken(null)
captchaRef.current?.resetCaptcha()
toast.error(`Failed to sign in: ${error.message}`, { id: toastId })
captureCriticalError(error, 'sign in via SSO')
}
}
return (
<Form {...form}>
<form
id={formId}
method="POST"
className="flex flex-col gap-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
key="email"
name="email"
control={form.control}
render={({ field }) => (
<FormItemLayout label="Email">
<FormControl>
<Input type="email" autoComplete="email" {...field} placeholder="gavin@hooli.com" />
</FormControl>
</FormItemLayout>
)}
/>
<div className="self-center">
<HCaptcha
ref={captchaRef}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
</div>
<Button block form={formId} type="submit" size="large" loading={isSubmitting}>
Sign in
</Button>
</form>
</Form>
)
}
export const SignInWithSSOButton = () => {
const router = useRouter()
return (
<LastSignInWrapper type="sso">
<Button asChild block size="large" variant="outline" icon={<Lock />}>
<Link href={{ pathname: '/sign-in-sso', query: router.query }}>Continue with SSO</Link>
</Button>
</LastSignInWrapper>
)
}