mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
Update org billing page with latest design
This commit is contained in:
1 parent
b253238b9e
commit
fe80274b28
8 files changed
+392
-302
No files matched your search
+29
@@ -0,0 +1,29 @@
|
||||
import { FC } from 'react'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
organization: any
|
||||
}
|
||||
|
||||
const AWSMarketplaceSubscription: FC<Props> = ({ organization }) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Typography.Text>
|
||||
This organization is subscribed via AWS Marketplace, with a limit of{' '}
|
||||
{organization.project_limit} projects and expiring on{' '}
|
||||
{organization.aws_marketplace.Entitlements[0].ExpirationDate.slice(0, 10)}.
|
||||
</Typography.Text>
|
||||
|
||||
<Button>
|
||||
<a
|
||||
href="https://aws.amazon.com/marketplace/saas/ordering?productId=dc498450-cecf-44c2-8c99-c3c13f16e70a&offerId=d1htt16t3ygx5brtwhooms0ei"
|
||||
target="_blank"
|
||||
>
|
||||
Manage AWS Marketplace subscription
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AWSMarketplaceSubscription
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FC } from 'react'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { Input, Button } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
address: Dictionary<any>
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const BillingAddress: FC<Props> = ({ loading, address, redirectToPortal }) => {
|
||||
const { city, country, line1, line2, postal_code, state } = address
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Billing address</h4>
|
||||
<p className="text-sm opacity-50">This will be reflected in every invoice</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button type="default" onClick={() => redirectToPortal('/customer/update')}>
|
||||
Edit billing address
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="w-3/5 space-y-2">
|
||||
<Input readOnly placeholder="Address line 1" value={line1 || ''} />
|
||||
<Input readOnly placeholder="Address line 2" value={line2 || ''} />
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input className="w-full" placeholder="Country" readOnly value={country || ''} />
|
||||
<Input
|
||||
className="w-full"
|
||||
placeholder="Postal code"
|
||||
readOnly
|
||||
value={postal_code || ''}
|
||||
/>
|
||||
</div>
|
||||
{city && <Input readOnly placeholder="City" value={city} />}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BillingAddress
|
||||
@@ -1,24 +1,17 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import {
|
||||
Typography,
|
||||
Badge,
|
||||
Button,
|
||||
IconTrash,
|
||||
IconCreditCard,
|
||||
Input,
|
||||
IconPlus,
|
||||
IconEdit2,
|
||||
IconLoader,
|
||||
} from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import ProjectSubscription from './ProjectSubscription'
|
||||
|
||||
import AWSMarketplaceSubscription from './AWSMarketplaceSubscription'
|
||||
import ProjectsSummary from './ProjectsSummary'
|
||||
import CreditBalance from './CreditBalance'
|
||||
import PaymentMethods from './PaymentMethods'
|
||||
import BillingAddress from './BillingAddress'
|
||||
import TaxID from './TaxID'
|
||||
|
||||
interface Props {
|
||||
organization: any
|
||||
@@ -35,7 +28,7 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
const [taxIds, setTaxIds] = useState<any>(null)
|
||||
const [paymentMethods, setPaymentMethods] = useState<any>(null)
|
||||
|
||||
const { stripe_customer_id, stripe_customer_object, name: orgName, slug: orgSlug } = organization
|
||||
const { stripe_customer_id, stripe_customer_object } = organization
|
||||
|
||||
const customerBalance = customer && customer.balance ? customer.balance / 100 : 0
|
||||
const isCredit = customerBalance < 0
|
||||
@@ -45,8 +38,6 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
? customerBalance.toString().replace('-', '')
|
||||
: customerBalance
|
||||
|
||||
const { city, country, line1, line2, postal_code, state } = stripe_customer_object?.address ?? {}
|
||||
|
||||
useEffect(() => {
|
||||
if (stripe_customer_id) {
|
||||
getStripeAccount()
|
||||
@@ -109,198 +100,27 @@ const BillingSettings: FC<Props> = ({ organization, projects = [] }) => {
|
||||
return (
|
||||
<article className="my-4 container max-w-4xl space-y-8">
|
||||
{organization.aws_marketplace ? (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<Typography.Text>
|
||||
This organization is subscribed via AWS Marketplace, with a limit of{' '}
|
||||
{organization.project_limit} projects and expiring on{' '}
|
||||
{organization.aws_marketplace.Entitlements[0].ExpirationDate.slice(0, 10)}.
|
||||
</Typography.Text>
|
||||
|
||||
<Button>
|
||||
<a
|
||||
href="https://aws.amazon.com/marketplace/saas/ordering?productId=dc498450-cecf-44c2-8c99-c3c13f16e70a&offerId=d1htt16t3ygx5brtwhooms0ei"
|
||||
target="_blank"
|
||||
>
|
||||
Manage AWS Marketplace subscription
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<AWSMarketplaceSubscription organization={organization} />
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12 lg:col-span-7">
|
||||
<Panel
|
||||
loading={loading}
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Typography.Title level={5}>Billing Information</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
These will be reflected in all invoices
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="outline"
|
||||
icon={<IconEdit2 />}
|
||||
onClick={() => redirectToPortal('/customer/update')}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="space-y-2">
|
||||
<Input readOnly layout="horizontal" label="Country" value={country || ''} />
|
||||
{city && <Input readOnly layout="horizontal" label="City" value={city} />}
|
||||
<Input readOnly layout="horizontal" label="Address" value={line1 || ''} />
|
||||
<Input readOnly layout="horizontal" label="" value={line2 || ''} />
|
||||
<Input readOnly layout="horizontal" label="" value={postal_code || ''} />
|
||||
</Panel.Content>
|
||||
<Panel.Content className="space-y-2">
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-4">
|
||||
<Typography.Text>Tax IDs</Typography.Text>
|
||||
</div>
|
||||
<div className="col-span-8">
|
||||
{!taxIds ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<Typography.Text>Retrieving tax information...</Typography.Text>
|
||||
</div>
|
||||
) : (taxIds?.data ?? []).length === 0 ? (
|
||||
<Typography.Text type="secondary">None</Typography.Text>
|
||||
) : (
|
||||
<>
|
||||
{taxIds.data.map((taxId: any) => (
|
||||
<Input readOnly value={taxId.value} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-5">
|
||||
<Panel
|
||||
className="!mb-4"
|
||||
loading={loading}
|
||||
title={<Typography.Title level={5}>Balance</Typography.Title>}
|
||||
>
|
||||
<Panel.Content>
|
||||
{isCredit && <Badge>You have credits available</Badge>}
|
||||
{isDebt && <Badge color="red">Outstanding payments</Badge>}
|
||||
|
||||
<Typography.Title level={2} className="mb-0">
|
||||
<Typography.Text type="secondary">
|
||||
<span className="text-lg font-normal">$</span>
|
||||
</Typography.Text>
|
||||
|
||||
<span>{balance}</span>
|
||||
<Typography.Text type="secondary">
|
||||
{isCredit && <span className="text-lg font-normal"> / credits</span>}
|
||||
</Typography.Text>
|
||||
</Typography.Title>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
<Panel
|
||||
loading={loading}
|
||||
title={
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<Typography.Title level={5}>Payment methods</Typography.Title>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="outline"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => redirectToPortal('/payment-methods')}
|
||||
>
|
||||
{(paymentMethods?.data ?? []).length >= 1 ? 'Add another' : 'Add new'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
{paymentMethods && paymentMethods.data.length >= 1 ? (
|
||||
<div className="space-y-1">
|
||||
{paymentMethods.data.map((paymentMethod: any) => {
|
||||
return (
|
||||
<div
|
||||
key={paymentMethod.id}
|
||||
className="rounded border border-border-secondary-light dark:border-border-secondary-dark p-3 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<img
|
||||
src={`/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Typography.Text>
|
||||
<span className="capitalize">{paymentMethod.card.brand}</span>{' '}
|
||||
ending in{' '}
|
||||
<span className="dark:text-white">{paymentMethod.card.last4}</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text>
|
||||
<span className="dark:text-white">
|
||||
{paymentMethod.card.exp_month}
|
||||
</span>
|
||||
/
|
||||
<span className="dark:text-white">
|
||||
{paymentMethod.card.exp_year}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconTrash />}
|
||||
onClick={() => redirectToPortal('/')}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 flex items-center rounded border border-border-secondary-light dark:border-border-secondary-dark border-dashed">
|
||||
<div className="flex items-center w-full justify-between space-x-2 hover:text-white hover:border-white">
|
||||
<Typography.Text className="flex items-center space-x-2">
|
||||
<div>
|
||||
<IconCreditCard />
|
||||
</div>
|
||||
<div>No payment methods</div>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
<div className="flex items-center w-full justify-between space-x-2 hover:text-white hover:border-white"></div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{projects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center px-16 py-12 border border-border-secondary-light dark:border-border-secondary-dark border-dashed rounded space-y-3">
|
||||
<Typography.Text className="block" type="secondary">
|
||||
You have no projects in organization {orgName} yet
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Link href={`/new/${orgSlug}`}>
|
||||
<Button type="outline">Create a project</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Typography.Text className="block mb-4">Projects in organization</Typography.Text>
|
||||
)}
|
||||
<div className="space-y-8">
|
||||
{projects.map((project) => {
|
||||
return <ProjectSubscription key={project.id} project={project} />
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<ProjectsSummary projects={projects} />
|
||||
<CreditBalance balance={balance} isCredit={isCredit} isDebt={isDebt} />
|
||||
<PaymentMethods
|
||||
loading={loading}
|
||||
paymentMethods={paymentMethods?.data ?? []}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
<BillingAddress
|
||||
loading={loading}
|
||||
address={stripe_customer_object?.address ?? {}}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
<TaxID
|
||||
loading={loading}
|
||||
taxIds={taxIds?.data ?? []}
|
||||
redirectToPortal={redirectToPortal}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { FC } from 'react'
|
||||
import { Badge } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
balance: string | number
|
||||
isCredit: boolean
|
||||
isDebt: boolean
|
||||
}
|
||||
|
||||
const CreditBalance: FC<Props> = ({ balance, isCredit, isDebt }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h4>Credit balance</h4>
|
||||
{isCredit && <Badge>You have credits available</Badge>}
|
||||
{isDebt && <Badge color="red">Outstanding payments</Badge>}
|
||||
</div>
|
||||
<p className="text-sm opacity-50">Charges will be deducted from your balance first</p>
|
||||
</div>
|
||||
<div className="flex items-end space-x-1">
|
||||
{isDebt && <h4 className="opacity-50">-</h4>}
|
||||
<h4 className="opacity-50">$</h4>
|
||||
<h2 className="text-4xl">{balance}</h2>
|
||||
{isCredit && <h4 className="opacity-50">/credits</h4>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreditBalance
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FC } from 'react'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import { IconPlus, IconEdit2, IconCreditCard, Button, Input, IconLoader } from '@supabase/ui'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
paymentMethods: any[]
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const PaymentMethods: FC<Props> = ({ loading, paymentMethods, redirectToPortal }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Payment methods</h4>
|
||||
<p className="text-sm opacity-50">Charges will be deducted from the default card</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
key="panel-footer"
|
||||
type="default"
|
||||
icon={<IconPlus />}
|
||||
onClick={() => redirectToPortal('/payment-methods')}
|
||||
>
|
||||
Add new card
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
{loading && paymentMethods.length === 0 ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p className="text-sm">Retrieving payment methods</p>
|
||||
</div>
|
||||
) : paymentMethods.length >= 1 ? (
|
||||
<div className="space-y-2">
|
||||
{paymentMethods.map((paymentMethod: any) => (
|
||||
<div key={paymentMethod.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<img
|
||||
src={`/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Input
|
||||
readOnly
|
||||
size="tiny"
|
||||
value={`•••• •••• •••• ${paymentMethod.card.last4}`}
|
||||
/>
|
||||
<p>
|
||||
{paymentMethod.card.exp_month}/{paymentMethod.card.exp_year}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="outline" icon={<IconEdit2 />} onClick={() => redirectToPortal('/')}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 opacity-50">
|
||||
<div>
|
||||
<IconCreditCard />
|
||||
</div>
|
||||
<div>No payment methods</div>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaymentMethods
|
||||
@@ -1,95 +0,0 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { get as _get, maxBy } from 'lodash'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { useStore, useSubscriptionStats } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { Dictionary } from '@supabase/grid'
|
||||
import { get, post } from 'lib/common/fetch'
|
||||
import { DATE_FORMAT, STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { Subscription, StripeSubscription, chargeableProducts } from 'components/interfaces/Billing'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
interface Props {
|
||||
project: any
|
||||
}
|
||||
|
||||
const ProjectSubscription: FC<Props> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
const subscriptionStats = useSubscriptionStats()
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
const [paygStats, setPaygStats] = useState<Dictionary<number>>()
|
||||
|
||||
useEffect(() => {
|
||||
getSubscription()
|
||||
}, [])
|
||||
|
||||
const getSubscription = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { data: subscription, error }: { data: StripeSubscription; error: any } = await post(
|
||||
`${API_URL}/stripe/subscription`,
|
||||
{
|
||||
subscription_id: project.subscription_id,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
setSubscription(subscription)
|
||||
|
||||
if (subscription.tier.prod_id === STRIPE_PRODUCT_IDS.PAYG) {
|
||||
fetchPaygStatistics()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project subscription: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPaygStatistics = async () => {
|
||||
const startDate = dayjs().utc().startOf('month').format(DATE_FORMAT)
|
||||
const endDate = dayjs().utc().endOf('month').format(DATE_FORMAT)
|
||||
const attributes =
|
||||
'total_db_size_bytes,total_db_egress_bytes,total_storage_size_bytes,total_storage_egress'
|
||||
const url = `${API_URL}/projects/${
|
||||
project.ref
|
||||
}/daily-stats?attribute=${attributes}&startDate=${encodeURIComponent(
|
||||
startDate
|
||||
)}&endDate=${encodeURIComponent(endDate)}&interval='1d'`
|
||||
|
||||
const { data } = await get(url)
|
||||
|
||||
const paygStats: any = {}
|
||||
chargeableProducts.forEach((product: any) => {
|
||||
product.features.forEach((feature: any) => {
|
||||
paygStats[feature.attribute] = _get(maxBy(data, feature.attribute), feature.attribute)
|
||||
})
|
||||
})
|
||||
setPaygStats(paygStats)
|
||||
}
|
||||
|
||||
return (
|
||||
<Subscription
|
||||
showProjectName
|
||||
loading={loading}
|
||||
project={project}
|
||||
// @ts-ignore
|
||||
subscription={subscription}
|
||||
paygStats={paygStats}
|
||||
subscriptionStats={subscriptionStats}
|
||||
// @ts-ignore
|
||||
currentPeriodStart={subscription?.billing.current_period_start}
|
||||
// @ts-ignore
|
||||
currentPeriodEnd={subscription?.billing.current_period_end}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectSubscription
|
||||
@@ -0,0 +1,116 @@
|
||||
import Link from 'next/link'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import { IconChevronRight, IconLoader } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
interface ProjectSummaryProps {
|
||||
project: any
|
||||
}
|
||||
|
||||
const ProjectSummary: FC<ProjectSummaryProps> = ({ project }) => {
|
||||
const { ui } = useStore()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [subscription, setSubscription] = useState<StripeSubscription>()
|
||||
|
||||
const currentPeriodStart = subscription?.billing?.current_period_start ?? 0
|
||||
const currentPeriodEnd = subscription?.billing?.current_period_end ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
getSubscription()
|
||||
}, [])
|
||||
|
||||
const getSubscription = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const { data: subscription, error }: { data: StripeSubscription; error: any } = await post(
|
||||
`${API_URL}/stripe/subscription`,
|
||||
{ subscription_id: project.subscription_id }
|
||||
)
|
||||
if (error) throw error
|
||||
setSubscription(subscription)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to get project subscription: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 py-3 flex items-center">
|
||||
<div className="w-[25%]">
|
||||
<p>{project.name}</p>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="w-[75%] flex items-center justify-center">
|
||||
<IconLoader size={14} className="animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-[20%]">
|
||||
<p>{subscription?.tier.name ?? ''}</p>
|
||||
</div>
|
||||
<div className="w-[40%] space-x-2 flex items-center">
|
||||
<p>{dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}</p>
|
||||
<p>-</p>
|
||||
<p>{dayjs.unix(currentPeriodEnd).utc().format('MMM D, YYYY')}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end w-[15%]">
|
||||
<Link href={`/project/${project.ref}/settings/billing`}>
|
||||
<a className="flex items-center space-x-2 group">
|
||||
<p className="transition group-hover:opacity-100 opacity-0 text-sm">View details</p>
|
||||
<IconChevronRight />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProjectsSummaryProps {
|
||||
projects: any
|
||||
}
|
||||
|
||||
const ProjectsSummary: FC<ProjectsSummaryProps> = ({ projects }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h4>Projects at a glance</h4>
|
||||
<Panel
|
||||
title={
|
||||
<div className="w-full flex items-center">
|
||||
<div className="w-[25%]">
|
||||
<p className="text-sm opacity-50">Name</p>
|
||||
</div>
|
||||
<div className="w-[20%]">
|
||||
<p className="text-sm opacity-50">Plan</p>
|
||||
</div>
|
||||
<div className="w-[40%]">
|
||||
<p className="text-sm opacity-50">Billing cycle</p>
|
||||
</div>
|
||||
<div className="w-[15%]" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{projects.map((project: any) => (
|
||||
<ProjectSummary key={project.ref} project={project} />
|
||||
))}
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsSummary
|
||||
@@ -0,0 +1,59 @@
|
||||
import { FC } from 'react'
|
||||
import { Input, Button, IconLoader } from '@supabase/ui'
|
||||
import Panel from 'components/to-be-cleaned/Panel'
|
||||
|
||||
interface Props {
|
||||
loading: boolean
|
||||
taxIds: any[]
|
||||
redirectToPortal: (url: string) => void
|
||||
}
|
||||
|
||||
const TaxID: FC<Props> = ({ loading, taxIds, redirectToPortal }) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4>Tax ID</h4>
|
||||
<p className="text-sm opacity-50">
|
||||
If you would like to include specific tax ID(s) to your invoices
|
||||
</p>
|
||||
</div>
|
||||
<Panel
|
||||
loading={loading}
|
||||
footer={
|
||||
<div>
|
||||
<Button type="default" onClick={() => redirectToPortal('/customer/update')}>
|
||||
Edit Tax IDs
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content className="w-3/5">
|
||||
{loading && taxIds.length === 0 ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
<p>Retrieving tax information</p>
|
||||
</div>
|
||||
) : taxIds.length >= 1 ? (
|
||||
<div className="w-full space-y-2">
|
||||
{taxIds.map((taxId: any) => {
|
||||
const taxIdType = taxId.type.replace('_', ' ').toUpperCase()
|
||||
return (
|
||||
<div key={taxId.id} className="flex items-center space-x-6">
|
||||
<p>{taxIdType}</p>
|
||||
<Input readOnly value={taxId.value} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 opacity-50">No tax IDs</p>
|
||||
</div>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaxID
|
||||
Reference in new issue
Block a user