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(
(conversationId: string, status: ConversationStatus) => {
setConversations((prev) =>
@ -197,6 +206,7 @@ export default function InboxPage() {
messages={messages}
onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage}
onUpdateMessage={handleUpdateMessage}
onStatusChange={handleStatusChange}
/>

View File

@ -100,40 +100,57 @@ export async function POST(request: Request) {
const accessToken = decrypt(config.access_token)
// 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
try {
if (message_type === 'template') {
const result = await sendTemplateMessage(
accessToken,
config.phone_number_id,
accessToken,
sanitizedPhone,
template_name,
undefined, // use default language (en_US)
template_params || []
)
waMessageId = result.messageId
} else {
const result = await sendTextMessage(
accessToken,
config.phone_number_id,
accessToken,
sanitizedPhone,
content_text
)
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
.from('messages')
.insert({
conversation_id,
user_id: user.id,
whatsapp_message_id: waMessageId,
direction: 'outbound',
message_type,
sender_type: 'agent',
content_type: message_type,
content_text: content_text || null,
media_url: media_url || null,
template_name: template_name || null,
message_id: waMessageId,
status: 'sent',
timestamp: new Date().toISOString(),
})
.select()
.single()
@ -141,7 +158,7 @@ export async function POST(request: Request) {
if (msgError) {
console.error('Error inserting sent message:', msgError)
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 }
)
}

View File

@ -22,6 +22,7 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area";
import { MessageBubble } from "./message-bubble";
import { MessageComposer } from "./message-composer";
import { toast } from "sonner";
interface MessageThreadProps {
conversation: Conversation | null;
@ -29,6 +30,7 @@ interface MessageThreadProps {
messages: Message[];
onMessagesLoaded: (messages: Message[]) => void;
onNewMessage: (message: Message) => void;
onUpdateMessage: (id: string, updates: Partial<Message>) => void;
onStatusChange: (conversationId: string, status: ConversationStatus) => void;
}
@ -68,6 +70,7 @@ export function MessageThread({
messages,
onMessagesLoaded,
onNewMessage,
onUpdateMessage,
onStatusChange,
}: MessageThreadProps) {
const [loading, setLoading] = useState(false);
@ -169,9 +172,11 @@ export function MessageThread({
async (text: string) => {
if (!conversation) return;
// Optimistic update
const tempId = `temp-${Date.now()}`;
// Optimistic update — shows the message immediately with "sending" status
const optimisticMsg: Message = {
id: `temp-${Date.now()}`,
id: tempId,
conversation_id: conversation.id,
sender_type: "agent",
content_type: "text",
@ -192,14 +197,29 @@ export function MessageThread({
}),
});
const payload = await res.json().catch(() => ({}));
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) {
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(