Client Integration

Using the auth client package in your app

Overview

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.

Installation

npm install @zooly/auth-client

Authentication Context

AuthContextProvider

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>
  );
}

useAuth Hook

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 user
  • isLoading: boolean - Loading state during user fetch
  • setUser: (user: User | null) => void - Manual user state update
  • refreshUser: () => Promise<void> - Refresh user data from server

LoginPage Component

The main authentication page component handles the complete authentication flow.

Basic Usage

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}
    />
  );
}

Complete Example with All Handlers

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}
    />
  );
}

LoginPage Props

  • returnTo?: string | null - Validated redirect URL after successful authentication
  • onLogin?: (email: string, password: string) => Promise<void> - Handler for email/password login
  • onSignUp?: (email: string, password: string, displayName?: string) => Promise<void> - Handler for user registration
  • onConfirmSignUp?: (email: string, code: string) => Promise<void> - Handler for email verification code confirmation
  • onResendCode?: (email: string) => Promise<void> - Handler for resending verification code
  • onForgotPassword?: (email: string) => Promise<void> - Handler for password reset request
  • onResetPassword?: (email: string, code: string, newPassword: string) => Promise<void> - Handler for password reset completion
  • onSocialLogin?: (provider: "google" | "apple") => void - Handler for social login buttons
  • onSuccess?: (returnTo?: string | null) => void - Callback after successful authentication

Form Components

Individual form components are also available for custom implementations:

  • LoginForm - Email input with "Continue with Email" button
  • PasswordForm - Password input shown after email step
  • SignUpForm - Registration form with email, optional display name, password, and confirm password fields
  • ConfirmSignUpForm - Email verification code input with resend code option
  • ForgotPasswordForm - Password reset request form with success state
  • ResetPasswordForm - Password reset completion form (code + new password)

Utilities

validateReturnTo

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:

  • Only allows relative paths or same-origin URLs
  • Prevents javascript: and data: URLs
  • For local development, allows localhost and relative paths

Theme and Styling

The client package uses shadcn/ui components with a custom Zooly theme:

  • Colors: Defined via CSS variables
    • Primary: Zooly slate (#3D4551)
    • Secondary: Zooly cream (#FAF8F5)
    • Accent: Zooly coral (#D97040)
    • Logo circle: Zooly peach (#FCE8E8)
  • Components: Button, Input, Label, Card, Separator (shadcn/ui components)
  • Responsive: Mobile-first design with Tailwind CSS breakpoints

Mini-App Integration

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.

User Profile Display

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>
  );
}

File Structure

  • packages/auth/client/src/components/auth/LoginPage.tsx - Main login page component
  • packages/auth/client/src/components/auth/LoginForm.tsx - Email login form
  • packages/auth/client/src/components/auth/PasswordForm.tsx - Password entry form
  • packages/auth/client/src/components/auth/SignUpForm.tsx - Registration form
  • packages/auth/client/src/components/auth/ConfirmSignUpForm.tsx - Email verification form
  • packages/auth/client/src/components/auth/ForgotPasswordForm.tsx - Password reset request form
  • packages/auth/client/src/components/auth/ResetPasswordForm.tsx - Password reset completion form
  • packages/auth/client/src/context/AuthContext.tsx - Authentication context provider
  • packages/auth/client/src/hooks/useAuth.ts - Authentication hook
  • packages/auth/client/src/utils/returnTo.ts - ReturnTo validation utility