@cantera/sign-in-cardblock
A multi-provider sign-in chooser card. Server-renderable via an href template, or client-driven via a callback.
npx shadcn@latest add @cantera/sign-in-card| Prop | Type | Default | Description |
|---|---|---|---|
| providers | OAuthProvider[] | — | Providers to offer, rendered as one ProviderSignInLink (with hrefTemplate) or ProviderSignInButton each. |
| hrefTemplate | string | — | Href for a provider auth route; "{provider}" is replaced with the provider id, e.g. "/api/auth/{provider}". Serializable, so the card can be rendered from a server component. |
| onSignIn | (providerId: string) => void | Promise<void> | — | Click handler alternative to hrefTemplate, for client-side flows. A returned promise drives the pending state for that provider. |
| loadingProvider | string | — | Id of the provider currently authenticating, to show its spinner. While one provider is pending its siblings lock — one OAuth flow at a time, since a second redirect would race the first. |
| title | ReactNode | 'Sign in' | Card title. |
| titleAs | 'h1' | … | 'h6' | 'div' | 'h2' | Heading element for the title — a card dropped onto a page needs a real heading. Pick the level that fits the page outline, or pass "div" when the surrounding page already provides one. |
| description | ReactNode | — | Optional text under the title. |
| footer | ReactNode | — | Optional muted footer content. |
| ...props | ComponentProps<typeof Card> | — | Remaining props are spread onto the underlying Card. |
This is the exact code the CLI installs into your project — you own it from there.
'use client'
import type * as React from 'react'
import { useState } from 'react'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { ProviderSignInButton, ProviderSignInLink } from '@/components/ui/provider-sign-in-button'
import type { OAuthProvider } from '@/lib/oauth-types'
import { cn } from '@/lib/utils'
interface SignInCardProps extends Omit<React.ComponentProps<typeof Card>, 'title'> {
providers: OAuthProvider[]
/** "{provider}" is replaced with the provider id, e.g. "/api/auth/{provider}".
* Serializable, so the card can render from a server component. */
hrefTemplate?: string
onSignIn?: (providerId: string) => void | Promise<void>
loadingProvider?: string
title?: React.ReactNode
/** Pick the heading level that fits the page outline, or "div" to opt out
* when the surrounding page already provides one. */
titleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div'
description?: React.ReactNode
footer?: React.ReactNode
}
/** Thenable check, not `instanceof Promise`: a polyfilled or cross-realm
* promise is still a pending round trip the buttons must reflect. */
function isPromiseLike(value: void | Promise<void>): value is Promise<void> {
return value != null && typeof (value as Promise<void>).then === 'function'
}
function SignInCard({
providers,
hrefTemplate,
onSignIn,
loadingProvider,
title = 'Sign in',
titleAs: TitleTag = 'h2',
description,
footer,
className,
...props
}: SignInCardProps) {
const [pendingProvider, setPendingProvider] = useState<string>()
const activeProvider = loadingProvider ?? pendingProvider
function handleSignIn(providerId: string) {
const result = onSignIn?.(providerId)
if (!isPromiseLike(result)) return
setPendingProvider(providerId)
result.then(
() => setPendingProvider(undefined),
() => setPendingProvider(undefined),
)
}
return (
<Card data-slot="sign-in-card" className={cn('w-full max-w-sm', className)} {...props}>
<CardHeader className="text-center">
<CardTitle className="text-2xl">
<TitleTag>{title}</TitleTag>
</CardTitle>
{description && <CardDescription>{description}</CardDescription>}
</CardHeader>
<CardContent className="flex flex-col gap-3">
{providers.map((provider) => {
const href = hrefTemplate?.replaceAll('{provider}', provider.id)
const loading = activeProvider === provider.id
// One OAuth flow at a time: a second redirect would race the first.
const disabled = activeProvider !== undefined && !loading
return href !== undefined ? (
<ProviderSignInLink
key={provider.id}
provider={provider}
href={href}
loading={loading}
disabled={disabled}
/>
) : (
<ProviderSignInButton
key={provider.id}
provider={provider}
onSignIn={onSignIn ? () => handleSignIn(provider.id) : undefined}
loading={loading}
disabled={disabled}
/>
)
})}
</CardContent>
{footer && (
<CardFooter className="justify-center text-center text-xs text-muted-foreground">
{footer}
</CardFooter>
)}
</Card>
)
}
export { SignInCard, type SignInCardProps }