fix(entitlements): billing reachable in settings modal + conversion redesign (#821)
- Register BillingSettingsContent in SETTINGS_SECTIONS so the settings MODAL renders Abonnemang (it was falling back to Företag — billing was only a standalone page, never a registered section). Page is now a thin wrapper over the same component. - Add GET /api/billing/status (isPaying / configured / trialEndsAt) so the client section gets state without server-only reads. - Redesign for conversion: trial days-left urgency banner, reactive monthly/yearly price with a 'Spara 2 mån' badge, full-width price-bearing CTA, Stripe trust line, design-system-compliant chrome (flat Card, no shadow/rounded-xl, on-scale spacing, serif headline). Trialing companies now see the upgrade path (not the manage button). The reported 'peach band' was not reproduced in code — no peach/salmon color exists in the app CSS and the only bottom drag-handle is in a md:hidden mobile sheet; most likely a macOS screenshot/desktop artifact. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
60e33c4b51
commit
0e8698f538
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { requireAuth } from '@/lib/auth/require-auth'
|
||||
import { requireCompanyId } from '@/lib/company/context'
|
||||
import { isStripeConfigured } from '@/lib/stripe/client'
|
||||
|
||||
/**
|
||||
* Billing status for the client-rendered billing section (which lives inside the
|
||||
* settings modal Dialog and can't read the DB server-side). Returns whether the
|
||||
* company is paying, whether Stripe checkout is configured, and the trial expiry
|
||||
* (for the days-left urgency banner). Read-only.
|
||||
*/
|
||||
export async function GET() {
|
||||
const { user, supabase, error } = await requireAuth()
|
||||
if (error) return error
|
||||
|
||||
let companyId: string | null = null
|
||||
try {
|
||||
companyId = await requireCompanyId(supabase, user.id)
|
||||
} catch {
|
||||
companyId = null
|
||||
}
|
||||
|
||||
let isPaying = false
|
||||
let trialEndsAt: string | null = null
|
||||
if (companyId) {
|
||||
const { data: sub } = await supabase
|
||||
.from('company_subscriptions')
|
||||
.select('status')
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
const status = (sub as { status: string | null } | null)?.status ?? null
|
||||
// Paying = a real subscription. Deliberately excludes 'trialing' so a
|
||||
// trialing company still sees the upgrade path (not the manage button).
|
||||
isPaying = status === 'active' || status === 'past_due'
|
||||
|
||||
const { data: trial } = await supabase
|
||||
.from('capability_grants')
|
||||
.select('expires_at')
|
||||
.eq('company_id', companyId)
|
||||
.eq('source', 'trial')
|
||||
.order('expires_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
trialEndsAt = (trial as { expires_at: string | null } | null)?.expires_at ?? null
|
||||
}
|
||||
|
||||
return NextResponse.json({ isPaying, configured: isStripeConfigured(), trialEndsAt })
|
||||
}
|
||||
Reference in New Issue
Block a user