fix: MCP OAuth 303 redirect, send dialog auto-close, bank details null payload (#175)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-04 10:27:40 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent c4a6d16e94
commit e2d9c8bb85
3 changed files with 37 additions and 53 deletions
+5 -2
View File
@@ -38,7 +38,7 @@ function errorRedirect(redirectUri: string, state: string | null, error: string,
url.searchParams.set('error', error) url.searchParams.set('error', error)
url.searchParams.set('error_description', desc) url.searchParams.set('error_description', desc)
if (state) url.searchParams.set('state', state) 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) callbackUrl.searchParams.set('code', code)
if (state) callbackUrl.searchParams.set('state', state) 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 { function escapeHtml(str: string): string {
@@ -98,14 +98,15 @@ export function BankDetailsSetupDialog({ open, onOpenChange, onComplete }: BankD
// Include bank name from combobox // Include bank name from combobox
data.bank_name = bankName data.bank_name = bankName
// Clean empty strings to null for API // Omit empty strings — API schema accepts undefined but not null
const payload: Record<string, string | number | null> = {} const payload: Record<string, string | number | null> = {}
for (const [key, val] of Object.entries(data)) { for (const [key, val] of Object.entries(data)) {
if (key === 'next_invoice_number') { if (key === 'next_invoice_number') {
const num = val ? parseInt(val as string, 10) : null const num = val ? parseInt(val as string, 10) : null
if (num !== null) payload[key] = num if (num !== null) payload[key] = num
} else { } else {
payload[key] = (val as string) || null const str = val as string
if (str) payload[key] = str
} }
} }
+29 -49
View File
@@ -15,7 +15,7 @@ import { JournalEntryReviewContent } from '@/components/bookkeeping/JournalEntry
import { proposeSendLines } from '@/lib/bookkeeping/propose-send-lines' import { proposeSendLines } from '@/lib/bookkeeping/propose-send-lines'
import { formatCurrency } from '@/lib/utils' import { formatCurrency } from '@/lib/utils'
import { createClient } from '@/lib/supabase/client' 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' import type { Invoice, InvoiceItem, Customer, EntityType } from '@/types'
interface InvoiceWithRelations extends Invoice { interface InvoiceWithRelations extends Invoice {
@@ -43,7 +43,6 @@ export default function SendInvoiceDialog({
const supabase = createClient() const supabase = createClient()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [sentMessage, setSentMessage] = useState<string | null>(null)
const [accountingMethod, setAccountingMethod] = useState<'accrual' | 'cash'>('accrual') const [accountingMethod, setAccountingMethod] = useState<'accrual' | 'cash'>('accrual')
const [entityType, setEntityType] = useState<EntityType>('enskild_firma') const [entityType, setEntityType] = useState<EntityType>('enskild_firma')
const [periodName, setPeriodName] = useState('') const [periodName, setPeriodName] = useState('')
@@ -52,7 +51,6 @@ export default function SendInvoiceDialog({
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setIsInitialized(false) setIsInitialized(false)
setSentMessage(null)
return return
} }
@@ -147,7 +145,11 @@ export default function SendInvoiceDialog({
onSuccess() onSuccess()
if (mode === 'email') { 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 { } else {
// For manual send, just close — no email to confirm // For manual send, just close — no email to confirm
onOpenChange(false) onOpenChange(false)
@@ -193,18 +195,7 @@ export default function SendInvoiceDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{sentMessage ? ( {!isInitialized ? (
<div className="flex items-start gap-3 rounded-lg border border-success/30 bg-success/5 p-4">
<CheckCircle2 className="h-5 w-5 text-success shrink-0 mt-0.5" />
<div className="space-y-1 text-sm">
<p className="font-medium">E-post skickad</p>
<p className="text-muted-foreground">{sentMessage}</p>
{accountingMethod === 'accrual' && (
<p className="text-muted-foreground">Bokföringsverifikationen har skapats.</p>
)}
</div>
</div>
) : !isInitialized ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
@@ -239,39 +230,28 @@ export default function SendInvoiceDialog({
)} )}
<DialogFooter> <DialogFooter>
{sentMessage ? ( <Button
<Button variant="outline"
onClick={handleClose} onClick={handleClose}
className="w-full sm:w-auto min-h-11" disabled={isSubmitting}
> className="w-full sm:w-auto min-h-11"
Stäng >
</Button> Avbryt
) : ( </Button>
<> <Button
<Button onClick={handleConfirm}
variant="outline" disabled={isSubmitting || !isInitialized}
onClick={handleClose} className="w-full sm:w-auto min-h-11"
disabled={isSubmitting} >
className="w-full sm:w-auto min-h-11" {isSubmitting ? (
> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Avbryt ) : mode === 'email' ? (
</Button> <Mail className="mr-2 h-4 w-4" />
<Button ) : (
onClick={handleConfirm} <Send className="mr-2 h-4 w-4" />
disabled={isSubmitting || !isInitialized} )}
className="w-full sm:w-auto min-h-11" {mode === 'email' ? 'Skicka faktura' : 'Markera som skickad'}
> </Button>
{isSubmitting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : mode === 'email' ? (
<Mail className="mr-2 h-4 w-4" />
) : (
<Send className="mr-2 h-4 w-4" />
)}
{mode === 'email' ? 'Skicka faktura' : 'Markera som skickad'}
</Button>
</>
)}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>