All files / pages AcceptInvitationPage.tsx

97.82% Statements 45/46
91.3% Branches 21/23
100% Functions 5/5
100% Lines 44/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                      68x 68x 68x 209x   68x 68x 68x 68x 68x 68x   68x 28x 3x 3x 3x     25x 25x 25x 20x     5x 5x   25x       25x     68x 10x   10x 10x   10x 10x     7x       7x 4x   3x     3x 3x 3x   10x       68x 28x             40x 3x                                                                               37x 8x                                   29x                          
 
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { getInvitation, acceptInvitation, type InvitationPublicResponse } from "@/services/invitations.service";
import { InvitationPasswordForm } from "@/components/invitations/InvitationPasswordForm";
import { useAuthStore } from "@/stores/auth.store";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertTriangle, Loader2 } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
 
export default function AcceptInvitationPage() {
    const [searchParams] = useSearchParams();
    const token = searchParams.get("token");
    const navigate = useNavigate();
    const setAuth = useAuthStore((state) => state.setAuth);
 
    const [invitation, setInvitation] = useState<InvitationPublicResponse | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);
    const [submitting, setSubmitting] = useState(false);
    const [submitError, setSubmitError] = useState<string | null>(null);
    const [success, setSuccess] = useState(false);
 
    useEffect(() => {
        if (!token) {
            setError("Invitation token is missing.");
            setLoading(false);
            return;
        }
 
        const fetchInvitation = async () => {
            try {
                const data = await getInvitation(token);
                setInvitation(data);
            } catch (err: any) {
                // Handle specific errors from backend
                const message = err.response?.data?.message || "Invalid or expired invitation.";
                setError(message);
            } finally {
                setLoading(false);
            }
        };
 
        fetchInvitation();
    }, [token]);
 
    const handleAccept = async (password: string) => {
        Iif (!token) return;
 
        setSubmitting(true);
        setSubmitError(null);
 
        try {
            const response = await acceptInvitation(token, { password });
 
            // Successfully accepted, set auth state
            setAuth(response);
 
            // Redirect based on role
            // Only admins/superadmins should access the admin dashboard
            if (response.user.role === 'admin' || response.user.role === 'superadmin') {
                navigate("/admin/dashboard", { replace: true });
            } else {
                setSuccess(true);
            }
        } catch (err: any) {
            console.error("Failed to accept invitation:", err);
            const message = err.response?.data?.message || err.response?.data?.error || "Failed to accept invitation. Please try again.";
            setSubmitError(message);
        } finally {
            setSubmitting(false);
        }
    };
 
    if (loading) {
        return (
            <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
                <Loader2 className="h-8 w-8 animate-spin text-primary" />
            </div>
        );
    }
 
    if (success) {
        return (
            <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
                <Card className="w-full max-w-md mx-auto shadow-lg border-green-500/50">
                    <CardContent className="pt-6 text-center space-y-4">
                        <div className="mx-auto w-12 h-12 rounded-full bg-green-100 flex items-center justify-center">
                            <svg
                                className="w-6 h-6 text-green-600"
                                fill="none"
                                stroke="currentColor"
                                viewBox="0 0 24 24"
                                xmlns="http://www.w3.org/2000/svg"
                            >
                                <path
                                    strokeLinecap="round"
                                    strokeLinejoin="round"
                                    strokeWidth={2}
                                    d="M5 13l4 4L19 7"
                                />
                            </svg>
                        </div>
                        <h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Welcome to SnackBase!</h2>
                        <p className="text-muted-foreground">
                            Your account has been successfully created and you have joined <strong>{invitation?.account_name}</strong>.
                        </p>
                        <Alert className="bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-900">
                            <AlertDescription className="text-blue-800 dark:text-blue-200">
                                You can now log in to the application using your credentials.
                            </AlertDescription>
                        </Alert>
                        <div className="pt-2">
                            <p className="text-sm text-gray-500">
                                (This is an admin portal. As a regular user, you may not have access to the dashboard features here.)
                            </p>
                        </div>
                    </CardContent>
                </Card>
            </div>
        );
    }
 
    if (error || !invitation) {
        return (
            <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
                <Card className="w-full max-w-md mx-auto shadow-lg border-destructive/50">
                    <CardContent className="pt-6">
                        <Alert variant="destructive">
                            <AlertTriangle className="h-4 w-4" />
                            <AlertTitle>Invitation Error</AlertTitle>
                            <AlertDescription>{error || "Invitation not found."}</AlertDescription>
                        </Alert>
                        <div className="mt-4 text-center">
                            <a href="/admin/login" className="text-sm text-primary hover:underline">Return to Login</a>
                        </div>
                    </CardContent>
                </Card>
            </div>
        );
    }
 
    return (
        <div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 p-4">
            <InvitationPasswordForm
                email={invitation.email}
                accountName={invitation.account_name}
                invitedByName={invitation.invited_by_name}
                isLoading={submitting}
                error={submitError}
                onSubmit={handleAccept}
            />
        </div>
    );
}