npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

azirid-react

v0.24.1

Published

Authentication components for React and Next.js — Login, Register, powered by TanStack Query and Zod.

Downloads

610

Readme

azirid-react

Authentication, billing, and referral components and hooks for React and Next.js — powered by TanStack Query and Zod.

Drop-in <LoginForm>, <SignupForm>, <PayButton> and more, or use the headless hooks to build fully custom UIs.

Installation

npm install azirid-react
# or
pnpm add azirid-react
# or
yarn add azirid-react

Peer dependencies

npm install react react-dom @tanstack/react-query

No Tailwind, no CSS import, no extra setup required. The built-in auth forms (<LoginForm>, <SignupForm>, <ForgotPasswordForm>, <ResetPasswordForm>) render inside a Shadow DOM since v0.21 — their styles are fully isolated from your app and ship compiled inside the component. Drop them into any project, any framework config, any CSS reset — they will look identical and nothing from your host stylesheet can leak in.


Quick Start — Next.js (App Router)

1. Create a client wrapper for the provider

<AziridProvider> is a client component. In the App Router the root layout.tsx is a server component, so you need a 'use client' wrapper that you can mount from the server layout.

// app/providers.tsx
'use client'

import { AziridProvider } from 'azirid-react'
import type { ReactNode } from 'react'

export function AziridClientProvider({ children }: { children: ReactNode }) {
  return (
    <AziridProvider publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}>
      {children}
    </AziridProvider>
  )
}

2. Mount it from the root layout

// app/layout.tsx — server component, no "use client"
import { AziridClientProvider } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AziridClientProvider>{children}</AziridClientProvider>
      </body>
    </html>
  )
}

Why the wrapper? Importing <AziridProvider> directly into a server layout works in most cases, but the wrapper makes the client boundary explicit and prevents subtle hydration bugs when the layout grows.

Note: You do not need to import 'azirid-react/styles.css'. The auth forms carry their own Shadow DOM + compiled CSS. The ./styles.css export still exists for the few light-DOM components (<PricingCard>, <PayButton>, etc.), but <AziridProvider> auto-injects those styles at mount too — so you can forget about it.

3. Protect routes with <AuthGuard> and <GuestGuard>

Because the SDK calls api.azirid.com cross-origin, auth cookies live on the Azirid domain — not on yours — so server-side middleware can't gate routes. Use the client-side guards shipped under azirid-react/next/guards instead.

// app/(protected)/layout.tsx — wraps /dashboard, /admin, etc.
import { AuthGuard } from 'azirid-react/next/guards'

export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
  return <AuthGuard>{children}</AuthGuard>
}
// app/(auth)/layout.tsx — wraps /login, /register, /forgot-password, etc.
import { GuestGuard } from 'azirid-react/next/guards'

export default function AuthLayout({ children }: { children: React.ReactNode }) {
  return <GuestGuard>{children}</GuestGuard>
}

AuthGuard waits for bootstrap, then redirects unauthenticated users to /login?redirect=<original-path>. GuestGuard does the inverse and honors the ?redirect= param so users land back where they came from after logging in.

4. Add the login page

// app/login/page.tsx
import { LoginForm } from 'azirid-react'

export default function LoginPage() {
  return <LoginForm />
}

5. Configure your environment

# .env.local
NEXT_PUBLIC_AZIRID_PK=pk_live_...

Restart the dev server after adding or changing NEXT_PUBLIC_* variables so Next.js picks them up.

Troubleshooting — login submit does nothing

If the form submits but no request appears in the Network tab:

  1. Confirm <AziridProvider> actually wraps the page containing <LoginForm>. Missing provider ⇒ <LoginForm> throws a descriptive error in development (added in 0.20.2).
  2. Confirm NEXT_PUBLIC_AZIRID_PK is set and the dev server was restarted. Missing publishable key ⇒ the provider logs a console.error on mount.
  3. Check the browser console for a useAzirid warning — it pinpoints a provider/component mismatch.

Cross-origin session handling (zero-config from 0.20.4)

Most consumers of azirid-react run their app on their own domain (e.g. myapp.com) while talking to api.azirid.com. Modern browsers — Safari ITP, Firefox ETP, Chrome Privacy Sandbox — treat cookies set by api.azirid.com as third-party and block them by default, which would break session bootstrap.

The SDK handles this automatically. At mount it inspects apiUrl vs. window.location.host and picks the right transport:

| Layout | SDK behavior | Where the refresh token lives | |---|---|---| | myapp.comapi.azirid.com | body-carried refresh token | In-memory + sent in request body | | app.acme.comapi.acme.com | HttpOnly cookie (default) | _asid cookie | | localhost:3001localhost:3000 | HttpOnly cookie (default) | _asid cookie |

No flag is required. If you want to override the auto-detection (e.g. for an unusual reverse-proxy setup) you can still pass crossOriginMode explicitly:

// Force body-RT
<AziridProvider publishableKey={…} crossOriginMode>…</AziridProvider>

// Force cookies (and accept that third-party browsers will break)
<AziridProvider publishableKey={…} crossOriginMode={false}>…</AziridProvider>

Under the hood, when the SDK picks the body-carried mode:

  • The login response includes the refresh token in the body.
  • The SDK persists it to sessionStorage so the session survives full-page reloads (window.location.href = "/dashboard" after login, F5, etc.).
  • bootstrap, refresh, and logout send { rt: "…" } in the POST body.
  • The backend's CSRF guard accepts the body token as proof of session when the cookie is missing — no extra server-side configuration required.
  • sessionStorage is scoped to the tab, cleared automatically when the tab closes, and isolated per origin. Tokens rotate on every refresh and are bound to the device-id.

Quick Start — React SPA (Vite)

1. Wrap your app with <AziridProvider>

// main.tsx
import { AziridProvider } from 'azirid-react'

createRoot(document.getElementById('root')!).render(
  <AziridProvider publishableKey={import.meta.env.VITE_AZIRID_PK}>
    <App />
  </AziridProvider>,
)

2. Configure your environment

VITE_AZIRID_PK=pk_live_...

3. Use the forms or hooks

import { LoginForm } from 'azirid-react'

export default function LoginPage() {
  return <LoginForm />
}

Google sign-in (zero-config from 0.23)

Google sign-in is configured per App by the customer admin in Dashboard → Social Providers. Once enabled, <LoginForm> and <AuthForm> automatically render a "Continue with Google" button — no SDK code changes required.

What the integrator (you) needs to do

Add <OAuthCallbackHandler /> somewhere inside <AziridProvider>. It detects ?ticket=… in the URL after the Google round-trip, exchanges the ticket for session tokens, strips the URL params, and triggers a hard reload so the provider re-bootstraps with the fresh session. It is silent when the URL has no OAuth params, so it's safe to mount once at the app shell.

'use client'

import { AziridProvider, OAuthCallbackHandler } from 'azirid-react'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AziridProvider publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}>
      <OAuthCallbackHandler />
      {children}
    </AziridProvider>
  )
}

That's it. The button appears automatically the moment your customer admin enables Google in the dashboard, and clicking it walks the user through the full Authorization Code + PKCE flow without any client-side OAuth library.

What the customer admin needs to do (one time, per App)

  1. Open https://console.cloud.google.com/apis/credentials and create an OAuth 2.0 Client ID of type "Web application".
  2. On the OAuth Consent Screen, register the scopes: openid, email, profile.
  3. Under Authorized redirect URIs, paste the value the dashboard shows you on the Social Providers page. It looks like https://api.azirid.com/v1/oauth/google/callback.
  4. Copy the Client ID and Client Secret from Google and paste them into the dashboard. Toggle Enable Google sign-in and save.

Customizing the button

If the built-in button doesn't fit your design, opt out by passing showSocialButtons={false} and build your own using useOAuthStartUrl:

import { LoginForm, useOAuthStartUrl } from 'azirid-react'

function CustomLogin() {
  const googleUrl = useOAuthStartUrl('google')
  return (
    <>
      <button onClick={() => googleUrl && (window.location.href = googleUrl)}>
        Continue with Google
      </button>
      <LoginForm showSocialButtons={false} />
    </>
  )
}

Forcing or hiding the auto-render

showSocialButtons accepts three states:

  • omitted (default) — the SDK asks the API which providers are enabled and renders only those.
  • true — render the buttons regardless of API state. Useful in design previews.
  • false — never render the buttons. Useful when you want a fully custom layout.

Components

LoginForm / SignupForm

See Quick Start examples above.

ForgotPasswordForm

Pre-built form that sends a password reset email.

import { ForgotPasswordForm } from 'azirid-react'

export default function ForgotPasswordPage() {
  return <ForgotPasswordForm onSubmit={(data) => console.log('Reset requested for:', data.email)} />
}

| Prop | Type | Default | Description | | --------------- | ----------------------------------- | -------- | --------------------------------- | | onSubmit | (data: { email: string }) => void | — | Override default submit behavior | | schema | ZodType | auto | Custom Zod schema for validation | | isLoading | boolean | — | Show loading state / disable form | | error | string \| null | — | Error message to display | | title | string | i18n | Form title | | description | string | i18n | Subtitle below the title | | logo | ReactNode | branding | Logo above the card | | submitText | string | i18n | Submit button text | | footer | ReactNode | — | Content below the form | | defaultValues | { email?: string } | — | Pre-fill the email field | | className | string | — | Additional CSS classes |

ResetPasswordForm

Pre-built form for confirming a password reset with a token.

import { ResetPasswordForm } from 'azirid-react'

export default function ResetPasswordPage({ searchParams }: { searchParams: { token: string } }) {
  return <ResetPasswordForm token={searchParams.token} onSuccess={() => router.push('/login')} />
}

| Prop | Type | Default | Description | | ------------- | -------------------------------------------------------- | -------- | -------------------------------------- | | token | string | — | Required. Reset token from the URL | | onSubmit | (data: { token: string; newPassword: string }) => void | — | Override default submit | | schema | ZodType | auto | Custom Zod schema | | isLoading | boolean | — | Show loading state | | error | string \| null | — | Error message | | title | string | i18n | Form title | | description | string | i18n | Subtitle | | logo | ReactNode | branding | Logo above the card | | submitText | string | i18n | Submit button text | | footer | ReactNode | — | Content below the form | | onSuccess | () => void | — | Called after successful reset | | className | string | — | Additional CSS classes |

AuthForm

Multi-view auth component that wraps LoginForm, SignupForm, ForgotPasswordForm, and ResetPasswordForm with built-in navigation between views.

import { AuthForm } from 'azirid-react'

// Uncontrolled — manages its own view state
<AuthForm defaultView="login" />

// Controlled — parent manages the view
const [view, setView] = useState<AuthView>('login')
<AuthForm view={view} onViewChange={setView} />

| Prop | Type | Default | Description | | --------------------- | --------------------------------------- | --------- | -------------------------------------------------------------------------------- | | view | AuthView | — | Controlled view ('login' \| 'signup' \| 'forgot-password' \| 'reset-password') | | onViewChange | (view: AuthView) => void | — | Callback when navigation links are clicked (controlled mode) | | defaultView | AuthView | 'login' | Initial view for uncontrolled mode | | logo | ReactNode | — | Logo passed to all child forms | | showSocialButtons | boolean | auto | Force-show or hide social-login buttons. When omitted, the form asks the API which providers the customer admin enabled in Dashboard → Social Providers and renders only those (currently Google). | | hideNavigation | boolean | — | Hide navigation links between views | | resetToken | string | — | Token for the reset-password view | | defaultValues | { email?: string; password?: string } | — | Default values passed to child forms | | loginProps | Partial<LoginFormProps> | — | Props forwarded to LoginForm | | signupProps | Partial<SignupFormProps> | — | Props forwarded to SignupForm | | forgotPasswordProps | Partial<ForgotPasswordFormProps> | — | Props forwarded to ForgotPasswordForm | | resetPasswordProps | Partial<ResetPasswordFormProps> | — | Props forwarded to ResetPasswordForm | | className | string | — | Additional CSS classes |

OAuthCallbackHandler (since 0.23)

Headless component that completes the Google sign-in round-trip. Mount it once inside <AziridProvider>. It does nothing until it sees ?ticket=… (or ?oauth_error=…) in the URL, at which point it exchanges the ticket server-side, lets the API set the session cookies, strips the params, and reloads.

import { AziridProvider, OAuthCallbackHandler } from 'azirid-react'

<AziridProvider publishableKey={pk}>
  <OAuthCallbackHandler />
  {/* …rest of your app */}
</AziridProvider>

| Prop | Type | Default | Description | | ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ | | redirectUrl | string | <current URL cleaned> | Where to send the user after success. Default: same URL minus the OAuth params. | | onSuccess | (user: unknown) => void | — | Called with the user object after a successful exchange. | | onError | (error: Error) => void | — | Called when the provider returns an error or the ticket exchange fails. | | loadingText | string | Signing you in… | Copy shown while exchanging the ticket. | | errorText | string | generic | Fallback copy when the exchange fails and there is no specific error message. | | silentWhenIdle | boolean | true | When true the component renders nothing while idle. Set to false to render a placeholder even when the URL has no OAuth params. |


Headless hooks

All hooks require <AziridProvider> in the tree.

useAzirid — session state

import { useAzirid } from 'azirid-react'

function Navbar() {
  const { user, isAuthenticated, isLoading, login, logout } = useAzirid()

  if (isLoading) return <Spinner />

  return isAuthenticated ? (
    <div>
      <span>Hello, {user!.email}</span>
      <button onClick={logout}>Sign out</button>
    </div>
  ) : (
    <button onClick={() => login({ email: '...', password: '...' })}>Sign in</button>
  )
}

isLoading is true during session bootstrap (page load/refresh), login, and signup. Use it to show a loading state and prevent rendering authenticated UI before the session is ready. This ensures no API calls are made without a valid token.

Auth Guard pattern (Next.js)

import { useAzirid } from 'azirid-react'

function AuthGuard({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAzirid()

  if (isLoading) return <LoadingSpinner />
  if (!isAuthenticated) return <RedirectToLogin />

  return <>{children}</>
}

useAziridOptional — non-throwing context read

Same shape as useAzirid(), but returns null when no <AziridProvider> is mounted instead of throwing. Use it in components that need to work with or without the provider (e.g. a form that optionally auto-submits to the API if a provider is present).

import { useAziridOptional } from 'azirid-react'

function OptionalSubmitter() {
  const ctx = useAziridOptional()
  if (!ctx) return <p>No provider — render in "offline" mode</p>
  return <button onClick={() => ctx.login({ email: '…', password: '…' })}>Sign in</button>
}

useLogin

import { useLogin } from 'azirid-react'

function CustomLoginForm() {
  const { login, isLoading, error } = useLogin({
    onSuccess: (data) => console.log(data.user),
    onError: (msg) => console.error(msg),
  })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const fd = new FormData(e.currentTarget)
        login({
          email: fd.get('email') as string,
          password: fd.get('password') as string,
        })
      }}
    >
      <input name="email" type="email" />
      <input name="password" type="password" />
      {error && <p>{error}</p>}
      <button disabled={isLoading}>Sign in</button>
    </form>
  )
}

useSignup

import { useSignup } from 'azirid-react'

const { signup, isLoading, error } = useSignup({
  onSuccess: (data) => console.log('Registered:', data.user),
})

signup({ email: '[email protected]', password: 'secret' })

useLogout

import { useLogout } from 'azirid-react'

const { logout, isLoading } = useLogout({
  onSuccess: () => router.push('/login'),
})

useSession

import { useSession } from 'azirid-react'

const { user, accessToken, isAuthenticated } = useSession()

useMagicLink

import { useMagicLink } from 'azirid-react'

const { requestMagicLink, verifyMagicLink, isLoading } = useMagicLink()

requestMagicLink({ email: '[email protected]' })
verifyMagicLink({ token: '...' })

useSocialLogin

import { useSocialLogin } from 'azirid-react'

const { loginWithProvider, isLoading } = useSocialLogin()

loginWithProvider({ provider: 'google' }) // "google" | "github"

Low-level hook — assumes you already obtained a providerAccountId from the provider yourself. For the standard server-driven flow (recommended) see useOAuthProviders + <OAuthCallbackHandler> below.

useOAuthProviders — list enabled social providers (since 0.23)

import { useOAuthProviders } from 'azirid-react'

const { data: providers, isLoading } = useOAuthProviders()
// → [{ provider: 'google', enabled: true }]

Returns the social-login providers the customer admin enabled for the current App (configured in Dashboard → Social Providers). The hook reads the publishable key from <AziridProvider> and resolves the App + environment server-side.

<LoginForm> calls this hook internally to decide which buttons to render. Use it directly only if you need a custom button layout.

useOAuthStartUrl — build a redirect URL for a custom button (since 0.23)

import { useOAuthStartUrl } from 'azirid-react'

const googleUrl = useOAuthStartUrl('google')

<button onClick={() => googleUrl && (window.location.href = googleUrl)}>
  Custom Google button
</button>

Builds the absolute URL the SDK should redirect to (<API>/v1/oauth/google/start?publishable_key=…). Use it when you don't want the built-in <LoginForm> button.

usePasskeys

import { usePasskeys } from 'azirid-react'

const { passkeys, registerPasskey, removePasskey, isLoading } = usePasskeys()

useChangePassword

import { useChangePassword } from 'azirid-react'

const { changePassword, isLoading, error } = useChangePassword()

changePassword({ currentPassword: 'old', newPassword: 'new' })

useBootstrap

Manually re-run the session bootstrap (useful after SSO redirects).

import { useBootstrap } from 'azirid-react'

const { bootstrap, isBootstrapping } = useBootstrap()

Note: The automatic bootstrap uses request deduplication — if React 18 Strict Mode (or any other scenario) triggers multiple bootstrap calls, only one HTTP request is made and all callers await the same promise. This prevents token rotation conflicts that would otherwise invalidate the session on page reload in development mode. During bootstrap, isLoading from useAzirid() is true, so components using the Auth Guard pattern will show a loading state until the session is restored. Hooks like usePayphoneCheckout, useSession, and <PayphoneCallback> also wait for bootstrap to complete before making authenticated requests.

useRefresh

Manually refresh the access token.

import { useRefresh } from 'azirid-react'

const { refresh } = useRefresh()

useAccessToken

Returns the current access token, or null when unauthenticated. Use it to pass the token to an external API client (e.g. an openapi-fetch instance) that is NOT routed through useAccessClient(). The value re-renders on bootstrap, refresh, tenant switch, and logout.

import { useAccessToken } from 'azirid-react'

function useMyApiClient() {
  const at = useAccessToken()
  return useMemo(() => createExternalClient({ getAuthToken: () => at }), [at])
}

useAccessTokenSync

Fires callback(current, previous) on every access-token transition (refresh, tenant switch, logout). The callback is not invoked on initial mount — only on subsequent changes. Typical use: bridge the provider to a TanStack Query client so a logout (string → null) clears cached data while silent refreshes (string → new string) do not clobber the cache.

import { useAccessTokenSync } from 'azirid-react'

useAccessTokenSync((current, previous) => {
  if (previous !== null && current === null) {
    queryClient.clear()
  }
})

useAuthStatus

Collapses isBootstrapping / isLoading / user into a single discriminant — convenient for guards and conditional UI.

import { useAuthStatus } from 'azirid-react'

const status = useAuthStatus() // 'loading' | 'authenticated' | 'unauthenticated'

if (status === 'loading') return <Spinner />
if (status === 'unauthenticated') return <LoginForm />
return <Dashboard />

This is the hook that backs <AuthGuard> and <GuestGuard> under azirid-react/next/guards.

useAccessClient

Access the raw AccessClient instance for custom API calls.

import { useAccessClient } from 'azirid-react'

function CustomAction() {
  const client = useAccessClient()

  async function fetchCustomData() {
    const data = await client.get('/v1/custom-endpoint')
    console.log(data)
  }

  return <button onClick={fetchCustomData}>Fetch</button>
}

useFormState

Headless form hook with Zod validation. Powers the built-in form components — use it to build fully custom forms.

import { useFormState, loginSchema } from 'azirid-react'

function CustomForm() {
  const { values, errors, isSubmitting, handleChange, handleSubmit, reset } = useFormState(
    { email: '', password: '' },
    loginSchema,
    async (values) => {
      // Submit logic
    },
  )

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email} onChange={handleChange('email')} />
      {errors.find((e) => e.field === 'email')?.message}
      <button disabled={isSubmitting}>Submit</button>
    </form>
  )
}

usePasswordToggle

Simple toggle between "password" and "text" input types.

import { usePasswordToggle } from 'azirid-react'

function PasswordInput() {
  const { visible, toggle, type } = usePasswordToggle()

  return (
    <div>
      <input type={type} name="password" />
      <button type="button" onClick={toggle}>
        {visible ? 'Hide' : 'Show'}
      </button>
    </div>
  )
}

usePasswordReset

Two-step password reset flow: request a reset email, then confirm with the token.

import { usePasswordReset } from 'azirid-react'

function PasswordResetFlow() {
  const passwordReset = usePasswordReset({
    onRequestSuccess: () => toast.success('Check your email'),
    onConfirmSuccess: () => router.push('/login'),
    onError: (err) => console.error(err),
  })

  // Step 1: request reset email
  passwordReset.request.mutate({ email: '[email protected]' })

  // Step 2: confirm with token from URL
  passwordReset.confirm.mutate({ token: 'abc', newPassword: 'newPass123!' })
}

Zod Schemas

The SDK exports pre-built Zod schemas with Spanish validation messages, plus factory functions for custom messages.

import {
  loginSchema,
  signupSchema,
  changePasswordSchema,
  magicLinkRequestSchema,
  magicLinkVerifySchema,
  socialLoginSchema,
  passkeyRegisterStartSchema,
  forgotPasswordSchema,
  resetPasswordConfirmSchema,
} from 'azirid-react'

// Default schemas (Spanish messages)
loginSchema.parse({ email: '[email protected]', password: 'secret' })

// Factory functions for custom messages
import {
  createLoginSchema,
  createSignupSchema,
  createForgotPasswordSchema,
  createResetPasswordConfirmSchema,
} from 'azirid-react'

const customSchema = createLoginSchema({
  emailRequired: 'Email is required',
  emailInvalid: 'Must be a valid email',
  passwordRequired: 'Password is required',
  passwordMinLength: 'At least 10 characters',
})

| Schema | Fields | | ---------------------------- | --------------------------------------------------------------------- | | loginSchema | email, password | | signupSchema | email, password, acceptedTosVersion?, acceptedPrivacyVersion? | | changePasswordSchema | currentPassword, newPassword | | magicLinkRequestSchema | email | | magicLinkVerifySchema | token | | socialLoginSchema | provider, providerAccountId, email, emailVerified?, ... | | passkeyRegisterStartSchema | deviceName? | | forgotPasswordSchema | email | | resetPasswordConfirmSchema | token, newPassword |


Billing & Payments

All billing hooks require <AziridProvider> in the tree and an authenticated user.

usePlans

Fetch all available billing plans for the current app.

import { usePlans } from 'azirid-react'

function PricingPage() {
  const { data: plans, isLoading } = usePlans()

  if (isLoading) return <Spinner />

  return (
    <ul>
      {plans?.map((plan) => (
        <li key={plan.id}>
          {plan.name} — ${(plan.amount / 100).toFixed(2)}/{plan.interval.toLowerCase()}
        </li>
      ))}
    </ul>
  )
}

useSubscription

Get the current user's active subscription.

import { useSubscription } from 'azirid-react'

function SubscriptionStatus() {
  const { data: sub } = useSubscription()

  if (!sub) return <p>No active subscription</p>

  return (
    <p>
      {sub.plan.name} — {sub.status}
      {sub.cancelAtPeriodEnd && ' (cancels at period end)'}
    </p>
  )
}

useCheckout

Initiate a checkout session. Auto-redirects to the payment provider on success (unless the provider is MANUAL_TRANSFER or PAYPHONE).

import { useCheckout } from 'azirid-react'

function UpgradeButton({ planId }: { planId: string }) {
  const { checkout, isPending } = useCheckout({
    onSuccess: (data) => console.log('Checkout created:', data),
    onError: (err) => console.error(err),
  })

  return (
    <button
      disabled={isPending}
      onClick={() =>
        checkout({
          planId,
          successUrl: `${window.location.origin}/billing?success=true`,
          cancelUrl: `${window.location.origin}/billing`,
        })
      }
    >
      Upgrade
    </button>
  )
}

useInvoices

List all invoices for the authenticated user.

import { useInvoices } from 'azirid-react'

function InvoiceHistory() {
  const { data: invoices } = useInvoices()

  return (
    <ul>
      {invoices?.map((inv) => (
        <li key={inv.id}>
          ${(inv.amount / 100).toFixed(2)} — {inv.status}
          {inv.invoiceUrl && <a href={inv.invoiceUrl}>View</a>}
        </li>
      ))}
    </ul>
  )
}

usePaymentProviders

Fetch available payment providers for the app (e.g., Stripe, PayPal, manual transfer).

import { usePaymentProviders } from 'azirid-react'

const { data: providers } = usePaymentProviders()
// [{ provider: 'STRIPE', checkout: true, subscriptions: true }, ...]

useBranches

List the branches configured for the current app (used for multi-location billing, e.g. a restaurant with several points of sale).

import { useBranches } from 'azirid-react'

const { data: branches, isLoading } = useBranches()
// [{ id, name, address?, ... }, ...]

usePaymentMethods

List the payment methods configured for the current app, optionally filtered by branchId.

import { usePaymentMethods } from 'azirid-react'

// All methods for the app
const { data: methods } = usePaymentMethods()

// Only methods available at a specific branch
const { data: branchMethods } = usePaymentMethods('branch_123')

useUploadTransferProof

Upload a file (image or PDF) to S3 via presigned URL for use as transfer proof.

import { useUploadTransferProof } from 'azirid-react'

const { upload, isUploading, publicUrl, error, reset } = useUploadTransferProof({
  onSuccess: (result) => console.log('Uploaded:', result.publicUrl),
  onError: (err) => console.error(err),
})

// Upload a file (returns { uploadUrl, publicUrl })
const result = await upload(file) // File from <input type="file">

Accepted file types: PNG, JPG, WebP, PDF (max 10MB)

useSubmitTransferProof

Submit proof of a manual bank transfer payment (requires a pre-uploaded file URL).

import { useSubmitTransferProof } from 'azirid-react'

const { submit, isPending } = useSubmitTransferProof({
  onSuccess: (proof) => console.log('Proof submitted:', proof.id),
})

submit({
  intentId: 'intent_123',
  fileUrl: 'https://storage.example.com/receipt.pdf',
  amount: 29.99,
  currency: 'USD',
  notes: 'Bank transfer from Account #1234',
})

useTransferProofs

List submitted transfer proofs and their review status.

import { useTransferProofs } from 'azirid-react'

const { data: proofs } = useTransferProofs()
// [{ id, status: 'PENDING_REVIEW' | 'APPROVED' | 'REJECTED', ... }]

usePayphoneCheckout

Hook that returns a renderable widget component and payment status. Pass an intentId and get full control over where the widget renders and how to react to the payment result.

import { usePayphoneCheckout } from 'azirid-react'

function CheckoutPage({ intentId }: { intentId: string }) {
  const { PayphoneWidget, status, error } = usePayphoneCheckout({
    intentId,
    onSuccess: (data) => {
      // data: { status: 'confirmed' | 'cancelled' | 'already_confirmed', intentId: string }
      console.log('Payment result:', data.status)
    },
    onError: (err) => console.error(err),
  })

  return (
    <div>
      <h1>Pay</h1>
      <PayphoneWidget />
      {status === 'loading' && <p>Preparing checkout...</p>}
      {status === 'confirming' && <p>Confirming payment...</p>}
      {status === 'confirmed' && <p>Payment confirmed!</p>}
      {status === 'cancelled' && <p>Payment was cancelled</p>}
      {status === 'error' && <p>Error: {error?.message}</p>}
    </div>
  )
}

| Option | Type | Description | |--------|------|-------------| | intentId | string | Required. Payment intent ID | | onSuccess | (data) => void | Called after payment confirmation | | onError | (error) => void | Called on any error |

| Return | Type | Description | |--------|------|-------------| | PayphoneWidget | () => ReactElement \| null | Component to render the Payphone widget | | status | PayphoneCheckoutStatus | 'idle' \| 'loading' \| 'ready' \| 'confirming' \| 'confirmed' \| 'cancelled' \| 'error' | | intentId | string | The intent ID passed in | | error | Error \| null | Error details if status is 'error' |

The hook handles both phases automatically:

  1. Widget phase: Calls checkout API, loads Payphone SDK, widget renders via <PayphoneWidget />.
  2. Confirmation phase: After Payphone redirects back to your configured Response URL (set in Payphone Developer dashboard), detects ?id=X&clientTransactionId=Y params and confirms the payment.

Important: The redirect URL after payment is configured in the Payphone Developer dashboard (not passed via code). Make sure your Response URL points to the page where usePayphoneCheckout or <PayphoneCallback> is rendered.

Session safety: The confirmation request waits for the session bootstrap to complete before firing. This prevents 401 errors when Payphone redirects back and the page reloads — the SDK restores the session first, then confirms the payment.

usePayButton

Hook that returns renderable components and state for a complete payment flow. Supports all providers (Stripe, PayPal, Payphone, Manual Transfer, Nuvei). You control the layout.

import { usePayButton } from 'azirid-react'

function CheckoutPage({ intentId }: { intentId: string }) {
  const {
    providers, checkout, ProviderSelector,
    TransferDetails, PayphoneWidget, status, error,
  } = usePayButton({
    intentId,
    onSuccess: (data) => console.log('Paid!', data),
    onError: (err) => console.error(err),
  })

  return (
    <div>
      {/* Option A: built-in provider selector */}
      <ProviderSelector />

      {/* Option B: custom UI with providers array */}
      {providers.map(p => (
        <button key={p.provider} onClick={() => checkout(p.provider)}>
          {p.provider}
        </button>
      ))}

      {/* Renders automatically based on selected provider */}
      <PayphoneWidget />
      <TransferDetails />

      {status === 'processing' && <p>Processing...</p>}
      {status === 'redirecting' && <p>Redirecting to payment...</p>}
      {status === 'error' && <p>Error: {error?.message}</p>}
    </div>
  )
}

| Option | Type | Description | |--------|------|-------------| | intentId | string | Payment intent ID | | planId | string | Plan ID (alternative to intentId) | | onSuccess | (data) => void | Called after successful payment | | onError | (error) => void | Called on any error |

| Return | Type | Description | |--------|------|-------------| | providers | AvailableProvider[] | Enabled payment providers | | checkout | (provider) => void | Trigger checkout for a specific provider | | ProviderSelector | Component | Built-in provider selection list | | TransferDetails | Component | Bank transfer details (after MANUAL_TRANSFER checkout) | | PayphoneWidget | Component | Payphone widget (after PAYPHONE checkout) | | status | PayButtonStatus | 'idle' \| 'loading_providers' \| 'ready' \| 'processing' \| 'redirecting' \| 'success' \| 'error' | | selectedProvider | PaymentProviderType \| null | Currently selected provider | | checkoutData | CheckoutResponse \| null | Full checkout response | | isPending | boolean | Checkout API call in progress | | error | Error \| null | Error details |

useTransferPayment

Hook for manual bank transfer payments. Pass an intentId and get a component with bank details + functions to upload and submit proof of payment.

import { useTransferPayment } from 'azirid-react'

function TransferPage({ intentId }: { intentId: string }) {
  const {
    TransferDetails, uploadAndSubmitProof, status, error,
  } = useTransferPayment({
    intentId,
    onProofSubmitted: (proof) => console.log('Proof sent:', proof.id),
    onError: (err) => console.error(err),
  })

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (file) uploadAndSubmitProof(file, 'Transfer ref #123')
  }

  return (
    <div>
      <TransferDetails />
      {status === 'ready' && <input type="file" accept="image/*,.pdf" onChange={handleFileChange} />}
      {status === 'uploading' && <p>Uploading...</p>}
      {status === 'submitting' && <p>Submitting...</p>}
      {status === 'submitted' && <p>Proof submitted!</p>}
      {status === 'error' && <p>Error: {error?.message}</p>}
    </div>
  )
}

| Option | Type | Description | |--------|------|-------------| | intentId | string | Required. Payment intent ID | | onSuccess | (data) => void | Called after checkout completes | | onProofSubmitted | (proof) => void | Called after proof is submitted | | onError | (error) => void | Called on any error |

| Return | Type | Description | |--------|------|-------------| | TransferDetails | Component | Amount, bank details, and transfer instructions | | uploadAndSubmitProof | (file: File, notes?: string) => Promise<void> | Upload file to S3 and submit proof in one step | | submitProof | (data) => void | Submit transfer proof with pre-uploaded file URL | | status | TransferPaymentStatus | 'idle' \| 'loading' \| 'ready' \| 'uploading' \| 'submitting' \| 'submitted' \| 'error' | | checkoutData | CheckoutResponse \| null | Full checkout response | | isUploading | boolean | File upload in progress | | isSubmitting | boolean | Proof submission in progress | | error | Error \| null | Error details |

usePayphoneConfirm

Confirm a Payphone payment callback. Used on the Payphone return URL page.

import { usePayphoneConfirm } from 'azirid-react'

const confirm = usePayphoneConfirm({
  onSuccess: (data) => console.log('Payment confirmed:', data.status),
})

confirm.mutate({ id: 12345, clientTransactionId: 'txn_abc' })

Billing Components

PricingTable

Drop-in pricing grid with checkout flow integrated.

import { PricingTable } from 'azirid-react'
;<PricingTable
  successUrl={`${window.location.origin}/billing?success=true`}
  cancelUrl={`${window.location.origin}/billing`}
  columns={3}
  onPlanSelect={(plan) => console.log('Selected:', plan.name)}
/>

| Prop | Type | Default | Description | | -------------- | ---------------- | ------- | --------------------------------------------------- | | successUrl | string | — | Required. Redirect URL after successful payment | | cancelUrl | string | — | Required. Redirect URL on cancel | | columns | number | 3 | Number of columns in the grid | | onPlanSelect | (plan) => void | — | Called when a plan is selected | | className | string | — | Additional CSS classes |

PayButton

Flexible payment button with provider selection modal. Supports both plans and payment intents.

import { PayButton } from 'azirid-react'
;<PayButton
  planId="plan_123"
  successUrl="/billing?success=true"
  cancelUrl="/billing"
  onSuccess={(data) => console.log('Payment success:', data)}
>
  Subscribe Now
</PayButton>

| Prop | Type | Default | Description | | ------------ | ----------------- | ------- | --------------------------------------------- | | planId | string | — | Plan to purchase (use planId or intentId) | | intentId | string | — | Payment intent ID (alternative to planId) | | successUrl | string | — | Required. Redirect URL after success | | cancelUrl | string | — | Required. Redirect URL on cancel | | onSuccess | (data) => void | — | Called on successful checkout. For MANUAL_TRANSFER and PAYPHONE, deferred until the user closes the modal | | onError | (error) => void | — | Called on error | | onProviderSelect | (provider: string) => void | — | Called when a provider is selected | | onProofSubmitted | (proof) => void | — | Called when transfer proof is submitted (MANUAL_TRANSFER only) | | children | ReactNode | — | Button label | | disabled | boolean | — | Disable the button |

CheckoutButton

Simple checkout button for a specific plan.

import { CheckoutButton } from 'azirid-react'
;<CheckoutButton planId="plan_123" successUrl="/billing?success=true" cancelUrl="/billing">
  Subscribe
</CheckoutButton>

SubscriptionBadge

Color-coded badge showing the current subscription status.

import { SubscriptionBadge } from 'azirid-react'
;<SubscriptionBadge />
// Renders: "Pro · Active" (green), "Free · Trialing" (blue), etc.

Status colors: ACTIVE (green), TRIALING (blue), PAST_DUE (yellow), CANCELED/UNPAID (red), INCOMPLETE (gray).

InvoiceList

Table of invoices with status badges and download links.

import { InvoiceList } from 'azirid-react'
;<InvoiceList />

PayphoneCallback

Page component for handling Payphone payment callbacks. Reads id and clientTransactionId from URL query params automatically. Deploy this page at the Response URL configured in your Payphone Developer dashboard. The component waits for the session bootstrap to complete before confirming, preventing 401 errors on page reload.

// app/payphone/callback/page.tsx
import { PayphoneCallback } from 'azirid-react'

export default function PayphoneCallbackPage() {
  return (
    <PayphoneCallback
      onSuccess={(data) => console.log('Confirmed:', data.status)}
      onError={(err) => console.error(err)}
    />
  )
}

Multi-tenant

Every authenticated user always has an active tenantId and tenantRole in the JWT. On signup, if no tenantId is provided, a personal tenant is automatically created and the user becomes OWNER.

Tenant switching via useAzirid()

import { useAzirid } from 'azirid-react'

function TenantSwitcher({ tenantId }: { tenantId: string }) {
  const { user, switchTenant } = useAzirid()

  return (
    <div>
      <p>
        Active tenant: {user?.tenantId} ({user?.tenantRole})
      </p>
      <button onClick={() => switchTenant(tenantId)}>Switch</button>
    </div>
  )
}

switchTenant(tenantId) calls the refresh endpoint with the new tenantId, updates the access token in memory, and invalidates all queries — no re-login required.

useTenants

List all tenants the authenticated user belongs to.

import { useTenants } from 'azirid-react'

function TenantList() {
  const { data: tenants, isLoading } = useTenants()

  if (isLoading) return <Spinner />

  return (
    <ul>
      {tenants?.map((t) => (
        <li key={t.tenantId}>
          {t.name} — {t.role}
        </li>
      ))}
    </ul>
  )
}

useTenantMembers

List all members of a specific tenant. The authenticated user must be a member.

import { useTenantMembers } from 'azirid-react'

function MemberList({ tenantId }: { tenantId: string }) {
  const { data: members } = useTenantMembers(tenantId)

  return (
    <ul>
      {members?.map((m) => (
        <li key={m.id}>
          {m.user.email} — {m.role}
        </li>
      ))}
    </ul>
  )
}

useSwitchTenant

Headless hook for tenant switching (same as switchTenant from useAzirid(), but usable outside the main context hook).

import { useSwitchTenant } from 'azirid-react'

const { switchTenant } = useSwitchTenant()

await switchTenant('tenant-id-here')

Referrals

All referral hooks require <AziridProvider> in the tree and an authenticated user.

useReferral

Fetch the current user's referral info and copy referral link to clipboard.

import { useReferral } from 'azirid-react'

function ReferralSection() {
  const { data, copyToClipboard } = useReferral()

  if (!data) return null

  return (
    <div>
      <p>Your referral code: {data.referralCode}</p>
      <input readOnly value={data.referralUrl} />
      <button onClick={copyToClipboard}>Copy Link</button>
      <p>
        {data.completedReferrals} completed / {data.totalReferred} total
      </p>
    </div>
  )
}

useReferralStats

Fetch detailed referral history with rewards.

import { useReferralStats } from 'azirid-react'

function ReferralHistory() {
  const { data } = useReferralStats()

  return (
    <ul>
      {data?.referrals.map((ref) => (
        <li key={ref.id}>
          {ref.referredEmail} — {ref.status}
          {ref.rewardAmount && ` ($${ref.rewardAmount})`}
        </li>
      ))}
    </ul>
  )
}

Referral Components

ReferralCard

Card displaying the referral link with copy button and stats.

import { ReferralCard } from 'azirid-react'
;<ReferralCard
  title="Refer a Friend"
  description="Share your link and earn rewards for each signup."
/>

| Prop | Type | Default | Description | | ------------- | -------- | ------------------ | ---------------------- | | title | string | "Refer a Friend" | Card title | | description | string | — | Card description | | className | string | — | Additional CSS classes |

ReferralStats

Table showing referral history with status and reward badges.

import { ReferralStats } from 'azirid-react'
;<ReferralStats />

Internationalization (i18n)

Built-in support for English and Spanish. The SDK ships two complete dictionaries; pass a locale prop to switch languages.

import { AziridProvider } from 'azirid-react'
;<AziridProvider publishableKey="pk_live_..." locale="en">
  {/* All form labels, validation messages, and UI text render in English */}
  {children}
</AziridProvider>

Supported locales

| Locale | Language | | ------ | ----------------- | | "es" | Spanish (default) | | "en" | English |

Custom messages

Override any string by passing a partial messages object:

<AziridProvider
  publishableKey="pk_live_..."
  locale="en"
  messages={{
    login: { title: 'Welcome back!', submit: 'Sign in' },
    validation: { emailRequired: 'Please enter your email' },
  }}
>
  {children}
</AziridProvider>

Using i18n hooks directly

import { useMessages, useBranding } from 'azirid-react'

function CustomForm() {
  const msg = useMessages() // resolved messages for current locale
  return <label>{msg.login.emailLabel}</label>
}

i18n exports

The SDK exports the raw dictionaries and the resolver function for advanced use cases:

import { resolveMessages, es, en } from 'azirid-react'

// Get the full Spanish dictionary
console.log(es.login.title) // "Iniciar sesión"

// Resolve with overrides
const messages = resolveMessages('en', {
  login: { title: 'Welcome back!' },
})

Locale-aware Zod schemas

import { createLoginSchema, createSignupSchema } from 'azirid-react'

// Pass custom validation messages
const schema = createLoginSchema({
  emailRequired: 'Email is required',
  emailInvalid: 'Must be a valid email',
  passwordRequired: 'Password is required',
  passwordMinLength: 'At least 10 characters',
})

Branding

The bootstrap endpoint returns branding data configured in the Azirid dashboard (Settings > Branding). The built-in form components automatically apply branding.

Auto-branding from bootstrap

If branding is configured for your app, the forms will automatically:

  • Show your logo (from branding.logoUrl) above the form
  • Use your display name as the form title
  • Apply your primary color to the submit button (overrides --aa-primary via inline style on the shadow host — see Styling & theming for per-token overrides like --aa-radius, --aa-foreground, etc.)
  • Show/hide the "Secured by Azirid" badge

No extra code needed — just configure branding in the dashboard.

Overriding branding with props

Per-component props always take priority over branding context:

<LoginForm
  logo={<MyCustomLogo />} // overrides branding.logoUrl
  title="Sign in to Acme" // overrides branding.displayName
  submitText="Continue"
/>

Using branding hooks

import { useBranding } from 'azirid-react'

function CustomHeader() {
  const branding = useBranding() // AppBranding | null

  return (
    <div>
      {branding?.logoUrl && <img src={branding.logoUrl} alt="Logo" />}
      <h1 style={{ color: branding?.primaryColor ?? '#000' }}>
        {branding?.displayName ?? 'My App'}
      </h1>
    </div>
  )
}

"Secured by Azirid" badge

The badge renders automatically below each built-in form component (LoginForm, SignupForm, etc.). It's hidden when branding.removeBranding is true (configurable in the dashboard). No extra code needed.


createAccessClient

Under the hood AziridProvider creates an AccessClient via createAccessClient. You can also create a client directly to make raw API calls.

import { createAccessClient, AUTH_BASE_PATH } from 'azirid-react'
import type { AccessClientConfig } from 'azirid-react'

const client = createAccessClient(
  {
    baseUrl: 'https://api.azirid.com',
    basePath: AUTH_BASE_PATH, // '/v1/users/auth'
  },
  { publishableKey: 'pk_live_...' },
)

// Set tokens after login
client.setAccessToken('eyJ...')
client.setRefreshToken('...')

// Make arbitrary authenticated calls
const data = await client.get(client.paths.me)
const result = await client.post('/v1/custom-endpoint', { foo: 'bar' })

createAccessClient signature

function createAccessClient(
  config: AccessClientConfig,
  appContext?: { publishableKey: string; tenantId?: string },
): AccessClient

| Param | Type | Description | | ------------ | -------------------- | ----------------------------------------- | | config | AccessClientConfig | { baseUrl, basePath?, headers? } | | appContext | object | Optional. publishableKey and tenantId |


AziridProvider props

| Prop | Type | Default | Description | | ------------------ | ------------------------- | ------- | -------------------------------------------------------------------------------- | | children | ReactNode | — | Required. Your app tree | | publishableKey | string | — | Publishable key (e.g. pk_live_...) | | tenantId | string | — | Tenant ID for multi-tenant apps | | fetchOptions | Record<string, string> | — | Extra headers to send with every request | | autoBootstrap | boolean | true | Auto-restore session on mount. Runs once (safe in React 18 Strict Mode) | | refreshInterval | number | 240000 | Token refresh interval in ms. 0 to disable. Tokens also refresh reactively on any 401 response, so a higher interval is safe. | | onAuthStateChange| () => void | — | Called after login, signup, or logout | | onLoginSuccess | (data) => void | — | Called after successful login | | onSignupSuccess | (data) => void | — | Called after successful signup | | onLogoutSuccess | () => void | — | Called after logout | | onSessionExpired | () => void | — | Called when the refresh token is definitively rejected (401/403). Use this to redirect to login. | | onError | (msg: string) => void | — | Called on any auth error | | locale | "es" \| "en" | "es" | UI language for built-in forms and validation messages | | messages | Partial<AccessMessages> | — | Override any i18n string (merged on top of the locale dictionary) |


Device Tracking

The SDK automatically generates a persistent device ID (UUIDv4) and stores it in localStorage under the key __az_device_id. This ID is sent as an X-Device-Id header on every auth request (login, signup, refresh, bootstrap).

How it works:

  • On first use, crypto.randomUUID() generates a unique ID per browser profile
  • The ID persists across tabs, page reloads, and browser restarts
  • Different browsers or incognito windows get different device IDs
  • The backend uses this to deduplicate sessions per device: re-logging from the same browser replaces the previous session instead of creating duplicates

Where session material lives, by mode:

| Key | Storage | Mode | Purpose | | ---------------- | ----------------- | ------------- | -------------------------------------------------------------------- | | __az_device_id | localStorage | both | Persistent device identity — survives browser restarts | | __session | HttpOnly cookie | both | Access token — server-owned (Set-Cookie on every auth response) | | _asid | HttpOnly cookie | same-origin | Refresh token — server-owned, never touched by JS | | _axc | cookie | same-origin | CSRF token — paired with _asid | | __az_rt | sessionStorage | cross-origin | Refresh token mirror — tab-scoped, cleared on tab close | | __az_xc | sessionStorage | cross-origin | CSRF token mirror — tab-scoped |

The SDK picks the right lane automatically at mount (see Cross-origin session handling): same-origin deployments rely entirely on HttpOnly cookies, cross-origin deployments mirror the refresh/CSRF tokens into sessionStorage so the session survives full-page navigations.

Legacy keys (__azrt / __azxc in localStorage) were removed in 0.18. The SDK no longer reads or writes them. If you are upgrading from 0.17 or earlier, users will re-authenticate once after the first deploy.

In SSR or environments where localStorage / sessionStorage is unavailable, storage operations are silently skipped.


Impersonation Handoff

The Azirid dashboard lets admins click "Sign in as user" to impersonate any end user in a customer app. This works via a short-lived handoff code that your app exchanges for real session tokens.

How it works:

  1. Admin clicks "Sign in as user" in the Azirid dashboard
  2. Dashboard generates a one-time handoff code (valid for 5 minutes) and redirects to your app at /auth/handoff?code=<code>&api=<apiUrl>
  3. Your app renders <HandoffCallback>, which exchanges the code for session tokens directly with the API
  4. Tokens are stored in localStorage + __session cookie, then the component redirects to redirectUrl via hard navigation so AziridProvider re-bootstraps with the new session

<HandoffCallback> is fully standalone — it does NOT depend on AziridProvider. It works even when the provider wraps the entire layout and its bootstrap would redirect to /login. No special route groups or layout changes needed.

Setup — create the handoff page:

// app/auth/handoff/page.tsx
'use client'
import { HandoffCallback } from 'azirid-react'

export default function HandoffPage() {
  return (
    <HandoffCallback
      redirectUrl="/dashboard"
      onError={() => window.location.href = '/login'}
    />
  )
}

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | redirectUrl | string | "/" | URL to redirect after successful handoff (hard navigation) | | onSuccess | (user: unknown) => void | — | Called before redirect for optional side effects (analytics, etc.) | | onError | (error: Error) => void | — | Called on failure (expired/invalid code); redirect to login | | loadingText | string | "Signing you in..." | Text shown while the exchange is in progress | | errorText | string | "Failed to complete sign-in. The link may have expired." | Fallback error text |

You can also call client.exchangeHandoff(code, apiUrl) directly if you prefer a headless approach:

const { user } = await client.exchangeHandoff(code, 'https://api.azirid.com/v1')

Next.js Integration

azirid-react supports Next.js 14, 15, and 16+ with full compatibility for each version's API conventions.

Server Actions & onAuthStateChange

If you use server actions or server components that read the session token (via createServerAccess), you must pass onAuthStateChange to keep server-side cookies in sync:

'use client'
import { useRouter } from 'next/navigation'
import { AziridProvider } from 'azirid-react'

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter()

  return (
    <AziridProvider
      publishableKey={process.env.NEXT_PUBLIC_AZIRID_PK!}
      onAuthStateChange={() => router.refresh()}
    >
      {children}
    </AziridProvider>
  )
}

Why? After login/signup/logout, azirid-react updates the __session cookie in the browser. But Next.js server actions keep using the old cookies until router.refresh() forces a new server request. Without this, server actions may return 401 after login.

Not using server actions? (e.g. pure client-side SPA with Vite) — you can skip onAuthStateChange.

Route Protection — <AuthGuard> and <GuestGuard>

azirid-react calls api.azirid.com cross-origin from your app, so the auth cookies set by the API live on the Azirid domain — they are not visible to your app's domain. That makes server-side cookie-based gating impossible for SaaS consumers, so route protection happens client-side after the provider bootstraps.

Setup

Group your protected routes under a layout that wraps them in <AuthGuard>, and your auth pages under a layout that wraps them in <GuestGuard>:

// app/(protected)/layout.tsx
import { AuthGuard } from 'azirid-react/next/guards'

export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
  return <AuthGuard>{children}</AuthGuard>
}
// app/(auth)/layout.tsx
import { GuestGuard } from 'azirid-react/next/guards'

export default function AuthLayout({ children }: { children: React.ReactNode }) {
  return <GuestGuard>{children}</GuestGuard>
}

Then move your routes into those groups:

app/
  (protected)/
    dashboard/page.tsx
    admin/page.tsx
  (auth)/
    login/page.tsx
    register/page.tsx
    forgot-password/page.tsx

How it works

<AuthGuard> — for pages that require authentication:

  1. Renders fallback (default: null) while the provider bootstraps.
  2. When bootstrap finishes and the user is not logged in, router.replace() to /login?redirect=<original-pathname>.
  3. When the user is authenticated, renders children.

<GuestGuard> — for login/register/etc. (logged-OUT only):

  1. Renders fallback while the provider bootstraps.
  2. When the user is authenticated, redirects to ?redirect=<path> (if present and starts with /) or to dashboardUrl (default: /dashboard).
  3. When the user is logged out, renders children.

Props

<AuthGuard>:

| Prop | Type | Default | Description | | ------------------- | ----------- | ----------- | ------------------------------------------------------------- | | loginUrl | string | '/login' | Redirect target for unauthenticated users | | preserveRedirect | boolean | true | Append ?redirect=<pathname> so login can send the user back | | fallback | ReactNode | null | Rendered while loading or during the redirect |

<GuestGuard>:

| Prop | Type | Default | Description | | ------------------- | ----------- | -------------- | ----------------------------------------------------------------- | | dashboardUrl | string | '/dashboard' | Redirect target for authenticated users | | honorRedirectParam| boolean | true | When true, redirects to ?redirect=<path> from the URL if vali