Account – Channels – Facebook Messenger
- Path:
/account/channels/facebook-messenger(TBD) - Parent:
channels.md
Purpose (business goal)
Section titled “Purpose (business goal)”Connect Facebook Messenger to enable messaging ingestion and outbound responses.
User roles & permissions
Section titled “User roles & permissions”- Workspace Admin
- Permissions:
channels.connect,chatti_live.manage(TBD)
Reference: Roles & Permissions Model
Layout structure
Section titled “Layout structure”- Connect action (OAuth)
- Connected page(s) list
- Webhook status + health
Components used
Section titled “Components used”src/app/account/channels/_components/add-channel-wizard.tsx
States
Section titled “States”- Empty: not connected
- Loading: verifying webhook/permissions
- Error: callback failure / missing scopes
- Complete: connected and receiving messages
Business logic (high-level)
Section titled “Business logic (high-level)”- 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
Overview
Section titled “Overview”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:
| Tab | Label | Default |
|---|---|---|
| 1 | New profile | ✅ active on open |
| 2 | Select existing one | — |
The step UI uses shadcn Tabs / TabsList / TabsTrigger / TabsContent with content-width list (no w-full) inside the wizard body.
Tab 1 — “New profile” (default)
Section titled “Tab 1 — “New profile” (default)”Body content: One control only — the “Add a new profile” button (outline variant, Facebook icon).
Flow:
- User clicks “Add a new profile”.
- A loading overlay (skeleton rows + spinner) covers the entire dialog panel (
absolute inset-0 z-10). The button is disabled while loading. - 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. - Multiple profiles can be added sequentially; each produces an additional chip.
- Add button becomes enabled as soon as ≥ 1 chip exists (
pendingNewConnectionIds.length > 0). - User clicks Add → dialog closes; all chips remain in the table (connections already committed to context).
- User clicks Cancel →
removeConnectionis called for every id inpendingNewConnectionIds, then the dialog closes. The table is left unchanged (rollback).
Tab 2 — “Select existing one”
Section titled “Tab 2 — “Select existing one””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:
- User opens the dropdown and selects one or more profiles. Selections are stored in local state (
pendingSelectedLabels: string[]) only —addConnectionis not called on combobox change. - Add button becomes enabled when
pendingSelectedLabels.length > 0. - User clicks Add → for each label in
pendingSelectedLabels, calladdConnection("facebook-messenger", "Facebook", label, "combobox"); then close the dialog. All selected profiles appear in the table. - User clicks Cancel → discard
pendingSelectedLabels(noaddConnectioncalls); close the dialog. Nothing is added to the table.
Wizard navigation / final commit
Section titled “Wizard navigation / final commit”| Button | Variant | Enabled rule |
|---|---|---|
| Cancel | Ghost / text | Always enabled |
| Continue | Primary | Tab 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.
Cancel behaviour summary
Section titled “Cancel behaviour summary”| Active tab | Cancel action |
|---|---|
| New profile | Call removeConnection(id) for every id in pendingNewConnectionIds, then close the wizard |
| Select existing one | Just close the wizard — no addConnection was ever called, nothing to roll back |
Wizard integration contract
Section titled “Wizard integration contract”interface AddChannelWizardProps { open: boolean; onClose: () => void; initialChannelType?: ChannelId;}// Tab navigationconst [activeTab, setActiveTab] = React.useState<"new" | "existing">("new");
// Tab 1 — tracks ids of connections added via "Add a new profile" button// Used for rollback on Cancelconst [pendingNewConnectionIds, setPendingNewConnectionIds] = React.useState<string[]>([]);
// Tab 1 — loading overlay while simulated OAuth/connect is in progressconst [isLoadingNewProfile, setIsLoadingNewProfile] = React.useState(false);
// Tab 2 — local pending selection; NOT committed to context until Addconst [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]);Derived values
Section titled “Derived values”// 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 Messengerconst 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 ruleconst canAdd = activeTab === "new" ? pendingNewConnectionIds.length > 0 : pendingSelectedLabels.length > 0;Handlers
Section titled “Handlers”handleAddNewProfile
Section titled “handleAddNewProfile”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:
addConnectionmust return the newly created connection’sidso it can be stored inpendingNewConnectionIdsfor potential rollback. Verify the context API exposes this; if not, derive the id fromconnectionsafter the state update (e.g. find the last-added connection with matching label).
handleComboboxChange (Tab 2)
Section titled “handleComboboxChange (Tab 2)”const handleComboboxChange = (labels: string[]) => { setPendingSelectedLabels(labels); // local state only — no addConnection call};handleAdd
Section titled “handleAdd”const handleAdd = () => { if (activeTab === "existing") { pendingSelectedLabels.forEach((label) => { addConnection("facebook-messenger", "Facebook", label, "combobox"); }); } // Tab 1: connections already committed; nothing extra to do onClose();};handleCancel
Section titled “handleCancel”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>Backdrop click
Section titled “Backdrop click”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 return value contract
Section titled “addConnection return value contract”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.
API dependencies
Section titled “API dependencies”POST /channels/facebook-messenger/connectGET /channels/facebook-messenger/status
Enterprise constraints
Section titled “Enterprise constraints”- Multiple pages per workspace (TBD)
Edge cases
Section titled “Edge cases”- Webhook disabled by provider
addConnectionreturnsvoid→ must be updated to returnid(see “addConnection return value contract” above)
Security & compliance considerations
Section titled “Security & compliance considerations”- Secrets and webhook tokens must be protected
Reference: Security & Compliance
Analytics events (if applicable)
Section titled “Analytics events (if applicable)”channel.connectedfb_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)