Skip to main content
Both hooks require <AilitaProvider> and <AuthProvider> ancestors. These are already provided if you used the standard <AilitaProvider> setup — it wraps children with both providers automatically. useAuth provides authentication state and actions. useUser provides profile management for the signed-in user.

useAuth

useAuth is the primary hook for authentication state and actions. It returns the current user, authentication status, and methods to login, logout, and signup.

Return Shape

interface UseAuthReturn {
  user: UserResponse | null
  isAuthenticated: boolean
  isLoading: boolean
  login: (email: string, password?: string, extraData?: Partial<LoginRequest>) => Promise<LoginResponse>
  logout: () => Promise<void>
  signup: (data: Omit<SignupRequest, 'app' | 'mid'>) => Promise<SignupResponse | null>
  error: string | null
  clearError: () => void
}
user
UserResponse | null
The authenticated user object. null while loading or when not authenticated.
This is the authoritative source for user data. After login, useAuth calls getMe() to populate user with the full UserResponse from the server. Do not store the userId from LoginForm.onSuccess and fetch user data yourself — useAuth().user is already populated and stays in sync across components.
isAuthenticated
boolean
true when the user is logged in with valid tokens.
isLoading
boolean
true during silent token refresh on mount and during login/logout operations.
On app mount, isLoading is true for 200–500 ms while the library attempts silent refresh. This is why AuthGuard requires a loadingFallback — without it, the flash-redirect anti-pattern occurs (the user sees a redirect to /login before the refresh completes).
login
(email, password?, extraData?) => Promise<LoginResponse>
Initiates login. Returns the full LoginResponse for branch detection (e.g., checking whether a challenge is required). Tokens are set automatically on a successful password login. extraData passes additional fields (such as loginMode) to the API.
logout
() => Promise<void>
Calls the server logout endpoint, then clears local tokens and auth state. Logout errors are swallowed — local state is always cleaned up.
signup
(data: Omit<SignupRequest, 'app' | 'mid'>) => Promise<SignupResponse | null>
Initiates signup. The app and mid fields are injected from the merchant context automatically — you do not need to pass them.
error
string | null
Last error message from a login, signup, or logout operation. Cleared automatically at the start of the next attempt.
clearError
() => void
Manually clear the error state (useful when navigating away from a form).

Common Patterns

'use client'
import { useAuth } from 'ailita-library'

export default function DashboardPage() {
  const { user, isAuthenticated, logout } = useAuth()

  if (!isAuthenticated) return null // AuthGuard handles redirect

  return (
    <div>
      <h1>Welcome, {user?.firstName}</h1>
      <p>{user?.email}</p>
      <button onClick={logout}>Log out</button>
    </div>
  )
}
user may be null briefly during the initial silent refresh, even inside an AuthGuard-protected route. Always use optional chaining (user?.firstName) or check isAuthenticated first.

Anti-pattern: userId from onSuccess

A common mistake is using the userId returned by onSuccess to fetch user data independently. This creates a redundant network request and a separate copy of user state that can drift from what the library manages.
Do not fetch user data from onSuccess. useAuth().user is already populated after login completes — no additional fetch is needed.
// WRONG — do not do this
<LoginForm
  onSuccess={async (userId) => {
    const response = await fetch(`/api/users/${userId}`)
    const userData = await response.json()
    setUser(userData) // Redundant — useAuth().user already has this
  }}
/>
// CORRECT — useAuth().user is already populated after login
<LoginForm onSuccess={() => navigate('/dashboard')} />

// In your dashboard:
const { user } = useAuth()  // Already populated with full UserResponse

useUser

useUser provides methods for the authenticated user to manage their own profile, password, email, and TOTP settings. It shares the same user object as useAuth — both read from AuthContext, so updates are reflected everywhere immediately.

Return Shape

return {
  user,           // UserResponse | null
  isLoading,      // boolean
  error,          // string | null
  refreshProfile, // () => Promise<UserResponse>
  updateProfile,  // (data: UpdateUserRequest) => Promise<UserResponse>
  updatePassword, // (data: ChangePasswordRequest) => Promise<void>
  startEmailChange, // (newEmail: string) => Promise<EmailChangeResponse>
  initTotp,       // () => Promise<TotpSetupResponse>
  verifyTotp,     // (code: string) => Promise<void>
  removeTotp,     // (code: string) => Promise<void>
  clearError,     // () => void
}
user
UserResponse | null
The same reference as useAuth().user. Reflects the latest profile state.
isLoading
boolean
Loading state for profile operations (not auth loading). true while an update, password change, email change, or TOTP operation is in progress.
error
string | null
Last error message from a profile operation. Cleared automatically on the next attempt.
refreshProfile
() => Promise<UserResponse>
Re-fetches GET /users/me and updates the shared user state. Use this after external changes or to force a sync with the server.
updateProfile
(data: UpdateUserRequest) => Promise<UserResponse>
Updates firstName, lastName, and/or phone. Returns the updated user.
The shared user state is updated automatically — all components reading useAuth().user or useUser().user will reflect the change without a page refresh or manual refetch.
updatePassword
(data: ChangePasswordRequest) => Promise<void>
Changes the user’s password. Requires currentPassword and newPassword. If the user has TOTP enabled, totpCode is also required.
startEmailChange
(newEmail: string) => Promise<EmailChangeResponse>
Initiates an email address change. Returns a challengeId that you pass to ChallengeView to complete verification via the OTP sent to the new address.
initTotp
() => Promise<TotpSetupResponse>
Starts TOTP setup. Returns a qrUri to render as a QR code and backupCodes to display to the user before they scan.
verifyTotp
(code: string) => Promise<void>
Confirms TOTP setup with a 6-digit code from the authenticator app. Automatically calls refreshProfile()user.totpEnabled will be true after this resolves.
removeTotp
(code: string) => Promise<void>
Disables TOTP with a 6-digit verification code. Automatically calls refreshProfile()user.totpEnabled will be false after this resolves.
clearError
() => void
Manually clear the error state.

Profile Update Example

import { useState } from 'react'
import { useUser } from 'ailita-library'

function ProfilePage() {
  const { user, updateProfile, isLoading, error } = useUser()
  const [firstName, setFirstName] = useState(user?.firstName ?? '')

  const handleSave = async () => {
    await updateProfile({ firstName })
    // user state is already updated — no need to refetch
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSave() }}>
      <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />
      <button disabled={isLoading}>{isLoading ? 'Saving...' : 'Save'}</button>
      {error && <p className="text-red-600">{error}</p>}
    </form>
  )
}

Password Change Example

const { updatePassword } = useUser()

await updatePassword({
  currentPassword: 'old-password',
  newPassword: 'new-password',
  totpCode: '123456', // Only required if the user has TOTP enabled
})

Key Types

interface UserResponse {
  id: string
  email: string
  firstName: string
  lastName: string
  phone: string | null
  status: UserStatus
  emailStatus: EmailStatus
  totpEnabled: boolean
  app: string
  mid: string
  createdAt: string
  updatedAt: string
  lastLoginAt?: string | null
  roles?: RoleResponse[]
}

interface UpdateUserRequest {
  firstName?: string
  lastName?: string
  phone?: string
}

interface ChangePasswordRequest {
  currentPassword: string
  newPassword: string
  totpCode?: string
}

interface TotpSetupResponse {
  qrUri: string
  backupCodes: string[]
}

interface EmailChangeResponse {
  challengeId: string
  devToken?: string
}

useSessions

useSessions fetches the authenticated user’s active sessions on mount and exposes revoke methods. It is the hook behind the SessionsList profile component — use it directly when you need programmatic session management rather than the prebuilt UI.

Return Shape

return {
  sessions,   // SessionResponse[]
  isLoading,  // boolean
  error,      // string | null
  refresh,    // () => Promise<void>
  revoke,     // (sessionId: string) => Promise<void>
  revokeAll,  // () => Promise<void>
}
sessions
SessionResponse[]
Array of active sessions for the current user. Each entry includes a current: boolean field that identifies the session belonging to this device/browser.
isLoading
boolean
true while the initial sessions fetch or a revoke operation is in progress.
error
string | null
Last error message from a fetch or revoke operation.
refresh
() => Promise<void>
Re-fetches GET /users/me/sessions and updates the local sessions array.
revoke
(sessionId: string) => Promise<void>
Revokes a single session by ID. Removes the session from local state on success. Throws on error — wrap in try/catch if you need to handle revoke failures.
revokeAll
() => Promise<void>
Revokes all sessions (except the current one, depending on backend behavior). Clears the local sessions array on success. Throws on error.

Session Management Guide

Full useSessions usage guide including onSessionExpired contract and the complete sessions list UI example.

useMerchant

useMerchant is an admin hook for reading and updating the tenant merchant record. It is used inside MerchantSettings — use it directly when you need to read merchant configuration or build custom settings UI.

Return Shape

return {
  merchant,     // MerchantResponse | null
  isLoading,    // boolean
  error,        // string | null
  refresh,      // () => Promise<void>
  update,       // (data: UpdateMerchantRequest) => Promise<MerchantResponse>
  addApiKey,    // (label: string) => Promise<GenerateApiKeyResponse>
  removeApiKey, // (keyId: string) => Promise<void>
  clearError,   // () => void
}
merchant
MerchantResponse | null
The loaded merchant record. null while loading on mount. Contains tenant settings including primaryColor, secondaryColor, loginMode, signupMode, and totpAllowed.
isLoading
boolean
true during the initial fetch and while an update call is in progress.
error
string | null
Last error message from any operation. Clear manually with clearError() or automatically at the start of the next operation.
refresh
() => Promise<void>
Re-fetches GET /merchants/me and updates the local merchant state.
update
(data: UpdateMerchantRequest) => Promise<MerchantResponse>
Updates the merchant record via PATCH /merchants/:id. Returns the updated merchant.
Calling update() automatically applies the updated primaryColor and secondaryColor to the current session via applyTheme(). You do not need to call any theme method manually — the UI updates immediately.
addApiKey
(label: string) => Promise<GenerateApiKeyResponse>
Generates a new API key for this merchant. Throws 'No merchant loaded' if called before mount completes — guard with if (!merchant) return before calling.
removeApiKey
(keyId: string) => Promise<void>
Revokes an API key by its ID. Throws 'No merchant loaded' if the merchant has not loaded yet.
clearError
() => void
Manually clear the error state.

Quick Example

import { useMerchant } from 'ailita-library'

function MerchantNameForm() {
  const { merchant, update, isLoading, error } = useMerchant()

  const handleSave = async (newName: string) => {
    await update({ name: newName })
    // merchant state is updated — no manual refetch needed
  }

  if (!merchant) return null

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleSave(e.currentTarget.name.value) }}>
      <input name="name" defaultValue={merchant.name} />
      <button disabled={isLoading}>{isLoading ? 'Saving...' : 'Save'}</button>
      {error && <p className="text-red-600">{error}</p>}
    </form>
  )
}

useRoles

useRoles is an admin hook for managing role definitions and assigning roles to users. It fetches all roles for the app on mount (both SYSTEM and MERCHANT roles). Use it to build role management UI in admin dashboards.

Return Shape

return {
  roles,     // RoleResponse[]
  isLoading, // boolean
  error,     // string | null
  refresh,   // () => Promise<void>
  add,       // (data: CreateRoleRequest) => Promise<RoleResponse>
  remove,    // (roleId: string) => Promise<void>
  assign,    // (data: AssignRoleRequest) => Promise<void>
  revoke,    // (userId: string, roleId: string) => Promise<void>
}
roles
RoleResponse[]
All roles defined for this app — both SYSTEM (built-in) and MERCHANT (custom) roles.
isLoading
boolean
true during the initial roles fetch.
error
string | null
Last error message from a fetch operation.
refresh
() => Promise<void>
Re-fetches GET /roles and updates the local roles array.
add
(data: CreateRoleRequest) => Promise<RoleResponse>
Creates a new MERCHANT-owned role and appends it to the local roles array. Returns the created role including its generated id.
remove
(roleId: string) => Promise<void>
Deletes a MERCHANT-owned role and removes it from the local array.
Only MERCHANT-owned roles (role.owner === 'MERCHANT') can be deleted. SYSTEM roles are built-in and cannot be removed.
assign
(data: AssignRoleRequest) => Promise<void>
Assigns a role to a user. Accepts an optional expiresAt ISO 8601 string for time-limited role grants. Does NOT update the local roles array — the role list is app-level, not user-level.
revoke
(userId: string, roleId: string) => Promise<void>
Removes a role from a user. Does NOT update the local roles array.

SYSTEM vs MERCHANT Role Hierarchy

OwnerDescriptionCan be deletedExample
SYSTEMBuilt-in roles defined by the ailita backendNoADMIN, USER
MERCHANTCustom roles created by your tenantYes, via remove()MODERATOR, PREMIUM
The priority field determines role precedence when conflicts occur — lower number = higher priority. SYSTEM roles typically have priority 1–10; assign MERCHANT roles priorities starting at 100 to avoid conflicts.

Role Checking vs Role Management

Do not use useRoles to check whether the current user has a specific role. useRoles is for managing role definitions and assignments — not for access control decisions.For route protection, use AuthGuard with requiredRoles:
<AuthGuard fallback={<Navigate to='/login' />} requiredRoles={['ADMIN']}>
  <AdminPanel />
</AuthGuard>
requiredRoles uses AND logic — the user must have ALL listed roles. For programmatic role checks, read useAuthContext().roles directly.

Quick Example: Assigning a Role

import { useMerchant, useRoles } from 'ailita-library'

function AssignRoleButton({ userId, roleId }: { userId: string; roleId: string }) {
  const { merchant } = useMerchant()
  const { assign } = useRoles()

  const handleAssign = async () => {
    if (!merchant) return
    await assign({
      userId,
      roleId,
      app: merchant.app,
      mid: merchant.mid,
    })
  }

  return <button onClick={handleAssign}>Assign role</button>
}
assign() requires app and mid values from useMerchant().merchant. Do not hardcode these — always read them from the loaded merchant record to ensure you are assigning to the correct tenant.

Next steps

Session Management

onSessionExpired contract, useSessions full guide, and active sessions list example.

Merchant & Roles Reference

Deep-dive reference for useMerchant and useRoles with API key management and role assignment patterns.

Auth Flows

Complete login state machine and ChallengeView dual-response contract.