mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
fix(studio): hide social sign-in after email sign-up (#49571)
## What kind of change does this PR introduce? Bug fix. Resolves [FE-4264](https://linear.app/supabase/issue/FE-4264/hide-social-sign-in-options-after-email-sign-up). ## What is the current behavior? After a successful email and password sign-up, GitHub and ChatGPT sign-in options remain visible even though they do not confirm or link the new account. The success state is presented in a bespoke `Alert` with verbose copywriting. ## What is the new behavior? The social sign-in options and divider are hidden after email sign-up succeeds. The email confirmation message and link back to sign in remain available. The success state is presented in a standard `success` `Admonition` with clearer copywriting. | Before | After | | --- | --- | | <img width="2576" height="1700" alt="CleanShot 2026-08-26 at 13 47 41@2x" src="https://github.com/user-attachments/assets/a29547b9-7949-4ff3-a1d4-db8bfb3beee6" /> | <img width="2576" height="1704" alt="CleanShot 2026-08-26 at 13 47 00@2x" src="https://github.com/user-attachments/assets/4f778571-74fe-4c20-b76e-a87e0391e4a9" /> | ## To test 1. Open `/sign-up` and complete an email and password sign-up. 2. Confirm the success message is shown without the GitHub, ChatGPT, or `or` options. 3. Open `/sign-in` and confirm GitHub and ChatGPT remain available there. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added shared provider options across sign-in and sign-up flows, including custom providers, external identity providers, and optional SSO. - Added an SSO sign-in button that preserves the current page context. - After successful email signup, alternative signup options are hidden and confirmation messaging appears. - **Bug Fixes** - Improved signup form spacing, submission state, and animated password guidance. - **Tests** - Added coverage for signup behavior across standard and focused-provider configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
bd02d7f297
commit
ee3f78ff37
8 files changed
+258
-141
No files matched your search
@@ -0,0 +1,60 @@
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { SignInWithSSOButton } from './SignInSSOForm'
|
||||
import { SignInWithCustom } from './SignInWithCustom'
|
||||
import { SignInWithExternalProvider } from './SignInWithExternalProvider'
|
||||
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { type ExternalIdentityProviderConfig } from '@/lib/external-identity-providers'
|
||||
|
||||
export const SignInOptions = ({
|
||||
providers,
|
||||
dividerBgClass = 'bg-studio',
|
||||
}: {
|
||||
providers: ExternalIdentityProviderConfig[]
|
||||
dividerBgClass?: string
|
||||
}) => {
|
||||
const {
|
||||
dashboardAuthSignInWithSso: signInWithSsoEnabled,
|
||||
dashboardAuthSignInWithEmail: signInWithEmailEnabled,
|
||||
} = useIsFeatureEnabled(['dashboard_auth:sign_in_with_sso', 'dashboard_auth:sign_in_with_email'])
|
||||
|
||||
const {
|
||||
dashboardAuthCustomProvider: customProvider,
|
||||
dashboardAuthCustomProviders: customProvidersNew,
|
||||
} = useCustomContent(['dashboard_auth:custom_provider', 'dashboard_auth:custom_providers'])
|
||||
|
||||
// [Joshen] This is just for backward compatibility - singular customProvider needs to be deprecated subsequently
|
||||
// Just need to remove customProvider and rename customProvidersNew to customProviders
|
||||
const customProviders = customProvidersNew ?? (customProvider ? [customProvider] : [])
|
||||
|
||||
const showOrDivider =
|
||||
(providers.length > 0 || signInWithSsoEnabled || customProviders.length > 0) &&
|
||||
signInWithEmailEnabled
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.isArray(customProviders) &&
|
||||
customProviders.map((providerName: string) => (
|
||||
<SignInWithCustom key={providerName} providerName={providerName} />
|
||||
))}
|
||||
|
||||
{providers.map((provider) => (
|
||||
<SignInWithExternalProvider key={provider.id} provider={provider} />
|
||||
))}
|
||||
|
||||
{signInWithSsoEnabled && <SignInWithSSOButton />}
|
||||
|
||||
{showOrDivider && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-strong" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className={cn('px-2 text-sm text-foreground', dividerBgClass)}>or</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
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'
|
||||
@@ -8,6 +11,7 @@ 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'
|
||||
@@ -119,3 +123,15 @@ export const SignInSSOForm = () => {
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { motion } from 'framer-motion'
|
||||
import { CheckCircle, Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsString, useQueryStates } from 'nuqs'
|
||||
import { useRef, useState } from 'react'
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
Input,
|
||||
} from 'ui'
|
||||
import { Button, cn, Form, FormControl, FormField, Input } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/Admonition'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import z from 'zod'
|
||||
|
||||
@@ -52,7 +43,7 @@ const schema = z.object({
|
||||
|
||||
const formId = 'sign-up-form'
|
||||
|
||||
export const SignUpForm = () => {
|
||||
export const SignUpForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
const [showConditions, setShowConditions] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
@@ -76,6 +67,7 @@ export const SignUpForm = () => {
|
||||
onSuccess: () => {
|
||||
toast.success(`Signed up successfully!`)
|
||||
setIsSubmitted(true)
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (error) => {
|
||||
setCaptchaToken(null)
|
||||
@@ -135,22 +127,22 @@ export const SignUpForm = () => {
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
className="absolute top-0 w-full"
|
||||
className="w-full"
|
||||
>
|
||||
<Alert variant="default">
|
||||
<CheckCircle />
|
||||
<AlertTitle>Check your email to confirm</AlertTitle>
|
||||
<AlertDescription className="text-xs">
|
||||
You've successfully signed up. Please check your email to confirm your account before
|
||||
signing in to the Supabase dashboard. The confirmation link expires in 10 minutes.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Admonition
|
||||
type="success"
|
||||
title="Check your email"
|
||||
description="We sent you a link to finish signing up. It expires in 10 minutes."
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
<div
|
||||
inert={isSubmitted || undefined}
|
||||
className={cn(
|
||||
'w-full py-1 transition-all duration-500',
|
||||
isSubmitted ? 'max-h-[100px] opacity-0 pointer-events-none' : 'max-h-[1000px] opacity-100'
|
||||
'w-full transition-all duration-500',
|
||||
isSubmitted
|
||||
? 'max-h-0 overflow-hidden opacity-0 pointer-events-none py-0'
|
||||
: 'max-h-[1000px] opacity-100'
|
||||
)}
|
||||
>
|
||||
<Form {...form}>
|
||||
@@ -213,13 +205,16 @@ export const SignUpForm = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${
|
||||
showConditions ? 'max-h-[500px]' : 'max-h-0'
|
||||
} transition-all duration-400 overflow-y-hidden`}
|
||||
>
|
||||
<PasswordConditionsHelper password={password} />
|
||||
</div>
|
||||
{showConditions && (
|
||||
<motion.div
|
||||
initial={{ maxHeight: '0px' }}
|
||||
animate={{ maxHeight: '500px' }}
|
||||
transition={{ duration: 0.8, delay: 0 }}
|
||||
className="overflow-y-hidden"
|
||||
>
|
||||
<PasswordConditionsHelper password={password} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="self-center">
|
||||
<HCaptcha
|
||||
|
||||
@@ -10,6 +10,14 @@ import { customRenderHook } from '@/tests/lib/custom-render'
|
||||
|
||||
vi.mock('next/router', () => import('next-router-mock'))
|
||||
|
||||
// The global mock in vitestSetup.ts stubs `useParams` to always return `{ ref: 'default' }`, which
|
||||
// doesn't reflect the `method`/`destination` query params this hook relies on — restore the real
|
||||
// implementation here so it reads them from the mocked router.
|
||||
vi.mock('common', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as object
|
||||
return { ...actual }
|
||||
})
|
||||
|
||||
const mockEnabledProviders = vi.hoisted(() => vi.fn<() => ExternalIdentityProviderConfig[]>())
|
||||
|
||||
vi.mock('../useEnabledIdentityProviders', () => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useParams } from 'common'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useEnabledIdentityProviders } from './useEnabledIdentityProviders'
|
||||
@@ -33,15 +33,9 @@ export type InboundBranding = {
|
||||
* screen.
|
||||
*/
|
||||
export function useInboundBranding(flow: 'sign-in' | 'sign-up' = 'sign-in'): InboundBranding {
|
||||
const router = useRouter()
|
||||
const enabledProviders = useEnabledIdentityProviders()
|
||||
|
||||
const destinationId =
|
||||
router.isReady && typeof router.query.destination === 'string'
|
||||
? router.query.destination
|
||||
: undefined
|
||||
const focusId =
|
||||
router.isReady && typeof router.query.method === 'string' ? router.query.method : undefined
|
||||
const { destination: destinationId, method: focusId } = useParams()
|
||||
|
||||
const focusProvider = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Lock } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, cn } from 'ui'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import { LastSignInWrapper } from '@/components/interfaces/SignIn/LastSignInWrapper'
|
||||
import { SignInForm } from '@/components/interfaces/SignIn/SignInForm'
|
||||
import { SignInWithCustom } from '@/components/interfaces/SignIn/SignInWithCustom'
|
||||
import { SignInOptions } from '@/components/interfaces/SignIn/SignInOptions'
|
||||
import { SignInWithExternalProvider } from '@/components/interfaces/SignIn/SignInWithExternalProvider'
|
||||
import { AuthenticationLayout } from '@/components/layouts/AuthenticationLayout'
|
||||
import { SignInLayout } from '@/components/layouts/SignInLayout/SignInLayout'
|
||||
@@ -15,7 +13,6 @@ import { useEnabledIdentityProviders } from '@/hooks/misc/useEnabledIdentityProv
|
||||
import { useInboundBranding } from '@/hooks/misc/useInboundBranding'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import type { ExternalIdentityProviderConfig } from '@/lib/external-identity-providers'
|
||||
import { getSignUpReturnTo } from '@/lib/gotrue'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
@@ -45,47 +42,6 @@ const SignInPage: NextPageWithLayout = () => {
|
||||
const { focusProvider } = useInboundBranding('sign-in')
|
||||
const signInProviders = useEnabledIdentityProviders().filter((provider) => provider.showOnSignIn)
|
||||
|
||||
const renderAuthOptions = (
|
||||
providers: ExternalIdentityProviderConfig[],
|
||||
dividerBgClass = 'bg-studio'
|
||||
) => {
|
||||
const showOrDivider =
|
||||
(providers.length > 0 || signInWithSsoEnabled || customProviders.length > 0) &&
|
||||
signInWithEmailEnabled
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.isArray(customProviders) &&
|
||||
customProviders.map((providerName: string) => (
|
||||
<SignInWithCustom key={providerName} providerName={providerName} />
|
||||
))}
|
||||
{providers.map((provider) => (
|
||||
<SignInWithExternalProvider key={provider.id} provider={provider} />
|
||||
))}
|
||||
{signInWithSsoEnabled && (
|
||||
<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>
|
||||
)}
|
||||
{showOrDivider && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-strong" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className={cn('px-2 text-sm text-foreground', dividerBgClass)}>or</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{signInWithEmailEnabled && <SignInForm />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_PLATFORM) {
|
||||
// on selfhosted instance just redirect to projects page
|
||||
@@ -106,9 +62,13 @@ const SignInPage: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<SignInWithExternalProvider provider={focusProvider} />
|
||||
|
||||
{hasOtherOptions &&
|
||||
(showOtherOptions ? (
|
||||
renderAuthOptions(otherProviders, 'bg-surface-100')
|
||||
<>
|
||||
<SignInOptions providers={otherProviders} dividerBgClass="bg-surface-100" />
|
||||
{signInWithEmailEnabled && <SignInForm />}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
block
|
||||
@@ -126,7 +86,10 @@ const SignInPage: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5">{renderAuthOptions(signInProviders)}</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<SignInOptions providers={signInProviders} />
|
||||
{signInWithEmailEnabled && <SignInForm />}
|
||||
</div>
|
||||
|
||||
{signUpEnabled && (
|
||||
<div className="self-center my-8 text-sm">
|
||||
|
||||
@@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Button, cn } from 'ui'
|
||||
|
||||
import { SignInOptions } from '@/components/interfaces/SignIn/SignInOptions'
|
||||
import { SignInWithExternalProvider } from '@/components/interfaces/SignIn/SignInWithExternalProvider'
|
||||
import { SignUpForm } from '@/components/interfaces/SignIn/SignUpForm'
|
||||
import { SignInLayout } from '@/components/layouts/SignInLayout/SignInLayout'
|
||||
@@ -9,11 +10,11 @@ import { UnknownInterface } from '@/components/ui/UnknownInterface'
|
||||
import { useEnabledIdentityProviders } from '@/hooks/misc/useEnabledIdentityProviders'
|
||||
import { useInboundBranding } from '@/hooks/misc/useInboundBranding'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import type { ExternalIdentityProviderConfig } from '@/lib/external-identity-providers'
|
||||
import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const SignUpPage: NextPageWithLayout = () => {
|
||||
const [showOtherOptions, setShowOtherOptions] = useState(false)
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
const { dashboardAuthSignUp: signUpEnabled } = useIsFeatureEnabled(['dashboard_auth:sign_up'])
|
||||
|
||||
const { focusProvider } = useInboundBranding('sign-up')
|
||||
@@ -23,62 +24,41 @@ const SignUpPage: NextPageWithLayout = () => {
|
||||
return <UnknownInterface fullHeight={false} urlBack="/sign-in" />
|
||||
}
|
||||
|
||||
// The sign-up options we offer besides a focused provider: other external providers and the email
|
||||
// form. Rendered both on the full screen and when the user expands "other options" from the
|
||||
// focused screen. The "or" pill's background matches the surface behind it: the page
|
||||
// (`bg-studio`) on the full screen, or the interstitial card (`bg-surface-100`) when revealed.
|
||||
const renderAuthOptions = (
|
||||
providers: ExternalIdentityProviderConfig[],
|
||||
dividerBgClass = 'bg-studio'
|
||||
) => (
|
||||
<>
|
||||
{providers.map((provider) => (
|
||||
<SignInWithExternalProvider key={provider.id} provider={provider} />
|
||||
))}
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-strong" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className={cn('px-2 text-sm text-foreground', dividerBgClass)}>or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SignUpForm />
|
||||
</>
|
||||
)
|
||||
|
||||
// Inbound link focused us on a single provider — lead with that one (SignInLayout renders the
|
||||
// matching interstitial frame around it), but let the user reveal the rest of our options.
|
||||
if (focusProvider) {
|
||||
const otherProviders = signUpProviders.filter((provider) => provider.id !== focusProvider.id)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<SignInWithExternalProvider provider={focusProvider} />
|
||||
{showOtherOptions ? (
|
||||
renderAuthOptions(otherProviders, 'bg-surface-100')
|
||||
) : (
|
||||
<Button
|
||||
block
|
||||
variant="text"
|
||||
size="large"
|
||||
className="-mt-2 text-foreground-light"
|
||||
onClick={() => setShowOtherOptions(true)}
|
||||
>
|
||||
Show other options
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5">{renderAuthOptions(signUpProviders)}</div>
|
||||
{focusProvider ? (
|
||||
<div className="flex flex-col gap-5">
|
||||
{!isSubmitted && <SignInWithExternalProvider provider={focusProvider} />}
|
||||
{showOtherOptions ? (
|
||||
<>
|
||||
{!isSubmitted && (
|
||||
<SignInOptions
|
||||
providers={signUpProviders.filter((provider) => provider.id !== focusProvider.id)}
|
||||
dividerBgClass="bg-surface-100"
|
||||
/>
|
||||
)}
|
||||
<SignUpForm onSuccess={() => setIsSubmitted(true)} />
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
block
|
||||
variant="text"
|
||||
size="large"
|
||||
className="-mt-2 text-foreground-light"
|
||||
onClick={() => setShowOtherOptions(true)}
|
||||
>
|
||||
Show other options
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
{!isSubmitted && <SignInOptions providers={signUpProviders} />}
|
||||
<SignUpForm onSuccess={() => setIsSubmitted(true)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="my-8 self-center text-sm">
|
||||
<div className={cn('self-center text-center text-sm mb-8', isSubmitted ? 'mt-2' : 'mt-8')}>
|
||||
<span className="text-foreground-light">Have an account?</span>{' '}
|
||||
<Link
|
||||
href="/sign-in"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
CHATGPT_IDENTITY_PROVIDER,
|
||||
GITHUB_IDENTITY_PROVIDER,
|
||||
type ExternalIdentityProviderConfig,
|
||||
} from '@/lib/external-identity-providers'
|
||||
import SignUpPage from '@/pages/sign-up'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
focusProvider: undefined as ExternalIdentityProviderConfig | undefined,
|
||||
}))
|
||||
|
||||
vi.mock('@/components/interfaces/SignIn/SignInWithExternalProvider', () => ({
|
||||
SignInWithExternalProvider: ({ provider }: { provider: ExternalIdentityProviderConfig }) => (
|
||||
<Button>Continue with {provider.displayName}</Button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/interfaces/SignIn/SignUpForm', () => ({
|
||||
SignUpForm: ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSubmitted(true)
|
||||
onSuccess?.()
|
||||
}}
|
||||
>
|
||||
Complete email sign-up
|
||||
</Button>
|
||||
{isSubmitted && <div>Check your email</div>}
|
||||
</>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/misc/useEnabledIdentityProviders', () => ({
|
||||
useEnabledIdentityProviders: () => [GITHUB_IDENTITY_PROVIDER, CHATGPT_IDENTITY_PROVIDER],
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/misc/useInboundBranding', () => ({
|
||||
useInboundBranding: () => ({ focusProvider: mocks.focusProvider }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
|
||||
useIsFeatureEnabled: () => ({
|
||||
dashboardAuthSignUp: true,
|
||||
dashboardAuthSignInWithSso: false,
|
||||
dashboardAuthSignInWithEmail: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('SignUpPage', () => {
|
||||
beforeEach(() => {
|
||||
mocks.focusProvider = undefined
|
||||
})
|
||||
|
||||
test('hides social sign-up options after email sign-up succeeds', async () => {
|
||||
const user = userEvent.setup()
|
||||
customRender(<SignUpPage dehydratedState={undefined} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue with GitHub' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Continue with ChatGPT' })).toBeInTheDocument()
|
||||
expect(screen.getByText('or')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Check your email')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Complete email sign-up' }))
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Continue with GitHub' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Continue with ChatGPT' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('or')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Check your email')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in')
|
||||
})
|
||||
|
||||
test('hides the focused provider after email sign-up succeeds', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.focusProvider = GITHUB_IDENTITY_PROVIDER
|
||||
customRender(<SignUpPage dehydratedState={undefined} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue with GitHub' })).toBeInTheDocument()
|
||||
expect(screen.queryByText('Check your email')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Show other options' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Complete email sign-up' }))
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Continue with GitHub' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Continue with ChatGPT' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('or')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Check your email')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/sign-in')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user