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:
co-authored by
Claude Opus 4.6
parent
c4a6d16e94
commit
e2d9c8bb85
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user