Client SDK Hooks¶
React hooks for authentication, OAuth, invitations, and more.
useAuth Hook¶
The primary hook for accessing auth state and methods:
import { useAuth } from '@authvital/sdk/client';
function Dashboard() {
const {
// ============ STATE ============
isAuthenticated, // boolean: is user logged in?
isLoading, // boolean: is auth state being determined?
isSigningIn, // boolean: is sign-in in progress?
isSigningUp, // boolean: is sign-up in progress?
user, // AuthVitalUser | null
tenants, // AuthVitalTenant[]
currentTenant, // AuthVitalTenant | null
error, // string | null
// ============ AUTH METHODS (redirect to OAuth) ============
login, // (email?, password?) => Promise<LoginResult>
signIn, // alias for login
signUp, // (data?) => Promise<SignUpResult>
signOut, // () => Promise<void>
logout, // alias for signOut
// ============ TENANT METHODS ============
setActiveTenant, // (tenantId: string) => void
switchTenant, // (tenantId: string) => void (alias)
// ============ SESSION METHODS (no-ops, server handles) ============
refreshToken, // () => Promise<void> - server refreshes via cookies
checkAuth, // () => Promise<boolean>
// ============ STATE SETTERS ============
setAuthState, // (user, tenants?) => void
clearAuthState, // () => void
} = useAuth();
// ...
}
State Management¶
Since the Client SDK doesn't call the IDP directly, you need to update auth state after your server verifies the JWT.
Setting Auth State (After Server Verification)¶
import { useAuth } from '@authvital/sdk/client';
// After OAuth callback - your callback route handler
function AuthCallbackPage() {
const { setAuthState } = useAuth();
const navigate = useNavigate();
useEffect(() => {
async function handleCallback() {
// 1. Your server exchanges the code and verifies the JWT
const response = await fetch('/api/auth/callback', {
method: 'POST',
body: JSON.stringify({ code: getCodeFromUrl() }),
credentials: 'include', // Important for cookies!
});
const { user, tenants } = await response.json();
// 2. Update client-side state with verified user data
setAuthState(user, tenants);
// 3. Redirect to dashboard
navigate('/dashboard');
}
handleCallback();
}, []);
return <p>Signing you in...</p>;
}
Clearing Auth State (On Logout)¶
import { useAuth } from '@authvital/sdk/client';
function LogoutButton() {
const { clearAuthState, logout } = useAuth();
const handleLogout = async () => {
// 1. Tell your server to clear the httpOnly cookies
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
// 2. Clear client-side state
clearAuthState();
// OR use the built-in logout which does both:
await logout();
};
return <button onClick={handleLogout}>Sign Out</button>;
}
Hydrating State on Page Load (SSR/Next.js)¶
// pages/_app.tsx (Next.js)
import { AuthVitalProvider } from '@authvital/sdk/client';
function MyApp({ Component, pageProps }) {
// Server passes user data via pageProps
const { user, tenants } = pageProps;
return (
<AuthVitalProvider
authVitalHost={process.env.NEXT_PUBLIC_AUTHVITAL_HOST!}
clientId={process.env.NEXT_PUBLIC_AUTHVITAL_CLIENT_ID!}
initialUser={user}
initialTenants={tenants}
>
<Component {...pageProps} />
</AuthVitalProvider>
);
}
// In getServerSideProps:
export async function getServerSideProps(context) {
const { getCurrentUser } = await import('@authvital/sdk');
// Server verifies the JWT from cookies
const { user, tenants } = await getCurrentUser(context.req);
return {
props: {
user: user || null,
tenants: tenants || [],
},
};
}
useOAuth Hook¶
For custom OAuth flow control:
import { useOAuth } from '@authvital/sdk/client';
function LoginPage() {
const {
isAuthenticated, // boolean
isLoading, // boolean
startLogin, // (options?) => void - redirects to AuthVital login
startSignup, // (options?) => void - redirects to AuthVital signup
logout, // () => Promise<void>
} = useOAuth({
redirectUri: '/api/auth/callback', // optional, has default
});
if (isLoading) {
return <LoadingSpinner />;
}
if (isAuthenticated) {
return <Navigate to="/dashboard" />;
}
return (
<div className="login-buttons">
<button onClick={() => startLogin()}>
Sign In
</button>
<button onClick={() => startLogin({ state: 'custom-state-value' })}>
Sign In (with custom state)
</button>
<button onClick={() => startSignup()}>
Create Account
</button>
<button onClick={() => startSignup({ inviteToken: 'abc123' })}>
Accept Invitation
</button>
</div>
);
}
useOAuth Types¶
interface UseOAuthOptions {
redirectUri?: string; // Override default callback URL
}
interface StartLoginOptions {
state?: string; // Custom state parameter
prompt?: 'login' | 'consent' | 'select_account';
}
interface StartSignupOptions {
state?: string;
inviteToken?: string; // Pre-fill invitation token
}
useInvitation Hook¶
Complete invitation handling flow:
import { useInvitation } from '@authvital/sdk/client';
import { useParams, useNavigate } from 'react-router-dom';
function AcceptInvitePage() {
const { token } = useParams<{ token: string }>();
const navigate = useNavigate();
const {
// ============ STATE ============
invitation, // InvitationDetails | null
isLoading, // boolean
error, // string | null
consumed, // boolean - Has the invite been consumed?
hasPendingInvite, // boolean - Is there a stored invite token?
// ============ METHODS ============
fetchInvitation, // (token: string) => Promise<InvitationDetails>
acceptAndLogin, // (token: string) => void - Stores token, starts OAuth
consumeInvite, // (token: string) => Promise<ConsumeResult>
} = useInvitation({
onConsumed: (result) => {
console.log('Invitation consumed!', result);
navigate('/dashboard');
},
onError: (error) => {
console.error('Invitation error:', error);
},
});
// Fetch invitation details on mount
useEffect(() => {
if (token) {
fetchInvitation(token);
}
}, [token, fetchInvitation]);
if (isLoading) {
return <p>Loading invitation details...</p>;
}
if (error) {
return (
<div className="error">
<h2>Invalid Invitation</h2>
<p>{error}</p>
<button onClick={() => navigate('/login')}>Go to Login</button>
</div>
);
}
if (consumed) {
return (
<div className="success">
<h2>Welcome to {invitation?.tenant.name}!</h2>
<p>Your invitation has been accepted.</p>
<button onClick={() => navigate('/dashboard')}>Go to Dashboard</button>
</div>
);
}
return (
<div className="invitation">
{invitation && (
<>
<h1>You've been invited!</h1>
<p>Join <strong>{invitation.tenant.name}</strong></p>
<p>Role: <strong>{invitation.role}</strong></p>
<button onClick={() => acceptAndLogin(token!)}>
Accept Invitation & Sign In
</button>
</>
)}
</div>
);
}
Invitation Flow¶
1. User clicks invite link → /invite/{token}
2. fetchInvitation(token) → Shows invite details
3. User clicks "Accept"
4. acceptAndLogin(token):
a. Stores token in sessionStorage
b. Redirects to OAuth login/signup
5. After OAuth, your callback:
a. Checks for pending invite token
b. Calls consumeInvite(token)
c. User is added to tenant
6. onConsumed callback fires
7. Redirect to dashboard
useInvitation Types¶
interface InvitationDetails {
id: string;
email: string;
role: string;
expiresAt: string;
tenant: {
id: string;
name: string;
slug: string;
};
invitedBy: {
name: string;
} | null;
}
interface ConsumeResult {
success: boolean;
tenantId: string;
userId: string;
}
interface UseInvitationOptions {
onConsumed?: (result: ConsumeResult) => void;
onError?: (error: string) => void;
}
Helper Hooks¶
Convenience hooks for common patterns:
useUser¶
import { useUser } from '@authvital/sdk/client';
function ProfileCard() {
const user = useUser(); // AuthVitalUser | null
if (!user) return null;
return (
<div className="profile-card">
{user.imageUrl && <img src={user.imageUrl} alt={user.fullName || ''} />}
<h3>{user.fullName || user.email}</h3>
<p>{user.email}</p>
</div>
);
}
useTenant¶
import { useTenant } from '@authvital/sdk/client';
function TenantBanner() {
const tenant = useTenant(); // AuthVitalTenant | null (current tenant)
if (!tenant) return null;
return (
<div className="tenant-banner">
{tenant.imageUrl && <img src={tenant.imageUrl} alt={tenant.name} />}
<span>{tenant.name}</span>
<span className="role">{tenant.role}</span>
</div>
);
}
useTenants¶
import { useTenants } from '@authvital/sdk/client';
function TenantSwitcher() {
const { tenants, currentTenant, switchTenant } = useTenants();
if (tenants.length <= 1) return null;
return (
<select
value={currentTenant?.id || ''}
onChange={(e) => switchTenant(e.target.value)}
>
{tenants.map((tenant) => (
<option key={tenant.id} value={tenant.id}>
{tenant.name}
</option>
))}
</select>
);
}
useAuthVitalConfig¶
Access provider configuration values:
import { useAuthVitalConfig } from '@authvital/sdk/client';
function CustomOAuthButton() {
const { authVitalHost, clientId, redirectUri } = useAuthVitalConfig();
// Build custom OAuth URL
const oauthUrl = `${authVitalHost}/oauth/authorize?` + new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'openid profile email',
});
return (
<a href={oauthUrl} className="custom-login-button">
Sign in with AuthVital
</a>
);
}