ContactIdentities
Every account one contact is reachable on (email, phone, WhatsApp, social) with a primary per channel, a link form that rejects duplicates, and a consent switch per channel.
Code
<ContactIdentities identities={[{ id: "1", channel: "email", value: "[email protected]", primary: true }]} consent={{ email: { status: "granted", at: new Date() } }} onAdd={async (channel, value) => save(channel, value)} onRemove={async (identity) => remove(identity.id)} onSetPrimary={async (identity) => makePrimary(identity.id)} onConsentChange={async (channel, status) => setConsent(channel, status)}/>CRM · beta
Live examples and controls: ContactIdentities in the lab.
Install
npx shadcn@latest add https://docs.nasaqui.com/r/contact-identities.jsonOne person, many accounts. It lists every address, number and handle a contact can be reached on, lets you link a new one (rejecting one already on the list), choose a primary per channel, remove one, and record whether the person agreed to be contacted on each channel.
When to use
- The identity section of a contact record.
- Anywhere consent has to be visible next to the address it applies to.
When not to use
- A single email field on a form: use
FieldandInput. - Merging two duplicate contacts: use
ContactMerge.
Import
import { ContactIdentities } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"Quick start
<ContactIdentities
identities={[{ id: "1", channel: "email", value: "[email protected]", primary: true }]}
consent={{ email: { status: "granted", at: new Date() } }}
onAdd={async (channel, value) => save(channel, value)}
onRemove={async (identity) => remove(identity.id)}
onSetPrimary={async (identity) => makePrimary(identity.id)}
onConsentChange={async (channel, status) => setConsent(channel, status)}
/>Anatomy
- Identity list: channel badge (text), value (left-to-right), primary and verified marks. Row actions open from a context-click or the row menu.
- Link form: channel select and value, with inline validation.
- Consent: one switch per consent channel, with the status word, date and source.
- Remove confirmation dialog.
API
| Prop | Type | Description |
|---|---|---|
identities | ContactIdentity[] | { id, channel, value, label?, primary?, verified? }. |
consent | Partial<Record<channel, ContactConsent>> | { status: "granted" or "denied" or "unknown", at?, source? }. |
channels | ContactChannel[] | Channels offered when linking. |
consentChannels | ContactChannel[] | Channels that get a consent switch. Default email, whatsapp, phone. |
onAdd | ({ channel, value, label? }) => Promise<void or { error? }> | Link an account. |
onRemove / onSetPrimary | (identity) => Promise<...> | Row actions. |
onConsentChange | (channel, "granted" or "denied") => Promise<...> | Switch flipped. |
readOnly | boolean | Hide every action. |
labels | ContactIdentitiesLabelOverrides | Override any string. |
Pure helpers: normalizeContactIdentity, contactIdentityKey, validateContactIdentity, mergeContactConsent, sortContactIdentities.
Examples
Read only
<ContactIdentities identities={identities} consent={consent} readOnly />Accessibility
Each switch is named with its channel. Errors use role="alert". Status is a word, never colour alone. The remove
dialog returns focus to the row.
RTL & i18n
English and Arabic ship. Emails, phone numbers and handles stay left-to-right. Channel names are text badges, so no brand mark is redrawn.
Styling & tokens
Semantic tokens only, logical Tailwind classes. Extend with className.
Do / Don't
- Do treat consent as per channel: agreeing to email is not agreeing to WhatsApp.
- Don't pre-set consent to granted.
Related
Lab
https://docs.nasaqui.com/?path=/docs/components-crm-contact-identities--docs
CompanyList
A list of companies as a table or cards with logo, domain, industry, location, contacts count, tags, owner and last activity, plus search, industry and tag and owner filters, bulk select, loading skeletons and an empty state.
ContactList
A list of contacts as a table or cards with avatar, company, stage, tags, owner and last activity, plus search, stage and tag and owner filters, bulk select, loading skeletons and an empty state.