From fe80274b28c0ce769379d42f2f802bcdbbf89cb9 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 24 Feb 2022 17:22:17 +0800 Subject: [PATCH] Update org billing page with latest design --- .../AWSMarketplaceSubscription.tsx | 29 +++ .../BillingSettings/BillingAddress.tsx | 49 ++++ .../BillingSettings/BillingSettings.tsx | 234 ++---------------- .../BillingSettings/CreditBalance.tsx | 31 +++ .../BillingSettings/PaymentMethods.tsx | 81 ++++++ .../BillingSettings/ProjectSubscription.tsx | 95 ------- .../BillingSettings/ProjectsSummary.tsx | 116 +++++++++ .../Organization/BillingSettings/TaxID.tsx | 59 +++++ 8 files changed, 392 insertions(+), 302 deletions(-) create mode 100644 studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx create mode 100644 studio/components/interfaces/Organization/BillingSettings/BillingAddress.tsx create mode 100644 studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx create mode 100644 studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx delete mode 100644 studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx create mode 100644 studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx create mode 100644 studio/components/interfaces/Organization/BillingSettings/TaxID.tsx diff --git a/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx b/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx new file mode 100644 index 00000000000..5a7acdac437 --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/AWSMarketplaceSubscription.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' +import { Button, Typography } from '@supabase/ui' + +interface Props { + organization: any +} + +const AWSMarketplaceSubscription: FC = ({ organization }) => { + return ( +
+ + 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)}. + + + +
+ ) +} + +export default AWSMarketplaceSubscription diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingAddress.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingAddress.tsx new file mode 100644 index 00000000000..f8f5df52373 --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/BillingAddress.tsx @@ -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 + redirectToPortal: (url: string) => void +} + +const BillingAddress: FC = ({ loading, address, redirectToPortal }) => { + const { city, country, line1, line2, postal_code, state } = address + return ( +
+
+

Billing address

+

This will be reflected in every invoice

+
+ + +
+ } + > + + + +
+ + +
+ {city && } +
+ + + ) +} + +export default BillingAddress diff --git a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index ef35b1fd14d..0fe97138558 100644 --- a/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -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 = ({ organization, projects = [] }) => { const [taxIds, setTaxIds] = useState(null) const [paymentMethods, setPaymentMethods] = useState(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 = ({ 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 = ({ organization, projects = [] }) => { return (
{organization.aws_marketplace ? ( -
- - 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)}. - - - -
+ ) : ( <> -
-
- -
- Billing Information - - These will be reflected in all invoices - -
- -
- } - > - - - {city && } - - - - - -
-
- Tax IDs -
-
- {!taxIds ? ( -
- - Retrieving tax information... -
- ) : (taxIds?.data ?? []).length === 0 ? ( - None - ) : ( - <> - {taxIds.data.map((taxId: any) => ( - - ))} - - )} -
-
-
- -
-
- Balance} - > - - {isCredit && You have credits available} - {isDebt && Outstanding payments} - - - - $ - - - {balance} - - {isCredit && / credits} - - - - - - Payment methods - -
- } - > - - {paymentMethods && paymentMethods.data.length >= 1 ? ( -
- {paymentMethods.data.map((paymentMethod: any) => { - return ( -
-
- - - {paymentMethod.card.brand}{' '} - ending in{' '} - {paymentMethod.card.last4} - -
-
- - - {paymentMethod.card.exp_month} - - / - - {paymentMethod.card.exp_year} - - -
- -
- ) - })} -
- ) : ( -
-
- -
- -
-
No payment methods
-
-
-
- )} -
-
- - - -
- {projects.length === 0 ? ( -
- - You have no projects in organization {orgName} yet - -
- - - -
-
- ) : ( - Projects in organization - )} -
- {projects.map((project) => { - return - })} -
+
+ + + + +
)} diff --git a/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx new file mode 100644 index 00000000000..a973f47ad6c --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx @@ -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 = ({ balance, isCredit, isDebt }) => { + return ( +
+
+
+

Credit balance

+ {isCredit && You have credits available} + {isDebt && Outstanding payments} +
+

Charges will be deducted from your balance first

+
+
+ {isDebt &&

-

} +

$

+

{balance}

+ {isCredit &&

/credits

} +
+
+ ) +} + +export default CreditBalance diff --git a/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx new file mode 100644 index 00000000000..8c45771c0e8 --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/PaymentMethods.tsx @@ -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 = ({ loading, paymentMethods, redirectToPortal }) => { + return ( +
+
+

Payment methods

+

Charges will be deducted from the default card

+
+ + +
+ } + > + + {loading && paymentMethods.length === 0 ? ( +
+ +

Retrieving payment methods

+
+ ) : paymentMethods.length >= 1 ? ( +
+ {paymentMethods.map((paymentMethod: any) => ( +
+
+ + +

+ {paymentMethod.card.exp_month}/{paymentMethod.card.exp_year} +

+
+ +
+ ))} +
+ ) : ( +
+

+

+ +
+
No payment methods
+

+
+ )} +
+ +
+ ) +} + +export default PaymentMethods diff --git a/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx b/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx deleted file mode 100644 index c6c7d604b37..00000000000 --- a/studio/components/interfaces/Organization/BillingSettings/ProjectSubscription.tsx +++ /dev/null @@ -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 = ({ project }) => { - const { ui } = useStore() - const subscriptionStats = useSubscriptionStats() - - const [loading, setLoading] = useState(true) - const [subscription, setSubscription] = useState() - const [paygStats, setPaygStats] = useState>() - - 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 ( - - ) -} - -export default ProjectSubscription diff --git a/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx b/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx new file mode 100644 index 00000000000..654db7fc69a --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/ProjectsSummary.tsx @@ -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 = ({ project }) => { + const { ui } = useStore() + + const [loading, setLoading] = useState(false) + const [subscription, setSubscription] = useState() + + 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 ( +
+
+

{project.name}

+
+ {loading ? ( +
+ +
+ ) : ( + <> +
+

{subscription?.tier.name ?? ''}

+
+
+

{dayjs.unix(currentPeriodStart).utc().format('MMM D, YYYY')}

+

-

+

{dayjs.unix(currentPeriodEnd).utc().format('MMM D, YYYY')}

+
+ + + )} +
+ ) +} + +interface ProjectsSummaryProps { + projects: any +} + +const ProjectsSummary: FC = ({ projects }) => { + return ( +
+

Projects at a glance

+ +
+

Name

+
+
+

Plan

+
+
+

Billing cycle

+
+
+
+ } + > + {projects.map((project: any) => ( + + ))} +
+
+ ) +} + +export default ProjectsSummary diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID.tsx b/studio/components/interfaces/Organization/BillingSettings/TaxID.tsx new file mode 100644 index 00000000000..2cf367bc91d --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID.tsx @@ -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 = ({ loading, taxIds, redirectToPortal }) => { + return ( +
+
+

Tax ID

+

+ If you would like to include specific tax ID(s) to your invoices +

+
+ + +
+ } + > + + {loading && taxIds.length === 0 ? ( +
+ +

Retrieving tax information

+
+ ) : taxIds.length >= 1 ? ( +
+ {taxIds.map((taxId: any) => { + const taxIdType = taxId.type.replace('_', ' ').toUpperCase() + return ( +
+

{taxIdType}

+ +
+ ) + })} +
+ ) : ( +
+

No tax IDs

+
+ )} +
+ + + ) +} + +export default TaxID