diff --git a/src/app/(dashboard)/inbox/page.tsx b/src/app/(dashboard)/inbox/page.tsx index 61968cd..6779558 100644 --- a/src/app/(dashboard)/inbox/page.tsx +++ b/src/app/(dashboard)/inbox/page.tsx @@ -156,6 +156,15 @@ export default function InboxPage() { }); }, []); + const handleUpdateMessage = useCallback( + (id: string, updates: Partial) => { + 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} /> diff --git a/src/app/api/whatsapp/send/route.ts b/src/app/api/whatsapp/send/route.ts index 7823280..b4cc04c 100644 --- a/src/app/api/whatsapp/send/route.ts +++ b/src/app/api/whatsapp/send/route.ts @@ -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 - if (message_type === 'template') { - const result = await sendTemplateMessage( - accessToken, - config.phone_number_id, - sanitizedPhone, - template_name, - template_params || [] + try { + if (message_type === 'template') { + const result = await sendTemplateMessage( + config.phone_number_id, + accessToken, + sanitizedPhone, + template_name, + undefined, // use default language (en_US) + template_params || [] + ) + waMessageId = result.messageId + } else { + const result = await sendTextMessage( + 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 } ) - waMessageId = result.messageId - } else { - const result = await sendTextMessage( - accessToken, - config.phone_number_id, - sanitizedPhone, - content_text - ) - waMessageId = result.messageId } - // 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 } ) } diff --git a/src/components/inbox/message-thread.tsx b/src/components/inbox/message-thread.tsx index 439b29f..f499428 100644 --- a/src/components/inbox/message-thread.tsx +++ b/src/components/inbox/message-thread.tsx @@ -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) => 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(