Skip to content

Account – Channels – Facebook Messenger

  • Path: /account/channels/facebook-messenger (TBD)
  • Parent: channels.md

Connect Facebook Messenger to enable messaging ingestion and outbound responses.

  • Workspace Admin
  • Permissions: channels.connect, chatti_live.manage (TBD)

Reference: Roles & Permissions Model

  • Connect action (OAuth)
  • Connected page(s) list
  • Webhook status + health
  • src/app/account/channels/_components/add-channel-wizard.tsx
  • Empty: not connected
  • Loading: verifying webhook/permissions
  • Error: callback failure / missing scopes
  • Complete: connected and receiving messages
  • Bind page identity to workspace channel entity.

Domain refs: Domain: Channels


FB Messenger inside the Add channel wizard

Section titled “FB Messenger inside the Add channel wizard”

File: src/app/account/channels/_components/add-channel-wizard.tsx

FB Messenger is added through Step 3 (Connect) of the shared Add channel wizard. For FB Messenger, the wizard preserves the existing two-path behavior inside that step:

TabLabelDefault
1New profile✅ active on open
2Select existing one

The step UI uses shadcn Tabs / TabsList / TabsTrigger / TabsContent with content-width list (no w-full) inside the wizard body.


Body content: One control only — the “Add a new profile” button (outline variant, Facebook icon).

Flow:

  1. User clicks “Add a new profile”.
  2. A loading overlay (skeleton rows + spinner) covers the entire dialog panel (absolute inset-0 z-10). The button is disabled while loading.
  3. After the simulated async delay (~2 s), the overlay disappears and a chip appears above the button showing the newly connected profile label (e.g. Profile 1). The chip uses the Facebook icon + profile label.
  4. Multiple profiles can be added sequentially; each produces an additional chip.
  5. Add button becomes enabled as soon as ≥ 1 chip exists (pendingNewConnectionIds.length > 0).
  6. User clicks Add → dialog closes; all chips remain in the table (connections already committed to context).
  7. User clicks CancelremoveConnection is called for every id in pendingNewConnectionIds, then the dialog closes. The table is left unchanged (rollback).

Body content: One control only — a multi-select combobox (SharedProfileCombobox) listing Facebook profiles that are already connected via the Social comments channel and not yet linked to FB Messenger.

Flow:

  1. User opens the dropdown and selects one or more profiles. Selections are stored in local state (pendingSelectedLabels: string[]) only — addConnection is not called on combobox change.
  2. Add button becomes enabled when pendingSelectedLabels.length > 0.
  3. User clicks Add → for each label in pendingSelectedLabels, call addConnection("facebook-messenger", "Facebook", label, "combobox"); then close the dialog. All selected profiles appear in the table.
  4. User clicks Cancel → discard pendingSelectedLabels (no addConnection calls); close the dialog. Nothing is added to the table.

ButtonVariantEnabled rule
CancelGhost / textAlways enabled
ContinuePrimaryTab 1: pendingNewConnectionIds.length > 0; Tab 2: pendingSelectedLabels.length > 0

When the user reaches the final Review step and clicks Add, the wizard commits the assignment to the selected workspace/default KB.


Active tabCancel action
New profileCall removeConnection(id) for every id in pendingNewConnectionIds, then close the wizard
Select existing oneJust close the wizard — no addConnection was ever called, nothing to roll back

interface AddChannelWizardProps {
open: boolean;
onClose: () => void;
initialChannelType?: ChannelId;
}

// Tab navigation
const [activeTab, setActiveTab] = React.useState<"new" | "existing">("new");
// Tab 1 — tracks ids of connections added via "Add a new profile" button
// Used for rollback on Cancel
const [pendingNewConnectionIds, setPendingNewConnectionIds] = React.useState<string[]>([]);
// Tab 1 — loading overlay while simulated OAuth/connect is in progress
const [isLoadingNewProfile, setIsLoadingNewProfile] = React.useState(false);
// Tab 2 — local pending selection; NOT committed to context until Add
const [pendingSelectedLabels, setPendingSelectedLabels] = React.useState<string[]>([]);

Reset all FB Messenger-specific state when open transitions to false:

React.useEffect(() => {
if (!open) {
setActiveTab("new");
setPendingNewConnectionIds([]);
setIsLoadingNewProfile(false);
setPendingSelectedLabels([]);
}
}, [open]);

// Chips shown in Tab 1 — connections added in this dialog session (not via combobox)
const newProfileChips = React.useMemo(
() => connections.filter((c) => pendingNewConnectionIds.includes(c.id)),
[connections, pendingNewConnectionIds]
);
// Options for Tab 2 combobox — Facebook profiles from Social comments not yet in FB Messenger
const facebookProfilesFromSocial = connections.filter(
(c) => c.channelType === "social-comments" && c.platform === "Facebook" && c.addedVia !== "combobox"
);
const alreadyLinkedLabels = connections
.filter((c) => c.channelType === "facebook-messenger" && c.addedVia === "combobox")
.map((c) => c.label);
const availableToSelect = facebookProfilesFromSocial.filter(
(p) => !alreadyLinkedLabels.includes(p.label)
);
// Add button enabled rule
const canAdd =
activeTab === "new"
? pendingNewConnectionIds.length > 0
: pendingSelectedLabels.length > 0;

const handleAddNewProfile = React.useCallback(() => {
if (isLoadingNewProfile) return;
setIsLoadingNewProfile(true);
setTimeout(() => {
const label = nextGlobalFacebookLabel(connections);
const id = addConnection("facebook-messenger", "Facebook", label); // returns new connection id
setPendingNewConnectionIds((prev) => [...prev, id]);
setIsLoadingNewProfile(false);
}, 2000);
}, [isLoadingNewProfile, connections, addConnection]);

Note: addConnection must return the newly created connection’s id so it can be stored in pendingNewConnectionIds for potential rollback. Verify the context API exposes this; if not, derive the id from connections after the state update (e.g. find the last-added connection with matching label).

const handleComboboxChange = (labels: string[]) => {
setPendingSelectedLabels(labels); // local state only — no addConnection call
};
const handleAdd = () => {
if (activeTab === "existing") {
pendingSelectedLabels.forEach((label) => {
addConnection("facebook-messenger", "Facebook", label, "combobox");
});
}
// Tab 1: connections already committed; nothing extra to do
onClose();
};
const handleCancel = () => {
if (activeTab === "new") {
pendingNewConnectionIds.forEach((id) => removeConnection(id));
}
// Tab 2: pendingSelectedLabels never reached context; just close
onClose();
};

JSX structure (outline inside the wizard connect step)

Section titled “JSX structure (outline inside the wizard connect step)”
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "new" | "existing")}
className="flex flex-col"
>
<div className="pt-2">
<TabsList className="h-auto rounded-lg p-1">
<TabsTrigger value="new" className="rounded-md px-3 py-1.5 text-sm font-medium">New profile</TabsTrigger>
<TabsTrigger value="existing" className="rounded-md px-3 py-1.5 text-sm font-medium">Select existing one</TabsTrigger>
</TabsList>
</div>
{/* Tab 1 body */}
<TabsContent value="new" className="mt-0 flex flex-col gap-4 py-5">
{newProfileChips.length > 0 && (
<div className="flex flex-wrap gap-2">
{newProfileChips.map((chip) => (
<span key={chip.id} className="inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-3 py-1 text-[13px] font-medium">
<Image src="/assets/icon-facebook.svg" alt="" width={16} height={16} aria-hidden />
{chip.label}
</span>
))}
</div>
)}
<Button type="button" variant="outline" className="h-auto w-fit gap-2 rounded-lg px-4 py-2"
onClick={handleAddNewProfile} disabled={isLoadingNewProfile}>
<Image src="/assets/icon-facebook.svg" alt="" width={20} height={20} aria-hidden />
Add a new profile
</Button>
</TabsContent>
{/* Tab 2 body */}
<TabsContent value="existing" className="mt-0 py-5">
<SharedProfileCombobox
options={availableToSelect}
selectedLabels={pendingSelectedLabels}
onChange={handleComboboxChange}
label="Select from connected profiles"
placeholder="Filter profiles…"
emptyOptionsMessage="All connected profiles are already added"
/>
</TabsContent>
</Tabs>

Closing the wizard from backdrop click, close button, or Cancel must funnel through the same FB Messenger-aware cancel handler so that any pending new connections are rolled back.


addConnection must return the newly created connection’s id (a string). This is required so handleAddNewProfile can push the id into pendingNewConnectionIds for rollback. If the current context implementation returns void, update the addConnection signature in ChannelsContext and its implementation to return the generated id.


  • POST /channels/facebook-messenger/connect
  • GET /channels/facebook-messenger/status
  • Multiple pages per workspace (TBD)
  • Webhook disabled by provider
  • addConnection returns void → must be updated to return id (see “addConnection return value contract” above)
  • Secrets and webhook tokens must be protected

Reference: Security & Compliance

  • channel.connected
  • fb_messenger.profile.added_new — fired when a new profile chip is committed (Tab 1, Add clicked)
  • fb_messenger.profile.added_existing — fired for each profile added via Tab 2 combobox (Add clicked)
  • fb_messenger.profile.add_cancelled — fired when Cancel is clicked (either tab)

Reference: Analytics Events (MVP)