# Angular

> Use provideNexafon, the standalone nexafon-ng components and NexafonService, safe under Angular SSR.

Source: https://docs.nexafone.so/v1/web/angular/

`@nexafon/angular` has standalone components and a service around the [web SDK](/v1/web/). Supported: Angular 22 with
RxJS 7.4 or later.

:::note[Not published yet]
The Nexafon packages are not on npm, Packagist or PyPI yet, and `go get` finds the Go module only once the Nexafon
repository is public and tagged, so installing them by name fails for now. Until then,
[install them from a checkout of the repo](/v1/quickstart/#install-from-a-checkout). The install commands shown here
are the ones to use after release.
:::

```sh
npm install @nexafon/angular
```

## 1. Provide Nexafon

`provideNexafon(options)` takes the `Nexafon.init` options and starts Nexafon once, with the app. The example's
`appConfig` takes the publishable key as an argument:

```ts title="examples/angular/src/app/app.config.ts"
import { type ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'
import { provideClientHydration, withEventReplay } from '@angular/platform-browser'
import { provideNexafon } from '@nexafon/angular'

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 appConfig(publishableKey: string): ApplicationConfig {
  return {
    providers: [
      provideBrowserGlobalErrorListeners(),
      provideClientHydration(withEventReplay()),
      provideNexafon({ publishableKey, getUserToken }),
    ],
  }
}
```

The browser entry point asks the example's server for the key (`GET /api/nexafon-config`) before it starts the app, so
one build serves any workspace:

```ts title="examples/angular/src/main.ts"
import { bootstrapApplication } from '@angular/platform-browser'
import { App } from './app/app'
import { appConfig } from './app/app.config'

// The publishable key comes from the server when the page loads, so one build serves any workspace.
fetch('/api/nexafon-config')
  .then((r) => r.json() as Promise<{ publishableKey: string }>)
  .then(({ publishableKey }) => bootstrapApplication(App, appConfig(publishableKey)))
  .catch((err: unknown) => console.error(err))
```

## 2. The server

The example's Express server (Angular SSR) answers `GET /api/nexafon-config` with `NEXAFON_PUBLISHABLE_KEY` and signs
user tokens at `POST /api/nexafon-token` with `NEXAFON_SECRET_KEY` and the [Node helper](/v1/server/node/), which
`getUserToken` calls:

```ts title="examples/angular/src/server.ts"
import { join } from 'node:path'
import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  isMainModule,
  writeResponseToNodeResponse,
} from '@angular/ssr/node'
import { createUserToken } from '@nexafon/node'
import express, { type Request } from 'express'

try {
  process.loadEnvFile()
} catch {
  // No .env: the variables come from the environment.
}

const browserDistFolder = join(import.meta.dirname, '../browser')

const app = express()
const angularApp = new AngularNodeAppEngine()

app.get('/api/nexafon-config', (_req, res) => {
  res.json({ publishableKey: process.env.NEXAFON_PUBLISHABLE_KEY ?? '' })
})

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: Request): 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
}

// Signs a token for the signed-in user with NEXAFON_SECRET_KEY.
app.post('/api/nexafon-token', (req, res) => {
  const user = signedInUser(req)
  if (!user) {
    res.sendStatus(401)
    return
  }
  if (!mayUseNexafon(user)) {
    res.sendStatus(403)
    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.type('text/plain').set('cache-control', 'no-store').send(token)
})

app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }))

app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next)
})

if (isMainModule(import.meta.url)) {
  const port = process.env.PORT || 5205
  app.listen(Number(port), '127.0.0.1', (error) => {
    if (error) throw error
    console.log(`Angular example on port ${port}`)
  })
}

export const reqHandler = createNodeRequestHandler(app)
```

`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.

:::caution[Send `emailVerified` as true only for a confirmed address]
Nexafon links a user’s first sign-in to the workspace member with the token’s email only when it is verified. If your
app lets users set an email it has not confirmed, someone could register with a member’s address and act as that
member. Send `true` only when your app has confirmed the address, from your own user record, never as a constant.
:::

## 3. Components and the service

```ts title="examples/angular/src/app/app.ts"
import { Component } from '@angular/core'
import { CallButtonComponent, InboxComponent, SoftphoneComponent } from '@nexafon/angular'

@Component({
  selector: 'app-root',
  imports: [InboxComponent, SoftphoneComponent, CallButtonComponent],
  template: `
    <main class="support">
      <nexafon-ng-inbox style="height: 640px" />
      <aside>
        <nexafon-ng-call-button phone="+252615550142" />
        <nexafon-ng-softphone />
      </aside>
    </main>
  `,
  styles: `.support { display: grid; gap: 16px; padding: 16px; grid-template-columns: 1fr 340px; }`,
})
export class App {}
```

| Component | Renders | Inputs |
| --- | --- | --- |
| `<nexafon-ng-app>` | `<nexafon-app>` | `store` |
| `<nexafon-ng-inbox>` | `<nexafon-inbox>` | `store` |
| `<nexafon-ng-softphone>` | `<nexafon-softphone>` | `store` |
| `<nexafon-ng-call-button>` | `<nexafon-call-button>` | `phone`, `store` |

Each renders inside a block host you size. `NexafonService.events(name)` is an Observable of one
[event](/v1/web/events/); `openChat`, `call`, `startInternalCall` and `setAppearance` call the started SDK
synchronously, so a pop-out window still opens inside the user’s click; `api` is a signal of the SDK (`null` on the
server and until it has started).

## SSR

On the server (`isPlatformBrowser` is false) the service never loads Nexafon and the components render an empty
host; the browser creates the elements after hydration. The full example is `examples/angular`.
