236 lines
9.9 KiB
JavaScript
236 lines
9.9 KiB
JavaScript
import { useState, useEffect, useRef } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import axios from 'axios';
|
|
|
|
export default function InviteResponse() {
|
|
const [searchParams] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const [loading, setLoading] = useState(true);
|
|
const [tokenData, setTokenData] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [processing, setProcessing] = useState(false);
|
|
const shouldAutoLink = useRef(false);
|
|
|
|
const handleLinkAccount = async () => {
|
|
setProcessing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Try to link the account directly - if not authenticated, backend will return 401
|
|
await axios.post('/api/link-account', {
|
|
token: tokenData.token
|
|
}, {
|
|
withCredentials: true
|
|
});
|
|
|
|
// Force a page reload to refresh user profile and trigger privacy settings check
|
|
window.location.href = '/signin-landing';
|
|
} catch (err) {
|
|
// If not authenticated, redirect to signin
|
|
if (err.response?.status === 401 || err.response?.status === 403) {
|
|
const returnUrl = encodeURIComponent(`/invite-response?token=${tokenData.token}&autolink=true`);
|
|
navigate(`/signin?redirect=${returnUrl}`);
|
|
return;
|
|
}
|
|
|
|
setError(err.response?.data?.error || 'Failed to link account. Please try again.');
|
|
setProcessing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
validateToken();
|
|
}, []);
|
|
|
|
// Auto-link when tokenData is set and autolink flag is true
|
|
useEffect(() => {
|
|
if (tokenData && shouldAutoLink.current && !processing) {
|
|
shouldAutoLink.current = false; // Prevent double-trigger
|
|
handleLinkAccount();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [tokenData]);
|
|
|
|
const validateToken = async () => {
|
|
const token = searchParams.get('token');
|
|
const autoLink = searchParams.get('autolink'); // Check if we should auto-link after signin
|
|
|
|
if (!token) {
|
|
setError('No invitation token provided');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Decode token to check if it's for existing user
|
|
const base64Url = token.split('.')[1];
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
const payload = JSON.parse(window.atob(base64));
|
|
|
|
if (payload.prp !== 'student_invite') {
|
|
setError('Invalid invitation token');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (payload.isNewUser !== false) {
|
|
// This is for a new user, redirect to signup
|
|
navigate(`/signup?invite=${token}`, { replace: true });
|
|
return;
|
|
}
|
|
|
|
// Token is valid for existing user
|
|
setTokenData({
|
|
token,
|
|
organizationId: payload.organizationId,
|
|
email: payload.email
|
|
});
|
|
setLoading(false);
|
|
|
|
// If autolink param is present, set flag to trigger auto-link
|
|
if (autoLink === 'true') {
|
|
shouldAutoLink.current = true;
|
|
}
|
|
} catch (err) {
|
|
setError('Invalid invitation token');
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreateSeparateAccount = () => {
|
|
// User needs to contact admin for a new invitation with different email
|
|
navigate('/', { replace: true });
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-aptiva mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Validating invitation...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
|
|
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8">
|
|
<div className="text-center">
|
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
|
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</div>
|
|
<h2 className="mt-4 text-xl font-semibold text-gray-900">Invalid Invitation</h2>
|
|
<p className="mt-2 text-gray-600">{error}</p>
|
|
<button
|
|
onClick={() => navigate('/')}
|
|
className="mt-6 w-full bg-aptiva text-white py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
Go to Home
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4 py-8">
|
|
<div className="max-w-2xl w-full bg-white shadow-lg rounded-lg p-8">
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-3xl font-bold text-gray-900">You're Invited!</h1>
|
|
<p className="mt-2 text-gray-600">Choose how you'd like to proceed</p>
|
|
</div>
|
|
|
|
<div className="bg-blue-50 border-l-4 border-aptiva p-4 mb-6">
|
|
<p className="text-sm text-blue-800">
|
|
<strong>We noticed you already have an AptivaAI account.</strong> You can either link your existing account or create a separate one for your organization.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<p className="text-sm text-red-800">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-4">
|
|
{/* Option 1: Link Existing Account */}
|
|
<div className="border-2 border-gray-200 rounded-lg p-6 hover:border-aptiva transition-colors">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Option 1: Link Your Existing Account</h3>
|
|
<ul className="space-y-2 mb-4 text-sm text-gray-600">
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Your existing data and progress will be preserved
|
|
</li>
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Your organization can view your activity based on your privacy settings
|
|
</li>
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
You'll gain premium access through your organization
|
|
</li>
|
|
</ul>
|
|
<button
|
|
onClick={handleLinkAccount}
|
|
disabled={processing}
|
|
className="w-full bg-aptiva text-white py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{processing ? 'Processing...' : 'Link My Existing Account'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Option 2: Create Separate Account */}
|
|
<div className="border-2 border-gray-200 rounded-lg p-6 hover:border-gray-400 transition-colors">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Option 2: Create a Separate Account</h3>
|
|
<ul className="space-y-2 mb-4 text-sm text-gray-600">
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-blue-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Keep your personal AptivaAI account completely separate
|
|
</li>
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-blue-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
Start fresh with a new profile for school/organization use
|
|
</li>
|
|
<li className="flex items-start">
|
|
<svg className="h-5 w-5 text-orange-500 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
<span><strong>Requires a different email address</strong></span>
|
|
</li>
|
|
</ul>
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded p-3 mb-4 text-xs text-yellow-800">
|
|
<strong>Note:</strong> To create a separate account, you'll need to contact your administrator and provide a different email address. They can then send you a new invitation.
|
|
</div>
|
|
<button
|
|
onClick={handleCreateSeparateAccount}
|
|
disabled={processing}
|
|
className="w-full bg-white border-2 border-gray-300 text-gray-700 py-3 px-4 rounded-lg hover:bg-gray-50 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Contact Administrator
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="mt-6 text-center text-sm text-gray-500">
|
|
Questions? Contact your administrator for help.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|