Files
accounted/components/settings/BillingActions.tsx
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

97 lines
2.9 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useTranslations } from 'next-intl'
import { ChevronRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import type { BillingPlan } from '@/lib/stripe/client'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
import { formatCurrency } from '@/lib/utils'
import { PLAN_PRICES } from '@/components/settings/billing-plans'
/**
* The billing call to action. Paying companies get the Stripe Customer
* Portal (manage/cancel) as a quiet row action; everyone else gets the
* Checkout button for the plan chosen in the offer rows above it. Both POST
* to a route that returns a hosted Stripe URL we redirect to.
*
* `firstChargeDeferred`: the checkout route will defer the first charge to
* the trial's end (see billing/checkout), so the CTA can truthfully say
* "0 kr idag" instead of quoting a price.
*/
export function BillingActions({
isPaying,
configured,
plan = 'yearly',
firstChargeDeferred = false,
}: {
isPaying: boolean
configured: boolean
plan?: BillingPlan
firstChargeDeferred?: boolean
}) {
const t = useTranslations('settings_billing')
const { toast } = useToast()
const [loading, setLoading] = useState(false)
async function go(endpoint: string, payload?: Record<string, unknown>) {
setLoading(true)
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload ?? {}),
})
const data = (await res.json().catch(() => ({}))) as {
url?: string
error?: string | { message?: string }
}
const errorMessage = typeof data.error === 'string' ? data.error : data.error?.message
if (!res.ok || !data.url) throw new Error(errorMessage || t('checkout_failed'))
window.location.href = data.url
} catch (e) {
toast({
title: t('checkout_failed'),
description: e instanceof Error ? getUserErrorMessage(e) : undefined,
variant: 'destructive',
})
setLoading(false)
}
}
if (isPaying) {
return (
<Button variant="outline" size="sm" onClick={() => go('/api/billing/portal')} disabled={loading}>
{t('manage_cta')}
</Button>
)
}
if (!configured) {
return (
<Button size="lg" disabled className="w-full sm:w-auto">
{t('cta_coming_soon')}
</Button>
)
}
const label = loading
? t('cta_opening')
: firstChargeDeferred
? t('cta_start_deferred')
: t('cta_start_now', { price: formatCurrency(PLAN_PRICES[plan].exVat), period: t(`period_${plan}`) })
return (
<Button
size="lg"
onClick={() => go('/api/billing/checkout', { plan })}
disabled={loading}
className="w-full sm:w-auto"
>
{label}
{!loading && <ChevronRight className="h-4 w-4" />}
</Button>
)
}