SignInSSOForm.tsx116 lines · main
1import HCaptcha from '@hcaptcha/react-hcaptcha'
2import { zodResolver } from '@hookform/resolvers/zod'
3import { useQueryClient } from '@tanstack/react-query'
4import { useRef, useState } from 'react'
5import { useForm, type SubmitHandler } from 'react-hook-form'
6import { toast } from 'sonner'
7import { Button, Form, FormControl, FormField, Input } from 'ui'
8import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
9import z from 'zod'
10
11import { useLastSignIn } from '@/hooks/misc/useLastSignIn'
12import { BASE_PATH } from '@/lib/constants'
13import { captureCriticalError } from '@/lib/error-reporting'
14import { auth, buildPathWithParams } from '@/lib/gotrue'
15
16const schema = z.object({
17 email: z.string().min(1, 'Email is required').email('Must be a valid email'),
18})
19
20const formId = 'sso-sign-in-form'
21
22export const SignInSSOForm = () => {
23 const queryClient = useQueryClient()
24 const captchaRef = useRef<HCaptcha>(null)
25 const [captchaToken, setCaptchaToken] = useState<string | null>(null)
26 const [_, setLastSignInUsed] = useLastSignIn()
27 const form = useForm<z.infer<typeof schema>>({
28 resolver: zodResolver(schema as any),
29 defaultValues: { email: '' },
30 })
31 const isSubmitting = form.formState.isSubmitting
32
33 const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ email }) => {
34 const toastId = toast.loading('Signing in...')
35
36 let token = captchaToken
37 if (!token) {
38 const captchaResponse = await captchaRef.current?.execute({ async: true })
39 token = captchaResponse?.response ?? null
40 }
41
42 // redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
43 const redirectTo = buildPathWithParams(
44 `${
45 process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
46 ? location.origin
47 : process.env.NEXT_PUBLIC_SITE_URL
48 }${BASE_PATH}/sign-in-mfa?method=sso`
49 )
50
51 const { data, error } = await auth.signInWithSSO({
52 domain: email.split('@')[1],
53 options: {
54 captchaToken: token ?? undefined,
55 redirectTo,
56 },
57 })
58
59 if (!error) {
60 await queryClient.resetQueries()
61 setLastSignInUsed('sso')
62 if (data) {
63 // redirect to SSO identity provider page
64 window.location.href = data.url
65 }
66 } else {
67 setCaptchaToken(null)
68 captchaRef.current?.resetCaptcha()
69 toast.error(`Failed to sign in: ${error.message}`, { id: toastId })
70 captureCriticalError(error, 'sign in via SSO')
71 }
72 }
73
74 return (
75 <Form {...form}>
76 <form id={formId} className="flex flex-col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
77 <FormField
78 key="email"
79 name="email"
80 control={form.control}
81 render={({ field }) => (
82 <FormItemLayout name="email" label="Email">
83 <FormControl>
84 <Input
85 id="email"
86 type="email"
87 autoComplete="email"
88 {...field}
89 placeholder="gavin@hooli.com"
90 />
91 </FormControl>
92 </FormItemLayout>
93 )}
94 />
95
96 <div className="self-center">
97 <HCaptcha
98 ref={captchaRef}
99 sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
100 size="invisible"
101 onVerify={(token) => {
102 setCaptchaToken(token)
103 }}
104 onExpire={() => {
105 setCaptchaToken(null)
106 }}
107 />
108 </div>
109
110 <Button block form={formId} htmlType="submit" size="large" loading={isSubmitting}>
111 Sign in
112 </Button>
113 </form>
114 </Form>
115 )
116}