From 7feaaa50bf9d8ceab7aa3e8f16ac253f59176bb8 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 9 Mar 2022 17:49:22 +0800 Subject: [PATCH] Update payment summary panel and add cost breakdown modal --- .../interfaces/Billing/Billing.types.ts | 7 +- .../CostBreakdownModal.tsx | 95 ++++++++++++++ .../PaymentSummaryPanel/PaymentTotal.tsx | 123 ++++++++++++------ .../Billing/PaymentSummaryPanel/index.tsx | 4 +- .../[ref]/settings/billing/update/free.tsx | 26 +++- 5 files changed, 209 insertions(+), 46 deletions(-) create mode 100644 studio/components/interfaces/Billing/PaymentSummaryPanel/CostBreakdownModal.tsx diff --git a/studio/components/interfaces/Billing/Billing.types.ts b/studio/components/interfaces/Billing/Billing.types.ts index 9d37321db29..cdc49b7f53d 100644 --- a/studio/components/interfaces/Billing/Billing.types.ts +++ b/studio/components/interfaces/Billing/Billing.types.ts @@ -12,12 +12,15 @@ export interface SubscriptionPreview { returned_credits: number // Amount due for each subsequent month base_amount_due_next_billing_cycle: number - // Date for next invoice (Billing cycle resets for every change to a subscription) - next_billing_cycle: number + // Date for which bill will be enacted + bill_on: number // Remaining number of days to end of current billing cycle // This is meant for FE to display the refund specifics remaining_days_in_current_billing_cycle: number // Total days in current billing cycle // This is meant for FE to display the refund specifics total_days_in_current_billing_cycle: number + + has_changes: boolean + cost_breakdown: any[] } diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/CostBreakdownModal.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/CostBreakdownModal.tsx new file mode 100644 index 00000000000..4435ea63823 --- /dev/null +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/CostBreakdownModal.tsx @@ -0,0 +1,95 @@ +import { FC } from 'react' +import { groupBy } from 'lodash' +import { Modal } from '@supabase/ui' + +interface Props { + visible: boolean + totalDue: number + billingDate: Date + costBreakdown: any[] + onCancel: () => void +} + +const CostBreakdownModal: FC = ({ + visible, + totalDue, + billingDate, + costBreakdown, + onCancel, +}) => { + const groupedBreakdown = groupBy(costBreakdown, 'period.start') + + return ( + +
+ +

+ The following shows the cost breakdown of the invoice that will be billed on{' '} + {billingDate.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + . +

+
+ +
+
+
Description
+
Qty
+
Amount
+
+ {Object.keys(groupedBreakdown).map((startPeriod: any) => { + const start = new Date(Number(startPeriod) * 1000).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + const end = new Date( + groupedBreakdown[startPeriod][0].period.end * 1000 + ).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + return ( +
+
+
+ {start} - {end} +
+
+ {groupedBreakdown[startPeriod].map((item: any) => { + return ( +
+
{item.description}
+
{item.quantity}
+
+ ${(item.amount / 100).toFixed(2)} +
+
+ ) + })} +
+ ) + })} +
+
Total amount
+
+
${totalDue.toFixed(2)}
+
+
+
+
+
+ ) +} + +export default CostBreakdownModal diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentTotal.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentTotal.tsx index 723197ca741..212aba6b7bf 100644 --- a/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentTotal.tsx +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/PaymentTotal.tsx @@ -1,6 +1,7 @@ -import { FC } from 'react' -import { Loading } from '@supabase/ui' +import { FC, useState } from 'react' +import { Loading, IconHelpCircle, Modal } from '@supabase/ui' import { SubscriptionPreview } from '../Billing.types' +import CostBreakdownModal from './CostBreakdownModal' interface Props { subscriptionPreview?: SubscriptionPreview @@ -15,76 +16,118 @@ const PaymentTotal: FC = ({ hasChangesToPlan, isSpendCapEnabled, }) => { + const hasChanges = subscriptionPreview?.has_changes ?? false const hasCreditsBalance = (subscriptionPreview?.available_credit_balance ?? 0) > 0 const availableCreditBalance = (subscriptionPreview?.available_credit_balance ?? 0) / 100 + const returnedCredits = (subscriptionPreview?.returned_credits ?? 0) / 100 const remainingCreditBalance = (subscriptionPreview?.remaining_credit_balance ?? 0) / 100 const totalMonthlyCost = (subscriptionPreview?.base_amount_due_next_billing_cycle ?? 0) / 100 const amountDueImmediately = (subscriptionPreview?.amount_due_immediately ?? 0) / 100 - const nextInvoiceDate = new Date((subscriptionPreview?.next_billing_cycle ?? 0) * 1000) + const billingDate = new Date((subscriptionPreview?.bill_on ?? 0) * 1000) + const isBillingToday = + new Date().toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) === + billingDate.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + + const [showCostBreakdown, setShowCostBreakdown] = useState(false) return ( <> {/* Credit balance details (If available) */} - {hasCreditsBalance && ( + {/* {hasCreditsBalance && ( <> -
-
-

Total amount due

-

- $ - {((subscriptionPreview?.base_amount_due_next_billing_cycle ?? 0) / 100).toFixed( - 2 - )} -

-
-
-

Available credit balance

+
+
+
+

Available credit balance

+

+ Including credits returned for unused time from this change +

+

${availableCreditBalance.toFixed(2)}

- )} + )} */} {/* Payment total */} -
-
-

Amount due today

-
+
+
+
+
+

Total amount due

+ {hasChangesToPlan && ( + setShowCostBreakdown(true)} + /> + )} +
+ {hasChanges ? ( +
+

+ This amount {!isSpendCapEnabled && !isBillingToday && '+ usage fees '}will be + charged on{' '} + + {billingDate.toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + + . +

+

+ You'll pay a monthly total of{' '} + + ${totalMonthlyCost} {!isSpendCapEnabled && '+ usage fees '} + {' '} + for each subsequent billing cycle. +

+
+ ) : ( +

No changes to current subscription

+ )} +
+

$

- {!hasChangesToPlan ? '0.00' : amountDueImmediately.toFixed(2)} + {!hasChangesToPlan + ? '0.00' + : (amountDueImmediately < 0 ? 0 : amountDueImmediately).toFixed(2)}

- {hasCreditsBalance && ( + {/* {hasCreditsBalance && (

Remaining credit balance

${remainingCreditBalance.toFixed(2)}

- )} + )} */}
- {hasChangesToPlan && ( -
-

- You'll pay a monthly total of{' '} - - ${totalMonthlyCost} {!isSpendCapEnabled && '+ usage fees '} - - starting on{' '} - {nextInvoiceDate.toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - })} -

-
- )} + + setShowCostBreakdown(false)} + /> ) } diff --git a/studio/components/interfaces/Billing/PaymentSummaryPanel/index.tsx b/studio/components/interfaces/Billing/PaymentSummaryPanel/index.tsx index c37bd0996a8..0ebfcb2d373 100644 --- a/studio/components/interfaces/Billing/PaymentSummaryPanel/index.tsx +++ b/studio/components/interfaces/Billing/PaymentSummaryPanel/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react' -import { Listbox, IconLoader, Button, Loading, IconPlus } from '@supabase/ui' +import { Listbox, IconLoader, Button, IconPlus } from '@supabase/ui' import { STRIPE_PRODUCT_IDS } from 'lib/constants' import { StripeProduct } from '..' @@ -51,7 +51,7 @@ const PaymentSummaryPanel: FC = ({ (currentPlan.prod_id !== STRIPE_PRODUCT_IDS.PAYG && !isSpendCapEnabled) || (currentPlan.prod_id === STRIPE_PRODUCT_IDS.PAYG && isSpendCapEnabled) const isChangingComputeSize = currentComputeSize.id !== selectedComputeSize.id - const hasChangesToPlan = isChangingPlan || isChangingComputeSize + const hasChangesToPlan = subscriptionPreview?.has_changes ?? false const getCurrentPlanName = () => { if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.PAYG) return 'Pro tier (No spend caps)' diff --git a/studio/pages/project/[ref]/settings/billing/update/free.tsx b/studio/pages/project/[ref]/settings/billing/update/free.tsx index 3c4feea7389..30147ccf6db 100644 --- a/studio/pages/project/[ref]/settings/billing/update/free.tsx +++ b/studio/pages/project/[ref]/settings/billing/update/free.tsx @@ -1,19 +1,41 @@ +import { useState, useEffect } from 'react' import { NextPage } from 'next' import { observer } from 'mobx-react-lite' import { useRouter } from 'next/router' import { withAuth, useStore } from 'hooks' +import { get } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' import { BillingLayout } from 'components/layouts' import { ExitSurvey } from 'components/interfaces/Billing' -import { useEffect } from 'react' const BillingUpdateFree: NextPage = () => { const { ui } = useStore() const router = useRouter() const projectRef = ui.selectedProject?.ref - // [TODO] If project is already free, redirect back to plan selection and show push notification + const [products, setProducts] = useState<{ tiers: any[]; addons: any[] }>() + const [isLoadingProducts, setIsLoadingProducts] = useState(false) + + useEffect(() => { + getStripeProducts() + }, []) + + const getStripeProducts = async () => { + try { + setIsLoadingProducts(true) + const products = await get(`${API_URL}/stripe/products`) + setProducts(products) + setIsLoadingProducts(false) + } catch (error: any) { + ui.setNotification({ + error, + category: 'error', + message: `Failed to get products: ${error.message}`, + }) + } + } return (