The @zooly/auth-client package provides React components and utilities for authentication UI. It is framework-agnostic and can be used in Next.js, Vite, or any React application.
npm install @zooly/auth-client
Wrap your app with AuthContextProvider to manage user authentication state:
import { AuthContextProvider } from '@zooly/auth-client';
function App() {
const fetchUser = async () => {
const response = await fetch('https://auth.zooly.ai/api/me', {
credentials: 'include', // Important: include cookies
});
if (response.ok) {
return response.json();
}
return null;
};
return (
<AuthContextProvider fetchUser={fetchUser}>
{/* Your app */}
</AuthContextProvider>
);
}
Access authentication state anywhere in your app:
import { useAuth } from '@zooly/auth-client';
function MyComponent() {
const { user, isLoading, refreshUser } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Not logged in</div>;
}
return (
<div>
<p>Welcome, {user.name}!</p>
<p>Email: {user.email}</p>
<p>Roles: {user.roles.join(', ')}</p>
</div>
);
}
Hook API:
user: User | null - Current authenticated userisLoading: boolean - Loading state during user fetchsetUser: (user: User | null) => void - Manual user state updaterefreshUser: () => Promise<void> - Refresh user data from serverThe main authentication page component handles the complete authentication flow.
import { LoginPage } from '@zooly/auth-client';
function AuthPage() {
const handleLogin = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
};
const handleSuccess = (returnTo?: string | null) => {
if (returnTo) {
window.location.href = returnTo;
} else {
window.location.href = '/';
}
};
return (
<LoginPage
returnTo={new URLSearchParams(window.location.search).get('returnTo')}
onLogin={handleLogin}
onSuccess={handleSuccess}
/>
);
}
import { LoginPage } from '@zooly/auth-client';
function AuthPage() {
const returnTo = new URLSearchParams(window.location.search).get('returnTo');
const handleLogin = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
};
const handleSignUp = async (email: string, password: string, displayName?: string) => {
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, password, displayName }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Sign up failed');
}
};
const handleConfirmSignUp = async (email: string, code: string) => {
const response = await fetch('/api/auth/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, code }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Confirmation failed');
}
};
const handleResendCode = async (email: string) => {
const response = await fetch('/api/auth/resend-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to resend code');
}
};
const handleForgotPassword = async (email: string) => {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to send reset code');
}
};
const handleResetPassword = async (email: string, code: string, newPassword: string) => {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email, code, newPassword }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Password reset failed');
}
};
const handleSocialLogin = (provider: 'google' | 'apple') => {
// Phase 2: Implement social login
console.log(`Social login: ${provider}`);
};
const handleSuccess = (returnTo?: string | null) => {
if (returnTo) {
window.location.href = returnTo;
} else {
window.location.href = '/';
}
};
return (
<LoginPage
returnTo={returnTo}
onLogin={handleLogin}
onSignUp={handleSignUp}
onConfirmSignUp={handleConfirmSignUp}
onResendCode={handleResendCode}
onForgotPassword={handleForgotPassword}
onResetPassword={handleResetPassword}
onSocialLogin={handleSocialLogin}
onSuccess={handleSuccess}
/>
);
}
returnTo?: string | null - Validated redirect URL after successful authenticationonLogin?: (email: string, password: string) => Promise<void> - Handler for email/password loginonSignUp?: (email: string, password: string, displayName?: string) => Promise<void> - Handler for user registrationonConfirmSignUp?: (email: string, code: string) => Promise<void> - Handler for email verification code confirmationonResendCode?: (email: string) => Promise<void> - Handler for resending verification codeonForgotPassword?: (email: string) => Promise<void> - Handler for password reset requestonResetPassword?: (email: string, code: string, newPassword: string) => Promise<void> - Handler for password reset completiononSocialLogin?: (provider: "google" | "apple") => void - Handler for social login buttonsonSuccess?: (returnTo?: string | null) => void - Callback after successful authenticationIndividual form components are also available for custom implementations:
LoginForm - Email input with "Continue with Email" buttonPasswordForm - Password input shown after email stepSignUpForm - Registration form with email, optional display name, password, and confirm password fieldsConfirmSignUpForm - Email verification code input with resend code optionForgotPasswordForm - Password reset request form with success stateResetPasswordForm - Password reset completion form (code + new password)Validates and sanitizes returnTo URL parameters to prevent open redirects:
import { validateReturnTo } from '@zooly/auth-client';
const returnTo = validateReturnTo(searchParams.get('returnTo'));
// Returns validated path or null if invalid
Validation Rules:
javascript: and data: URLslocalhost and relative pathsThe client package uses shadcn/ui components with a custom Zooly theme:
#3D4551)#FAF8F5)#D97040)#FCE8E8)For mini-apps that need inline authentication without redirects:
import { LoginPage } from '@zooly/auth-client';
function MiniApp() {
const { user } = useAuth();
if (!user) {
return (
<div className="mini-app-container">
<LoginPage
onLogin={handleLogin}
onSuccess={() => {
// No redirect - just refresh user state
refreshUser();
}}
/>
</div>
);
}
return <div>Authenticated content</div>;
}
The LoginPage component works seamlessly in both redirect-based and inline scenarios.
After authentication, use the useAuth hook to display user information:
import { useAuth } from '@zooly/auth-client';
function UserProfile() {
const { user } = useAuth();
if (!user) return null;
return (
<div>
{user.image && <img src={user.image} alt={user.name} />}
<h2>{user.name}</h2>
<p>{user.email}</p>
{user.roles.length > 0 && (
<p>Roles: {user.roles.join(', ')}</p>
)}
</div>
);
}
packages/auth/client/src/components/auth/LoginPage.tsx - Main login page componentpackages/auth/client/src/components/auth/LoginForm.tsx - Email login formpackages/auth/client/src/components/auth/PasswordForm.tsx - Password entry formpackages/auth/client/src/components/auth/SignUpForm.tsx - Registration formpackages/auth/client/src/components/auth/ConfirmSignUpForm.tsx - Email verification formpackages/auth/client/src/components/auth/ForgotPasswordForm.tsx - Password reset request formpackages/auth/client/src/components/auth/ResetPasswordForm.tsx - Password reset completion formpackages/auth/client/src/context/AuthContext.tsx - Authentication context providerpackages/auth/client/src/hooks/useAuth.ts - Authentication hookpackages/auth/client/src/utils/returnTo.ts - ReturnTo validation utility