f49dc3438d
* fix(sandbox): lock what the sandbox cannot actually do Three surfaces in the sandbox advertised capability the sandbox blocks outright, or rendered a staged preview wrong. Skatteverket promo card: hidden for sandbox companies. The sandbox landing page tells users Skatteverket is off, and the authorize route 403s via guardSandbox, so the dashboard nudge was a dead end. Same precedent as TaxSettingsContent, which already hides its Skatteverket section on is_sandbox. Dokumentinkorg: locked with a state that says what the workspace does and sends the user to registration. Checked before the capability gate on purpose: the seed_trial trigger grants every new company (sandbox included) 30 days of every paid capability, so the existing paywall waved a demo company straight through. The CTA signs the anonymous session out first, mirroring SandboxBanner. Staged categorize_transaction preview: the seed wrote its kontering under the generic preview_lines key, but categorize_transaction is the one type with a dedicated preview component, and it reads `lines`. The card fell through to its legacy summary branch and rendered blank Debetkonto and Kreditkonto plus "NaN kr" from formatCurrency(undefined). The seeded blob now mirrors what gnubok_categorize_transaction stages, extracted into buildSandboxPendingOperations so both shapes are unit-testable. CategorizePreview also learns to read preview_lines and to show a missing amount as a gap, so a live 24h sandbox stops showing NaN before its data expires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sandbox): don't leave for /register when sign-out failed CodeRabbit review: the ExtensionSandboxLockState CTA ignored the signOut() result, so a failure routed to /register with the anonymous session still live, which registers INTO the sandbox instead of leaving it: exactly what the sign-out exists to prevent. Surface the failure and stay put so the user can retry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Button } from '@/components/ui/button'
|
|
import { EmptyState } from '@/components/ui/empty-state'
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { resolveIcon } from '@/lib/extensions/icon-resolver'
|
|
|
|
interface ExtensionSandboxLockStateProps {
|
|
iconName?: string
|
|
title: string
|
|
/** One line on what the workspace actually does, so a locked page still explains itself. */
|
|
description: string
|
|
/** Why it is locked here and what unlocks it. */
|
|
note: string
|
|
ctaLabel: string
|
|
}
|
|
|
|
/**
|
|
* Sandbox lock for an extension workspace whose value is an external service
|
|
* (invoice-inbox: AI field extraction plus the forwarding mail address). The
|
|
* sandbox blocks those services outright (lib/sandbox/guard.ts), so an
|
|
* unlocked workspace looks functional and then quietly does nothing.
|
|
*
|
|
* Deliberately not ExtensionUpsellState: an anonymous demo user has no billing
|
|
* to upgrade, they need an account. The CTA signs the anonymous session out
|
|
* first, mirroring SandboxBanner: /register on top of a live anonymous session
|
|
* registers into the sandbox instead of leaving it.
|
|
*
|
|
* Same RSC constraint as ExtensionUpsellState: every prop stays a plain string
|
|
* and the icon is resolved client-side from its name, because passing a
|
|
* resolved component across the server/client boundary 500s the page.
|
|
*/
|
|
export function ExtensionSandboxLockState({
|
|
iconName,
|
|
title,
|
|
description,
|
|
note,
|
|
ctaLabel,
|
|
}: ExtensionSandboxLockStateProps) {
|
|
const [isLeaving, setIsLeaving] = useState(false)
|
|
const router = useRouter()
|
|
const t = useTranslations('extensions')
|
|
const { toast } = useToast()
|
|
|
|
async function handleCreateAccount() {
|
|
setIsLeaving(true)
|
|
const supabase = createClient()
|
|
const { error } = await supabase.auth.signOut()
|
|
if (error) {
|
|
// Navigating anyway would land on /register with the anonymous session
|
|
// still live, which registers INTO the sandbox: the exact outcome the
|
|
// sign-out exists to prevent. Stay put and let the user retry.
|
|
toast({
|
|
title: t('sandbox_locked_signout_error_title'),
|
|
description: t('sandbox_locked_signout_error_description'),
|
|
variant: 'destructive',
|
|
})
|
|
setIsLeaving(false)
|
|
return
|
|
}
|
|
router.push('/register')
|
|
}
|
|
|
|
const Icon = iconName ? resolveIcon(iconName) : undefined
|
|
|
|
return (
|
|
<EmptyState icon={Icon} title={title} description={description}>
|
|
<div className="flex flex-col items-center gap-4">
|
|
<p className="max-w-sm text-sm text-muted-foreground text-balance">{note}</p>
|
|
<Button onClick={handleCreateAccount} disabled={isLeaving}>
|
|
{ctaLabel}
|
|
</Button>
|
|
</div>
|
|
</EmptyState>
|
|
)
|
|
}
|