Skip to content

@nexafon/react wraps the web SDK in typed React components. Supported: React 19.

Terminal window
npm install @nexafon/react

<NexafonProvider> takes the Nexafon.init options and starts Nexafon once, on mount; use one per page. Put it high in your tree, inside your app’s signed-in area:

examples/react/src/App.tsx
import { CallButton, Inbox, NexafonProvider, Softphone } from '@nexafon/react'
const getUserToken = async () => {
const r = await fetch('/api/nexafon-token', { method: 'POST' })
if (!r.ok) throw new Error(`The token endpoint answered ${r.status}`)
return r.text()
}
export function App() {
return (
<NexafonProvider
publishableKey={import.meta.env.VITE_NEXAFON_PUBLISHABLE_KEY}
getUserToken={getUserToken}
>
<main style={{ display: 'grid', gap: 16, padding: 16, gridTemplateColumns: '1fr 340px' }}>
<Inbox style={{ height: 640 }} />
<aside>
<CallButton phone="+252615550142" />
<Softphone />
</aside>
</main>
</NexafonProvider>
)
}
Component Renders Props
<NexafonApp/> <nexafon-app>, the whole dashboard store?, className, style, id
<Inbox/> <nexafon-inbox> store?, className, style, id
<Softphone/> <nexafon-softphone> store?, className, style, id
<CallButton/> <nexafon-call-button> phone, store?, className, style, id
import { useNexafon, useNexafonEvent } from '@nexafon/react'
function OrderHeader({ phone }: { phone: string }) {
const nexafon = useNexafon() // null on the server and until Nexafon has started
useNexafonEvent('call.ringing', (call) => console.log('ringing', call.callerNumber))
return <button onClick={() => nexafon?.openChat(phone)}>Chat</button>
}

useNexafonEvent listens while the component is mounted; the events are listed in the events reference.

getUserToken calls your server, which signs a token with a server helper. The example’s POST /api/nexafon-token handler:

examples/react/server/token.ts
import type { IncomingMessage, ServerResponse } from 'node:http'
import { createUserToken } from '@nexafon/node'
interface SignedInUser {
id: string
name: string
role: string
email: string
emailVerified: boolean
isStaff: boolean
}
// Your app's own session decides who is signed in, and its own records whether it has confirmed their email. The
// example has no sign-in: EXAMPLE_DEMO_USER_EMAIL is a demo user for local runs only, so replace this with your session.
function signedInUser(_req: IncomingMessage): SignedInUser | null {
const email = process.env.EXAMPLE_DEMO_USER_EMAIL
if (!email) return null
return {
id: 'demo-user-1',
name: 'Demo User',
role: process.env.EXAMPLE_DEMO_USER_ROLE || 'agent',
email,
// The demo address is a member you invited, so you have confirmed it.
emailVerified: true,
isStaff: true,
}
}
// Everyone who gets a token may reach your workspace's inbox and calls: sign only for your staff, never for customers
// who signed up. The example's only user is the demo teammate.
function mayUseNexafon(user: SignedInUser): boolean {
return user.isStaff
}
// POST /api/nexafon-token signs a token for the signed-in user with NEXAFON_SECRET_KEY.
export function userToken(req: IncomingMessage, res: ServerResponse): void {
if (req.method !== 'POST') {
res.writeHead(405).end()
return
}
const user = signedInUser(req)
if (!user) {
res.writeHead(401).end()
return
}
if (!mayUseNexafon(user)) {
res.writeHead(403).end()
return
}
const token = createUserToken({
userId: user.id,
name: user.name,
role: user.role,
email: user.email,
// Nexafon links a first sign-in to the member with this email only when it is verified: never send true for an
// address your app has not confirmed, or whoever sets a member's email on your app acts as that member.
emailVerified: user.emailVerified,
})
res.writeHead(200, { 'content-type': 'text/plain', 'cache-control': 'no-store' }).end(token)
}

signedInUser stands for your app’s own session: answer 401 when nobody is signed in. The example has no sign-in, so on a local run it signs for the demo member named in EXAMPLE_DEMO_USER_EMAIL and answers 401 without one; replace it with your session. mayUseNexafon answers 403 to signed-in users who must not use Nexafon: sign only for your staff, never for customers who signed up, since a token can make its user a member of your workspace. The example treats its demo user as staff.

The example mounts it as middleware of the Vite dev and preview servers in examples/react/vite.config.ts; in production it is a route of your own server, behind your own session check.

The full example is examples/react (Vite), tested end to end with Playwright.