@snugdesk/whatsapp-widget
v1.0.12
Published
WhatsApp widget for Angular Apps - Powered by Snugdesk
Readme
Snugdesk - WhatsApp Widget for Angular
A production-ready Angular library that embeds the full Snugdesk WhatsApp agent experience inside your web application. The widget handles authentication, conversation management, message templates, media uploads, emoji reactions, AI-assisted replies, real-time translation, and rich UI states out of the box.
To purchase licenses or to get implementation assistance, reach out to:
SNUG Technologies Pvt Ltd
📧 [email protected]
✅ Requirements
- Angular 21.0.0+ (matches the published peer dependency range)
@snugdesk/core0.2.42+ — the widget callsauthenticate(token), the token-only signature introduced in this release; earlier builds expectauthenticate(token, tenantId, userId)and will not establish a session- A licensed Snugdesk tenant with access to the WhatsApp channel
📦 Installation
Install the core runtime first (all Snugdesk widgets depend on it), then add the WhatsApp widget.
npm install @snugdesk/core
npm install @snugdesk/whatsapp-widgetOther runtime dependencies are bundled (@aws-sdk/client-s3, @ctrl/ngx-emoji-mart, ngx-avatars, ngx-infinite-scroll, ngx-skeleton-loader, moment-timezone, libphonenumber-js, sort-nested-json, uuid, …).
🛠 Workspace Configuration (required)
The AWS SDK for JavaScript (v3) expects Node-style globals (global, process) to exist. Add the following once in your host application:
Create
src/custom-polyfills.ts// src/custom-polyfills.ts (window as any).global = window; (window as any).process = { env: { DEBUG: undefined } };Keep additional polyfills above if your app already uses this file.
Register the polyfill file in
angular.json{ "projects": { "your-app": { "architect": { "build": { "options": { "polyfills": [ "zone.js", "src/custom-polyfills.ts" ] } }, "test": { "options": { "polyfills": [ "zone.js", "zone.js/testing", "src/custom-polyfills.ts" ] } } } } } }Let TypeScript know about the polyfill file
Add the file to the
filesarray in bothtsconfig.app.jsonandtsconfig.spec.json:{ "files": [ "src/custom-polyfills.ts" ] }
These changes ensure the widget (and its client libraries) run reliably in both builds and tests.
⚙️ Angular Setup
Standalone-bootstrapped apps
The widget component is not an Angular standalone component — it is exported by WhatsAppWidgetModule. In a standalone-bootstrapped app you make it available by importing the module into whichever component renders the widget (standalone components can import NgModules).
Provide animations and HttpClient at bootstrap (
main.ts)import { bootstrapApplication } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ provideAnimations(), provideHttpClient(withInterceptorsFromDi()), ] });Import
WhatsAppWidgetModulein the consuming standalone componentimport { Component } from '@angular/core'; import { WhatsAppWidgetModule } from '@snugdesk/whatsapp-widget'; @Component({ selector: 'app-root', standalone: true, imports: [WhatsAppWidgetModule], templateUrl: './app.component.html', }) export class AppComponent {}Without this import the
<snugdesk-whatsapp-widget>element will not be recognised.
NgModule-based apps
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { WhatsAppWidgetModule } from '@snugdesk/whatsapp-widget';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, WhatsAppWidgetModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}🔐 Authentication Workflow
The widget takes a single [token] — a short-lived session token — and calls authenticate(token) on SnugdeskAuthenticationService from @snugdesk/core. The token carries the tenant, the user and the session id, so the widget needs nothing else to identify the agent.
Your server obtains that token by presenting a tenant integration credential. The credential is long-lived and belongs on your server; only the session token it produces ever reaches the browser.
Browser Your server Snugdesk API
| | |
|-- "start a session" ------->| |
| |-- POST /auth/library ----->|
| | Bearer <keyId>.<secret> |
| | { "userId": "…" } |
| |<-- { sessionToken } -------|
|<-- sessionToken ------------| |
| |
|-- [token]="sessionToken" → widget ---------------------->|All calls below go to https://api.snugdesk.com and additionally require the x-api-key header issued to you by Snugdesk. Contact support if you do not have one.
Step 1 — Create an integration credential (once)
In the Snugdesk console, go to Settings → Developers → API Credentials and create a credential. Or call the API with a signed-in user's session token:
POST /tenants/{tenantId}/api-credentials
x-api-key: <your Snugdesk API key>
Authorization: Bearer <a session token for a user in this tenant>
Content-Type: application/json
{ "name": "Support console" }// 201 Created
{
"message": "Integration credential created. Store it now — it cannot be shown again.",
"data": {
"id": "…",
"keyId": "00000000-0000-4000-8000-000000000000",
"name": "Support console",
"createdAt": 1786440869,
// "<keyId>.<secret>" — returned exactly once, stored only as a salted hash
"credential": "00000000-0000-4000-8000-000000000000.<32-byte base64url secret>"
}
}credential is shown once and cannot be recovered — put it straight into your secret store. If it is lost, revoke it and create another.
Two companion routes manage the set:
| Method | Path | Purpose |
| --- | --- | --- |
| GET | /tenants/{tenantId}/api-credentials | List credentials — keyId, name, createdAt, lastUsedAt, revokedAt. Never the secret. |
| DELETE | /tenants/{tenantId}/api-credentials/{credentialId} | Revoke. Takes effect on the next request; sessions already issued run to their natural expiry. |
lastUsedAt (refreshed at most every 5 minutes) is the cheapest way to tell whether a credential is still wired up before you revoke it.
Step 2 — Exchange it for a session token (per agent, on your server)
POST /auth/library
x-api-key: <your Snugdesk API key>
Authorization: Bearer <keyId>.<secret>
Content-Type: application/json
{ "userId": "<the agent's Snugdesk user id>" }// 200 OK
{
"message": "Session token generated successfully",
"data": {
"sessionToken": "eyJhbGciOiJIUzI1NiIs…",
"expiresIn": 1800
}
}Note what is not in the request: the tenant is read from the verified credential, never from the body. A credential for one tenant therefore cannot open a session for another tenant's user — that mismatch is rejected with a 403.
Optionally pass ipDetails alongside userId to record the end user's origin on the session.
Responses to handle
| Status | Meaning |
| --- | --- |
| 400 | userId missing, or the body is not valid JSON |
| 401 | Missing, malformed, unknown or revoked credential — deliberately indistinguishable, so probing reveals nothing |
| 403 | Tenant blocked, user blocked, or the user belongs to a different tenant than the credential |
| 404 | Tenant or user not found (or soft-deleted) |
Step 3 — Hand the token to the widget
Fetch the token from your own backend endpoint and render the widget only once it resolves:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
@Component({
selector: 'my-component',
templateUrl: './my-component.html'
})
export class ConversationShellComponent implements OnInit {
token = '';
themeColor = '#0f8b5f';
constructor(private http: HttpClient) {}
async ngOnInit(): Promise<void> {
// Your endpoint — it holds the integration credential and calls /auth/library.
const res: any = await lastValueFrom(this.http.post('/api/snugdesk/session', {}));
this.token = res.sessionToken;
}
}@if (token) {
<snugdesk-whatsapp-widget
[token]="token"
[baseColor]="themeColor">
</snugdesk-whatsapp-widget>
}The widget authenticates once, in ngOnInit. Rendering it with an empty or stale token surfaces an authentication error in the UI, and assigning a new value to [token] later does not re-authenticate — to renew a session, destroy and re-create the component (the @if above does this when you blank the token first).
Session lifetime. 30 minutes by default. Tenants can raise or lower this via security.librarySessionTokenExpiry; the exact value for the token you were handed comes back as expiresIn (seconds) from /auth/library. Request a fresh token per agent session rather than caching one across users.
⚠️ Never put an integration credential in browser code, in a public repo, or in a mobile bundle. It authenticates as your tenant and can open a session for any user in it — including an administrator. Anything the page can read, so can its visitors. Keep the exchange on the server and ship only the resulting session token.
Inputs
token(required) – The session token from/auth/library; the widget callsauthenticate(token)internally.mode(optional) –'Embedded'or'Standalone'. Defaults to'Standalone'. See Widget Modes.entityConversation(required inEmbeddedmode) – The conversation to load when embedding the widget in an existing context.widgetId(optional) – Which WhatsApp Business account to open on. Defaults to the tenant's first account. See Choosing the starting account.baseColor(optional) – Overrides the primary accent colour used across the UI.theme(optional) –'Dark','Light', or'Device'. Defaults to'Device', which follows the OS colour-scheme preference.
Choosing the starting account
Tenants with more than one WhatsApp Business account can pick which one the widget opens on by passing that account's interaction-widget id:
<snugdesk-whatsapp-widget
[token]="token"
[widgetId]="'0f1c9a2b-…'">
</snugdesk-whatsapp-widget>Two things to know before you reach for it:
- It selects, it does not restrict. The account switcher still lists every account on the tenant, the agent can switch at will, and opening a conversation owned by another account switches to that account automatically.
widgetIdonly decides where the session starts. - An id that matches no account on the tenant is fatal, not ignored — the widget fails with WhatsApp account not found rather than falling back to the first account. Pass a value you read from the tenant's own accounts, or omit the input.
It also namespaces the widget's local cache. Two instances of the widget on one page with no widgetId share a single cached account configuration; giving each its own id keeps them separate.
🧭 Widget Modes
The widget supports two modes: Embedded and Standalone. Use the mode input to choose how the UI behaves.
Embedded mode
- Set
mode="Embedded"when you want to embed the widget inside an existing conversation context. - Requires
tokenandentityConversation(to load the selected conversation).
Standalone mode (default)
- Set
mode="Standalone"to run the widget as a full client for the WhatsApp Business API. - Only
tokenis required;entityConversationis not needed.
<snugdesk-whatsapp-widget
[token]="sessionToken"
[mode]="'Standalone'">
</snugdesk-whatsapp-widget>🎨 Assets & Styling
The library bundles icons, background artwork, and shared CSS under @snugdesk/whatsapp-widget/assets. To make them available during your build, add the folder to the Angular CLI asset list:
{
"projects": {
"your-app": {
"architect": {
"build": {
"options": {
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/@snugdesk/whatsapp-widget/assets",
"output": "assets/snugdesk-whatsapp"
}
],
"styles": [
"src/styles.css",
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
]
}
}
}
}
}
}If you already use custom asset pipelines, copy the contents of node_modules/@snugdesk/whatsapp-widget/assets into a location your app serves at runtime and keep the relative directory structure.
Fonts & Icons (required)
The widget renders with the Open Sans font and Font Awesome icons. These are intentionally not bundled into the component styles — loading remote stylesheets from inside a library's component CSS bloats every component and breaks production builds against the anyComponentStyle budget. Instead, load them once, globally, in your host app's index.html:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700,800"
rel="stylesheet"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
rel="stylesheet"
/>If your app already ships Open Sans and Font Awesome (≥ 6.x) globally, no further action is needed. Without these, text falls back to a system font and icon glyphs will not render.
🆕 What's New in 1.0.10
- 🔐 Breaking: the
tenantIdanduserIdinputs are gone. The session token carries both, so[token]is all the widget needs. Upgrade to@snugdesk/core0.2.42+ and drop the two bindings — see Authentication Workflow.
1.0.8
- 📱 One account per conversation, everywhere. A thread now belongs to exactly one WhatsApp Business account across the list, the composer and realtime updates — starting a chat from a new number no longer reopens the same customer's thread from another number.
- 🔔 Cross-account alerts. A message arriving on an account the agent isn't viewing raises a toast with a View action, a per-account unread count on the switcher, and an audible alert; opening the account jumps straight to the chat that raised it.
- 👁 Read receipts. Opening a conversation marks the customer's latest message as read, so they see blue ticks.
- 🎛 Redesigned account switcher — pill trigger with a rotating chevron, animated dropdown, SIM badge with a count overlay on the conversation avatar, and full dark-mode parity. It also paints instantly from cache on reload instead of waiting on Meta lookups.
- ✅✅ Delivery ticks in the conversation list, alongside the message-bubble ticks introduced in 1.0.7, updating live as the status changes.
See Release Notes for the full history.
Alongside these, the 1.0.x line carries a set of AI-assisted and rich-messaging capabilities on top of the core console:
- 🌐 Multi-language support — real-time message translation powered by AI:
- Per-message Translate to… menu across 21+ languages (English + major Indic languages: Hindi, Bengali, Punjabi, Marathi, Gujarati, Tamil, Telugu, Kannada, Malayalam, Odia, Assamese, Urdu, and more).
- Composer live-preview and translate-on-send so agents can reply in the customer's language.
- Conversation-language mode — view the entire open chat in a single chosen language in one round-trip.
- Offline language detection — scripts and common Indic-language spellings are detected client-side without a backend call.
- 🤖 AI smart replies — up to three short, context-aware reply suggestions for the tail of a conversation, revealed progressively.
- 😀 Emoji reactions — WhatsApp-style optimistic reactions with an emoji picker, reaction detail popup (filter by emoji), and dark-theme support.
- ⭐ Starred messages — bookmark messages, browse them in a dedicated starred panel, and clear/unstar in place.
- ↪️ Message forwarding — forward with a "Forwarded" label and 24-hour-window gating.
- 📎 Multi-file uploads — WhatsApp-style multi-file picker with a thumbnail strip, captions, and swipe gestures in the media preview.
- 💬 Quoted replies — WhatsApp-style reply previews with jump-to-original.
- 🎙 Audio message player — inline audio preview/playback.
- 📝 Per-conversation drafts — unsent composer text is retained per conversation.
AI features (smart replies + translation) route through Snugdesk's server-side lambda; the model API key never reaches the browser. The backend contract is documented in
docs/AI_BACKEND_CONTRACT.mdin the widget source repository (not shipped in the npm package) — ask support for a copy.
✨ Feature Highlights
Everything below is available in the published 1.0.10 package.
Agent console
- Omni-channel agent console for WhatsApp conversations, contacts, and templates
- Real-time thread updates with infinite-scroll history, skeleton states, and unread indicators
- Embedded and Standalone modes, dark/light/device theming, and a configurable accent colour
- Timezone-aware message timelines, phone-number parsing, and avatar rendering
Messaging
- Delivery-status ticks on message bubbles and conversation rows: sent (single grey), delivered (double grey), read (double blue)
- Read receipts sent back to the customer when the agent opens a conversation
- Message template browsing, preview, placeholder filling, and quick actions
- Quoted replies with jump-to-original, and message forwarding with a "Forwarded" label and 24-hour-window gating
- Emoji reactions with an emoji picker and a reaction-detail popup, plus starred/bookmarked messages in a dedicated panel
- Multi-file uploads with a thumbnail strip, captions, swipe gestures in the media preview, and inline audio playback
- Per-conversation composer drafts
AI
- Smart replies — up to three short, context-aware suggestions, revealed progressively
- Per-message and whole-conversation translation across 21+ languages, composer live-preview, and translate-on-send
- Offline (client-side) language detection for scripts and common Indic-language spellings
Multi-account
- Multiple WhatsApp Business accounts (WABA) per tenant, each conversation owned by exactly one account
- Account switcher with per-account conversation filtering, a SIM badge on the conversation avatar, and per-account unread counts
- Cross-account alerts: toast with a View action plus an audible alert when a message lands on an account the agent isn't viewing
- Verified-name and phone-number labels resolved from Meta for each account, cached for instant paint on reload
- Optional
widgetIdinput to choose which account the widget opens on — see Choosing the starting account
📜 Release Notes
1.0.10 — 11 Aug 2026
Breaking — authentication
- The
tenantIdanduserIdinputs are gone. The session token already carries both, so the widget reads them back from@snugdesk/coreafterauthenticate(token)resolves. Remove the two bindings from your template. - Requires
@snugdesk/core0.2.42+ for the token-onlyauthenticate(token)signature. - Session tokens are now issued by
POST /auth/libraryagainst a tenant integration credential, from your server. See Authentication Workflow.
Documentation
- The
widgetIdinput is now documented. It has shipped since the multi-account line but was absent from the input list, so integrators had no way to know a starting account could be chosen.
1.0.8 — 1 Aug 2026
Multi-account correctness
- A conversation now belongs to exactly one WhatsApp Business account. Starting a chat from a fresh number used to reopen the same customer's thread from another number, history and all — the lookup matched on tenant + phone only, while the inbound webhook strict-filters by account. New threads are stamped with the account picked in the switcher, and an existing thread is reused only when it belongs to the target account.
- Conversations indexed before multi-account support (no account on the record) are attributed to the tenant's oldest account instead of surfacing under every number.
- Switching accounts closes an open chat the new account can't see.
- Cross-account alerts — a toast with a View action, a per-account count on the switcher, and an audible alert when a message arrives on an account the agent isn't viewing. Coming to an account from its alert opens the chat that raised it. Alerts are routed by the conversation's real owning account and deduped on the message id, so one message can't announce itself twice or on two accounts.
- The alert sound now arms on the first gesture anywhere in the app; previously a suspended audio context made every beep a silent no-op for an agent who only switched accounts.
- Redesigned account switcher — pill trigger with a rotating chevron, animated dropdown, brand-coloured SIM badge with a count overlay moved onto the conversation avatar, green check on the active row, and full dark-mode parity.
- The switcher paints from cache on reload: the account list and its phone/verified-name maps are cached alongside the widget config, Meta lookups no longer block init, and each label publishes as it resolves so one slow number can't hold back the rest.
Messaging
- Read receipts — opening a conversation (and receiving a new inbound while it's open) marks the customer's latest message read, so they see blue ticks. Subject to the customer's mutual read-receipt privacy setting.
- Delivery ticks now appear on conversation-list rows as well as message bubbles, and bubble ticks update live on status change.
- Pasting an image into the composer or caption box attaches it through the media preview, keeping any typed text as the first image's caption.
Fixes
- Template bubbles no longer render blank when the metadata map was stored as a double-encoded JSON string; the placeholder-filled body text is also persisted on outgoing template messages so the list preview and bubble always have a fallback.
- Preview message is disabled until an approved template is selected.
- Jumping to a message above the current view works again. The smooth scroll was routinely cancelled by the thread pinning itself to the bottom on new messages, media load and template resolution; the position is now set outright and re-asserted on the next frame for media that measures late.
- The sending spinner clears as soon as the message lands instead of lingering.
- The contact-list avatar matches the conversation list, and the refresh button no longer overflows on narrow screens.
1.0.7 — 24 Jul 2026
- Delivery-status ticks on message bubbles. Single grey tick for sent, double grey for delivered, double blue for read, driven by
message.status. - Partial status pushes arriving over the conversation subscription are merged onto the existing bubble instead of replacing it wholesale, so a live delivered/read update no longer blanks the message body or avatar.
- Requires
@snugdesk/core0.2.36+, which selects thestatusfield in the message queries and subscription.
1.0.6 — 24 Jul 2026
- Account switcher now labels each account with its Meta verified name, falling back to the phone number.
- Numbered SIM badge per conversation row, with a
Verified Name • +91 98765 43210tooltip, so it is obvious which account a thread belongs to. - Conversation list auto-fills the next page when the container is too tall to scroll, instead of waiting for an infinite-scroll event that can never fire.
- Assorted multi-account layout and dark-theme fixes in the conversation list.
1.0.5 — 24 Jul 2026
- Multiple WhatsApp Business accounts per tenant: account filter (
Allor a specific account) in the conversation list and per-account endpoint resolution. - Fixed interaction-widget resolution when a tenant has more than one WhatsApp widget.
- Updated against a newer
@snugdesk/core.
1.0.2 – 1.0.4 — 22–23 Jul 2026
- AI smart replies and AI translation (per-message, composer live-preview, translate-on-send, whole-conversation mode) routed through Snugdesk's server-side lambda.
- Offline language detection covering script detection plus common Indic-language spellings (Hindi, Marathi, Bhojpuri, Bengali) and short CJK strings.
- Starred messages: star from the message menu, browse in a starred panel with media and Read more, unstar in place or clear all.
- Initial support for multiple WABA numbers.
- The message-edit feature added during this line was removed again before release.
1.0.0 – 1.0.1 — 29 Jun 2026
- First 1.0 release on Angular 21: emoji reactions, quoted replies with jump-to-original, message forwarding, multi-file uploads, audio preview player, per-conversation drafts, and search by phone number.
- Remote fonts are no longer inlined into component CSS — load Open Sans and Font Awesome globally in the host app (see Fonts & Icons).
🛠 Troubleshooting & Tips
- When adjusting the asset output path, make sure the relative URLs in the generated CSS still resolve (keep
/assets/snugdesk-whatsapp/...in the final build). - Upgrade the widget in lockstep with your Angular major version to stay within the supported peer dependency range (
>=21.0.0). - "Authentication failed" on load — the token is empty, expired (30 minutes by default) or was not issued by
/auth/library. Confirm you are rendering the widget only after the token resolves, and remember that replacing[token]on a live component does not re-authenticate. 401from/auth/library— one answer covers a missing, malformed, unknown and revoked credential. Check the header isAuthorization: Bearer <keyId>.<secret>with the dot intact, and that the credential is not revoked in Settings → Developers → API Credentials.403naming a tenant mismatch — theuserIdbelongs to a different tenant than the credential. The tenant is never taken from your request, so the two must line up.- A browser call to
/auth/libraryfails with no status — that route is server-only and its CORS allow-list does not include your site. Move the exchange behind your own backend endpoint.
📬 Support
Need help with deployment, theming, or backend setup?
SNUG Technologies Pvt Ltd
📧 [email protected]
Include your tenant name, Angular version, and a summary of the issue so the support team can assist quickly.
