Merge pull request #12 from ArnasDon/fix/send-route-and-handler

fix: outbound messages silently failing (Meta args + schema + silent errors)
This commit is contained in:
Arnas Donauskas 2026-04-16 21:25:40 +03:00 committed by GitHub
commit 0a5d274e02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 27 deletions

View File

@ -156,6 +156,15 @@ export default function InboxPage() {
}); });
}, []); }, []);
const handleUpdateMessage = useCallback(
(id: string, updates: Partial<Message>) => {
setMessages((prev) =>
prev.map((m) => (m.id === id ? { ...m, ...updates } : m))
);
},
[]
);
const handleStatusChange = useCallback( const handleStatusChange = useCallback(
(conversationId: string, status: ConversationStatus) => { (conversationId: string, status: ConversationStatus) => {
setConversations((prev) => setConversations((prev) =>
@ -197,6 +206,7 @@ export default function InboxPage() {
messages={messages} messages={messages}
onMessagesLoaded={handleMessagesLoaded} onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage} onNewMessage={handleNewMessage}
onUpdateMessage={handleUpdateMessage}
onStatusChange={handleStatusChange} onStatusChange={handleStatusChange}
/> />

View File

@ -100,40 +100,57 @@ export async function POST(request: Request) {
const accessToken = decrypt(config.access_token) const accessToken = decrypt(config.access_token)
// Send via Meta API // Send via Meta API
// Function signatures are:
// sendTextMessage(phoneNumberId, accessToken, to, text)
// sendTemplateMessage(phoneNumberId, accessToken, to, templateName, language?, params?)
// The args were previously swapped (accessToken before phoneNumberId),
// causing Meta to reject every send.
let waMessageId: string let waMessageId: string
try {
if (message_type === 'template') { if (message_type === 'template') {
const result = await sendTemplateMessage( const result = await sendTemplateMessage(
accessToken,
config.phone_number_id, config.phone_number_id,
accessToken,
sanitizedPhone, sanitizedPhone,
template_name, template_name,
undefined, // use default language (en_US)
template_params || [] template_params || []
) )
waMessageId = result.messageId waMessageId = result.messageId
} else { } else {
const result = await sendTextMessage( const result = await sendTextMessage(
accessToken,
config.phone_number_id, config.phone_number_id,
accessToken,
sanitizedPhone, sanitizedPhone,
content_text content_text
) )
waMessageId = result.messageId waMessageId = result.messageId
} }
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown Meta API error'
console.error('Meta API send failed:', message)
return NextResponse.json(
{ error: `Meta API error: ${message}` },
{ status: 502 }
)
}
// Insert message into DB // Insert message into DB — field names MUST match the messages schema
// (see supabase/migrations/001_initial_schema.sql):
// conversation_id, sender_type, content_type, content_text,
// media_url, template_name, message_id, status, created_at
const { data: messageRecord, error: msgError } = await supabase const { data: messageRecord, error: msgError } = await supabase
.from('messages') .from('messages')
.insert({ .insert({
conversation_id, conversation_id,
user_id: user.id, sender_type: 'agent',
whatsapp_message_id: waMessageId, content_type: message_type,
direction: 'outbound',
message_type,
content_text: content_text || null, content_text: content_text || null,
media_url: media_url || null, media_url: media_url || null,
template_name: template_name || null,
message_id: waMessageId,
status: 'sent', status: 'sent',
timestamp: new Date().toISOString(),
}) })
.select() .select()
.single() .single()
@ -141,7 +158,7 @@ export async function POST(request: Request) {
if (msgError) { if (msgError) {
console.error('Error inserting sent message:', msgError) console.error('Error inserting sent message:', msgError)
return NextResponse.json( return NextResponse.json(
{ error: 'Message sent but failed to save to database' }, { error: `Message sent to Meta but failed to save to DB: ${msgError.message}` },
{ status: 500 } { status: 500 }
) )
} }

View File

@ -22,6 +22,7 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageBubble } from "./message-bubble"; import { MessageBubble } from "./message-bubble";
import { MessageComposer } from "./message-composer"; import { MessageComposer } from "./message-composer";
import { toast } from "sonner";
interface MessageThreadProps { interface MessageThreadProps {
conversation: Conversation | null; conversation: Conversation | null;
@ -29,6 +30,7 @@ interface MessageThreadProps {
messages: Message[]; messages: Message[];
onMessagesLoaded: (messages: Message[]) => void; onMessagesLoaded: (messages: Message[]) => void;
onNewMessage: (message: Message) => void; onNewMessage: (message: Message) => void;
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
onStatusChange: (conversationId: string, status: ConversationStatus) => void; onStatusChange: (conversationId: string, status: ConversationStatus) => void;
} }
@ -68,6 +70,7 @@ export function MessageThread({
messages, messages,
onMessagesLoaded, onMessagesLoaded,
onNewMessage, onNewMessage,
onUpdateMessage,
onStatusChange, onStatusChange,
}: MessageThreadProps) { }: MessageThreadProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -169,9 +172,11 @@ export function MessageThread({
async (text: string) => { async (text: string) => {
if (!conversation) return; if (!conversation) return;
// Optimistic update const tempId = `temp-${Date.now()}`;
// Optimistic update — shows the message immediately with "sending" status
const optimisticMsg: Message = { const optimisticMsg: Message = {
id: `temp-${Date.now()}`, id: tempId,
conversation_id: conversation.id, conversation_id: conversation.id,
sender_type: "agent", sender_type: "agent",
content_type: "text", content_type: "text",
@ -192,14 +197,29 @@ export function MessageThread({
}), }),
}); });
const payload = await res.json().catch(() => ({}));
if (!res.ok) { if (!res.ok) {
console.error("Failed to send message"); const reason = payload?.error || `HTTP ${res.status}`;
console.error("Failed to send message:", reason);
toast.error(`Failed to send: ${reason}`);
// Mark the optimistic bubble as failed so the user sees what happened
onUpdateMessage(tempId, { status: "failed" });
return;
} }
// Success — the realtime INSERT event will replace the temp bubble
// with the real DB row. If realtime hasn't arrived yet, at least
// flip status to 'sent' so the UI stops showing "sending".
onUpdateMessage(tempId, { status: "sent" });
} catch (err) { } catch (err) {
console.error("Failed to send message:", err); console.error("Failed to send message:", err);
const reason = err instanceof Error ? err.message : "network error";
toast.error(`Failed to send: ${reason}`);
onUpdateMessage(tempId, { status: "failed" });
} }
}, },
[conversation, onNewMessage] [conversation, onNewMessage, onUpdateMessage]
); );
const handleStatusChange = useCallback( const handleStatusChange = useCallback(