wacrm/src/app/(dashboard)/inbox/page.tsx
Arnas Donauskas cf706a8c9d
fix: outbound messages silently failing (wrong Meta args + schema)
Two silent bugs in the send pipeline that made agent messages
never reach WhatsApp AND disappear on page reload:

1. /api/whatsapp/send passed Meta API args in the wrong order.
   The helpers have signature (phoneNumberId, accessToken, to, ...)
   but the route was calling sendTextMessage(accessToken, phoneNumberId,
   to, text). So the request went to
     POST https://graph.facebook.com/v21.0/<ACCESS_TOKEN>/messages
   with Authorization: Bearer <PHONE_NUMBER_ID>
   which Meta rejects. The try block caught the error and the outer
   catch returned a generic "Failed to send message" with no detail.

2. Even if Meta had succeeded, the DB insert would have failed because
   the route was using field names that don't exist in the messages
   table — same class of bug as the webhook had before PR #10:
     user_id           → not a column on messages
     whatsapp_message_id → should be message_id
     direction          → should be sender_type ('customer'|'agent'|'bot')
     message_type       → should be content_type
     timestamp          → should be created_at
   Insert would have failed, DB row would never exist, and the optimistic
   message in the UI would vanish on the next page load / fetchMessages.

3. The frontend swallowed errors silently (console.error only), so the
   UI looked like the message had been sent even when nothing reached
   Meta and nothing was saved.

Changes:

src/app/api/whatsapp/send/route.ts
- Swap Meta API args to the correct order
  (phoneNumberId first, then accessToken)
- Wrap the Meta call in its own try/catch so the error message from
  Meta bubbles up to the client (returned as 502 with the actual text)
- Fix messages insert to use schema-correct columns: sender_type,
  content_type, content_text, media_url, template_name, message_id,
  status. Dropped non-existent user_id/direction/message_type/timestamp.
- Also surface the underlying DB error if the insert fails.

src/components/inbox/message-thread.tsx
- Added onUpdateMessage prop.
- handleSend parses the error JSON, toasts the actual reason, and
  flips the optimistic bubble's status to 'failed' so MessageBubble
  renders the red XCircle. On success flips to 'sent' immediately
  (realtime INSERT still replaces the temp bubble with the real row).

src/app/(dashboard)/inbox/page.tsx
- Added handleUpdateMessage that mutates one message by id.
- Passed as onUpdateMessage to MessageThread.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:21:01 +03:00

221 lines
6.9 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect } from "react";
import { createClient } from "@/lib/supabase/client";
import type { Conversation, Message, Contact, ConversationStatus } from "@/types";
import { useRealtime } from "@/hooks/use-realtime";
import { ConversationList } from "@/components/inbox/conversation-list";
import { MessageThread } from "@/components/inbox/message-thread";
import { ContactSidebar } from "@/components/inbox/contact-sidebar";
import { toast } from "sonner";
import { WifiOff } from "lucide-react";
export default function InboxPage() {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [activeConversation, setActiveConversation] =
useState<Conversation | null>(null);
const [activeContact, setActiveContact] = useState<Contact | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [whatsappConnected, setWhatsappConnected] = useState<boolean | null>(
null
);
// Check WhatsApp connection status on mount
useEffect(() => {
const checkConnection = async () => {
const supabase = createClient();
const {
data: { session },
} = await supabase.auth.getSession();
const user = session?.user;
if (!user) return;
// Table is `whatsapp_config` (singular) — the previous "whatsapp_configs"
// query always returned no rows, so the banner always showed "not connected".
const { data } = await supabase
.from("whatsapp_config")
.select("status")
.eq("user_id", user.id)
.maybeSingle();
setWhatsappConnected(data?.status === "connected");
};
checkConnection();
}, []);
// Handle realtime message events
const handleMessageEvent = useCallback(
(event: { eventType: string; new: Message; old: Partial<Message> }) => {
const newMsg = event.new;
if (event.eventType === "INSERT") {
// Add to messages if it belongs to active conversation
if (
activeConversation &&
newMsg.conversation_id === activeConversation.id
) {
setMessages((prev) => {
// Avoid duplicates
if (prev.some((m) => m.id === newMsg.id)) return prev;
// Replace optimistic message if it exists
const withoutOptimistic = prev.filter(
(m) => !m.id.startsWith("temp-")
);
return [...withoutOptimistic, newMsg];
});
}
// Update conversation list preview
setConversations((prev) =>
prev.map((c) =>
c.id === newMsg.conversation_id
? {
...c,
last_message_text: newMsg.content_text ?? "",
last_message_at: newMsg.created_at,
unread_count:
activeConversation?.id === newMsg.conversation_id
? 0
: c.unread_count + 1,
}
: c
)
);
}
if (event.eventType === "UPDATE") {
// Update message status
setMessages((prev) =>
prev.map((m) => (m.id === newMsg.id ? { ...m, ...newMsg } : m))
);
}
},
[activeConversation]
);
// Handle realtime conversation events
const handleConversationEvent = useCallback(
(event: {
eventType: string;
new: Conversation;
old: Partial<Conversation>;
}) => {
const conv = event.new;
if (event.eventType === "INSERT") {
setConversations((prev) => [conv, ...prev]);
}
if (event.eventType === "UPDATE") {
setConversations((prev) =>
prev.map((c) => (c.id === conv.id ? { ...c, ...conv } : c))
);
// Update active conversation if it changed
if (activeConversation && conv.id === activeConversation.id) {
setActiveConversation((prev) =>
prev ? { ...prev, ...conv } : prev
);
}
}
},
[activeConversation]
);
// Subscribe to realtime
useRealtime({
channelName: "inbox-realtime",
onMessageEvent: handleMessageEvent,
onConversationEvent: handleConversationEvent,
enabled: true,
});
const handleConversationsLoaded = useCallback(
(loaded: Conversation[]) => {
setConversations(loaded);
},
[]
);
const handleSelectConversation = useCallback((conv: Conversation) => {
setActiveConversation(conv);
setActiveContact(conv.contact ?? null);
setMessages([]);
}, []);
const handleMessagesLoaded = useCallback((loaded: Message[]) => {
setMessages(loaded);
}, []);
const handleNewMessage = useCallback((msg: Message) => {
setMessages((prev) => {
if (prev.some((m) => m.id === msg.id)) return prev;
return [...prev, msg];
});
}, []);
const handleUpdateMessage = useCallback(
(id: string, updates: Partial<Message>) => {
setMessages((prev) =>
prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
);
},
[]
);
const handleStatusChange = useCallback(
(conversationId: string, status: ConversationStatus) => {
setConversations((prev) =>
prev.map((c) => (c.id === conversationId ? { ...c, status } : c))
);
if (activeConversation?.id === conversationId) {
setActiveConversation((prev) => (prev ? { ...prev, status } : prev));
}
},
[activeConversation]
);
return (
<div className="-m-6 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden">
{/* WhatsApp connection banner — in the flex column, not absolute,
so it pushes the panels down instead of overlapping them. */}
{whatsappConnected === false && (
<div className="flex shrink-0 items-center justify-center gap-2 border-b border-amber-500/20 bg-amber-500/10 px-4 py-2">
<WifiOff className="h-4 w-4 text-amber-400" />
<p className="text-xs text-amber-400">
WhatsApp is not connected. Go to Settings to connect your account.
</p>
</div>
)}
<div className="flex flex-1 overflow-hidden">
{/* Left panel: Conversation list */}
<ConversationList
activeConversationId={activeConversation?.id ?? null}
onSelect={handleSelectConversation}
conversations={conversations}
onConversationsLoaded={handleConversationsLoaded}
/>
{/* Center panel: Message thread */}
<MessageThread
conversation={activeConversation}
contact={activeContact}
messages={messages}
onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage}
onUpdateMessage={handleUpdateMessage}
onStatusChange={handleStatusChange}
/>
{/* Right panel: Contact sidebar (hidden on small screens) */}
<div className="hidden lg:block">
<ContactSidebar contact={activeContact} />
</div>
</div>
</div>
);
}