({
organizationId,
noCardWrapper = false,
}: BillingStatusProps)
| 54 | } |
| 55 | |
| 56 | export function BillingStatus({ |
| 57 | organizationId, |
| 58 | noCardWrapper = false, |
| 59 | }: BillingStatusProps) { |
| 60 | const isMobile = useIsMobile() |
| 61 | |
| 62 | const billingPortalMutation = useMutation({ |
| 63 | mutationFn: async () => { |
| 64 | const res = await fetch(`/api/orgs/${organizationId}/billing/portal`, { |
| 65 | method: 'POST', |
| 66 | }) |
| 67 | if (!res.ok) { |
| 68 | const error = await res.json().catch(() => ({ error: 'Failed to open billing portal' })) |
| 69 | throw new Error(error.error || 'Failed to open billing portal') |
| 70 | } |
| 71 | const data = await res.json() |
| 72 | return data.url as string |
| 73 | }, |
| 74 | onSuccess: (url) => { |
| 75 | window.open(url, '_blank', 'noopener,noreferrer') |
| 76 | }, |
| 77 | onError: (err: Error) => { |
| 78 | toast({ |
| 79 | title: 'Error', |
| 80 | description: err.message || 'Failed to open billing portal', |
| 81 | variant: 'destructive', |
| 82 | }) |
| 83 | }, |
| 84 | }) |
| 85 | |
| 86 | const { |
| 87 | data: billingStatus, |
| 88 | isLoading, |
| 89 | error, |
| 90 | } = useQuery({ |
| 91 | queryKey: ['billingStatus', organizationId], |
| 92 | queryFn: () => fetchBillingStatus(organizationId), |
| 93 | staleTime: 2 * 60 * 1000, // 2 minutes |
| 94 | refetchOnWindowFocus: false, |
| 95 | }) |
| 96 | |
| 97 | if (isLoading) { |
| 98 | return ( |
| 99 | <Card |
| 100 | className={cn( |
| 101 | 'w-full', |
| 102 | noCardWrapper && 'border-0 shadow-none bg-transparent', |
| 103 | )} |
| 104 | > |
| 105 | <CardHeader |
| 106 | className={noCardWrapper ? 'p-0' : 'px-4 py-3 sm:px-6 sm:py-4'} |
| 107 | > |
| 108 | <CardTitle className="flex items-center text-base sm:text-lg"> |
| 109 | <CreditCard className="mr-2 h-4 w-4 sm:h-5 sm:w-5" /> |
| 110 | Billing & Seats |
| 111 | </CardTitle> |
| 112 | </CardHeader> |
| 113 | <CardContent |
nothing calls this directly
no test coverage detected