From e2d9c8bb85bed0626df73fb828fdcb995d8a8cfe Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 4 Apr 2026 10:27:40 +0200 Subject: [PATCH] fix: MCP OAuth 303 redirect, send dialog auto-close, bank details null payload (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MCP OAuth 303 redirect, send dialog auto-close, bank details null payload - OAuth authorize: use 303 See Other instead of default 307, which preserved POST method and caused Claude's callback to return 405 - SendInvoiceDialog: close dialog and show toast after email send instead of leaving a success message that requires manual close - BankDetailsSetupDialog: omit empty fields from payload instead of sending null, which fails Zod validation on non-nullable schema fields Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: remove dead sentMessage state and fix stale comment Remove sentMessage state, its success banner JSX, and the CheckCircle2 import — all unreachable after the dialog now auto-closes on email send. Fix stale "to null" comment in BankDetailsSetupDialog. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- app/api/mcp-oauth/authorize/route.ts | 7 +- .../invoices/BankDetailsSetupDialog.tsx | 5 +- components/invoices/SendInvoiceDialog.tsx | 78 +++++++------------ 3 files changed, 37 insertions(+), 53 deletions(-) diff --git a/app/api/mcp-oauth/authorize/route.ts b/app/api/mcp-oauth/authorize/route.ts index 40d60207..c49acf81 100644 --- a/app/api/mcp-oauth/authorize/route.ts +++ b/app/api/mcp-oauth/authorize/route.ts @@ -38,7 +38,7 @@ function errorRedirect(redirectUri: string, state: string | null, error: string, url.searchParams.set('error', error) url.searchParams.set('error_description', desc) if (state) url.searchParams.set('state', state) - return NextResponse.redirect(url.toString()) + return NextResponse.redirect(url.toString(), 303) } /** @@ -205,7 +205,10 @@ export async function POST(request: Request) { callbackUrl.searchParams.set('code', code) if (state) callbackUrl.searchParams.set('state', state) - return NextResponse.redirect(callbackUrl.toString()) + // 303 See Other: forces browser to GET the callback URL, even though this + // handler was reached via POST. NextResponse.redirect() defaults to 307, + // which preserves POST and causes Claude's callback to return 405. + return NextResponse.redirect(callbackUrl.toString(), 303) } function escapeHtml(str: string): string { diff --git a/components/invoices/BankDetailsSetupDialog.tsx b/components/invoices/BankDetailsSetupDialog.tsx index 713741aa..5dfd5b32 100644 --- a/components/invoices/BankDetailsSetupDialog.tsx +++ b/components/invoices/BankDetailsSetupDialog.tsx @@ -98,14 +98,15 @@ export function BankDetailsSetupDialog({ open, onOpenChange, onComplete }: BankD // Include bank name from combobox data.bank_name = bankName - // Clean empty strings to null for API + // Omit empty strings — API schema accepts undefined but not null const payload: Record = {} for (const [key, val] of Object.entries(data)) { if (key === 'next_invoice_number') { const num = val ? parseInt(val as string, 10) : null if (num !== null) payload[key] = num } else { - payload[key] = (val as string) || null + const str = val as string + if (str) payload[key] = str } } diff --git a/components/invoices/SendInvoiceDialog.tsx b/components/invoices/SendInvoiceDialog.tsx index 3a2d0bcd..0ce73dec 100644 --- a/components/invoices/SendInvoiceDialog.tsx +++ b/components/invoices/SendInvoiceDialog.tsx @@ -15,7 +15,7 @@ import { JournalEntryReviewContent } from '@/components/bookkeeping/JournalEntry import { proposeSendLines } from '@/lib/bookkeeping/propose-send-lines' import { formatCurrency } from '@/lib/utils' import { createClient } from '@/lib/supabase/client' -import { CheckCircle2, Loader2, Mail, Send } from 'lucide-react' +import { Loader2, Mail, Send } from 'lucide-react' import type { Invoice, InvoiceItem, Customer, EntityType } from '@/types' interface InvoiceWithRelations extends Invoice { @@ -43,7 +43,6 @@ export default function SendInvoiceDialog({ const supabase = createClient() const [isSubmitting, setIsSubmitting] = useState(false) - const [sentMessage, setSentMessage] = useState(null) const [accountingMethod, setAccountingMethod] = useState<'accrual' | 'cash'>('accrual') const [entityType, setEntityType] = useState('enskild_firma') const [periodName, setPeriodName] = useState('') @@ -52,7 +51,6 @@ export default function SendInvoiceDialog({ useEffect(() => { if (!open) { setIsInitialized(false) - setSentMessage(null) return } @@ -147,7 +145,11 @@ export default function SendInvoiceDialog({ onSuccess() if (mode === 'email') { - setSentMessage(data.message || `Fakturan har skickats till ${invoice.customer.email}`) + onOpenChange(false) + toast({ + title: 'Faktura skickad', + description: data.message || `Fakturan har skickats till ${invoice.customer.email}`, + }) } else { // For manual send, just close — no email to confirm onOpenChange(false) @@ -193,18 +195,7 @@ export default function SendInvoiceDialog({ - {sentMessage ? ( -
- -
-

E-post skickad

-

{sentMessage}

- {accountingMethod === 'accrual' && ( -

Bokföringsverifikationen har skapats.

- )} -
-
- ) : !isInitialized ? ( + {!isInitialized ? (
@@ -239,39 +230,28 @@ export default function SendInvoiceDialog({ )} - {sentMessage ? ( - - ) : ( - <> - - - - )} + +