feat: expired link UX — auto-redirect to login with friendly notice
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- /error?error=Verification: shows "Link Expired" with amber icon, auto-redirects to /login?expired=1 after 5 seconds - /accept-invite: expired/invalid/already-accepted tokens auto-redirect to login after 4 seconds with "Redirecting..." message - /login: amber banner when ?expired=1 explains the link expired and prompts to sign in again or request a new magic link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,52 @@ import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
type InviteState = 'loading' | 'valid' | 'accepting' | 'error'
|
||||
|
||||
function ErrorRedirectCard({
|
||||
errorContent,
|
||||
redirectTarget,
|
||||
}: {
|
||||
errorContent: { icon: React.ReactNode; title: string; description: string; redirect?: string }
|
||||
redirectTarget: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
router.push(redirectTarget)
|
||||
}, 4000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [redirectTarget, router])
|
||||
|
||||
return (
|
||||
<AnimatedCard>
|
||||
<Card className="w-full max-w-md overflow-hidden">
|
||||
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-100">
|
||||
{errorContent.icon}
|
||||
</div>
|
||||
<CardTitle className="text-xl">{errorContent.title}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{errorContent.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => router.push(redirectTarget)}
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
<p className="text-xs text-center text-muted-foreground">
|
||||
Redirecting to login in a few seconds...
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)
|
||||
}
|
||||
|
||||
function AcceptInviteContent() {
|
||||
const [state, setState] = useState<InviteState>('loading')
|
||||
const [errorType, setErrorType] = useState<string | null>(null)
|
||||
@@ -105,18 +151,21 @@ function AcceptInviteContent() {
|
||||
icon: <XCircle className="h-6 w-6 text-red-600" />,
|
||||
title: 'Invalid Invitation',
|
||||
description: 'This invitation link is not valid. It may have already been used or the link is incorrect.',
|
||||
redirect: '/login?expired=1',
|
||||
}
|
||||
case 'EXPIRED_TOKEN':
|
||||
return {
|
||||
icon: <Clock className="h-6 w-6 text-amber-600" />,
|
||||
title: 'Invitation Expired',
|
||||
description: 'This invitation has expired. Please contact your administrator to receive a new invitation.',
|
||||
redirect: '/login?expired=1',
|
||||
}
|
||||
case 'ALREADY_ACCEPTED':
|
||||
return {
|
||||
icon: <CheckCircle2 className="h-6 w-6 text-blue-600" />,
|
||||
title: 'Already Accepted',
|
||||
description: 'This invitation has already been accepted. You can sign in with your credentials.',
|
||||
redirect: '/login',
|
||||
}
|
||||
case 'AUTH_FAILED':
|
||||
return {
|
||||
@@ -148,34 +197,12 @@ function AcceptInviteContent() {
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
// Error state — auto-redirect to login after 4 seconds for known errors
|
||||
if (state === 'error') {
|
||||
const errorContent = getErrorContent()
|
||||
return (
|
||||
<AnimatedCard>
|
||||
<Card className="w-full max-w-md overflow-hidden">
|
||||
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-100">
|
||||
{errorContent.icon}
|
||||
</div>
|
||||
<CardTitle className="text-xl">{errorContent.title}</CardTitle>
|
||||
<CardDescription className="text-base">
|
||||
{errorContent.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => router.push('/login')}
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)
|
||||
const redirectTarget = errorContent.redirect || '/login'
|
||||
|
||||
return <ErrorRedirectCard errorContent={errorContent} redirectTarget={redirectTarget} />
|
||||
}
|
||||
|
||||
// Valid invitation - show welcome
|
||||
|
||||
Reference in New Issue
Block a user