Update payment summary panel and add cost breakdown modal

This commit is contained in:
Joshen Lim committed 2022-03-09 17:49:22 +08:00
1 parent d68193ceb5
commit 7feaaa50bf
5 files changed
+209 -46

No files matched your search

@@ -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[]
}
@@ -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<Props> = ({
visible,
totalDue,
billingDate,
costBreakdown,
onCancel,
}) => {
const groupedBreakdown = groupBy(costBreakdown, 'period.start')
return (
<Modal
hideFooter
visible={visible}
size="xlarge"
header="Cost breakdown details"
onCancel={onCancel}
>
<div className="py-4 space-y-2">
<Modal.Content>
<p className="text-sm">
The following shows the cost breakdown of the invoice that will be billed on{' '}
{billingDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
.
</p>
</Modal.Content>
<Modal.Content>
<div className="bg-scale-500 border border-scale-600 rounded">
<div className="flex items-center py-2 px-4 border-b border-scale-600">
<div className="text-xs w-[75%]">Description</div>
<div className="text-xs w-[10%] text-right">Qty</div>
<div className="text-xs w-[15%] text-right">Amount</div>
</div>
{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 (
<div className="border-b border-scale-600">
<div className="flex items-center py-2 px-4 ">
<div className="text-xs w-full text-scale-1100">
{start} - {end}
</div>
</div>
{groupedBreakdown[startPeriod].map((item: any) => {
return (
<div className="flex items-center py-2 px-4">
<div className="text-sm w-[75%]">{item.description}</div>
<div className="text-sm w-[10%] text-right">{item.quantity}</div>
<div className="text-sm w-[15%] text-right">
${(item.amount / 100).toFixed(2)}
</div>
</div>
)
})}
</div>
)
})}
<div className="flex items-center py-2 px-4">
<div className="text-sm w-[75%]">Total amount</div>
<div className="text-sm w-[10%] text-right"></div>
<div className="text-xl w-[15%] text-right">${totalDue.toFixed(2)}</div>
</div>
</div>
</Modal.Content>
</div>
</Modal>
)
}
export default CostBreakdownModal
@@ -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<Props> = ({
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 && (
<>
<Loading active={isRefreshingPreview}>
<div className="space-y-1">
<div className="flex items-center justify-between">
<p className="text-sm">Total amount due</p>
<p className="text-sm">
$
{((subscriptionPreview?.base_amount_due_next_billing_cycle ?? 0) / 100).toFixed(
2
)}
</p>
</div>
<div className="flex items-center justify-between">
<p className="text-sm">Available credit balance</p>
<div className="space-y-2">
<div className="flex items-start justify-between space-x-20">
<div>
<p className="text-sm">Available credit balance</p>
<p className="text-sm text-scale-1100">
Including credits returned for unused time from this change
</p>
</div>
<p className="text-sm">${availableCreditBalance.toFixed(2)}</p>
</div>
</div>
</Loading>
<div className="h-px w-full bg-scale-600" />
</>
)}
)} */}
{/* Payment total */}
<Loading active={isRefreshingPreview}>
<div className="mb-2">
<div className="flex items-start justify-between">
<p>Amount due today</p>
<div className="flex justify-end items-end relative -top-[5px]">
<div className="mb-2 space-y-4">
<div className="flex items-start justify-between space-x-12">
<div className="">
<div className="flex items-center space-x-2">
<p>Total amount due</p>
{hasChangesToPlan && (
<IconHelpCircle
size={16}
strokeWidth={1.5}
className="cursor-pointer opacity-50 hover:opacity-100 transition"
onClick={() => setShowCostBreakdown(true)}
/>
)}
</div>
{hasChanges ? (
<div className="space-y-2">
<p className="text-sm text-scale-1100">
This amount {!isSpendCapEnabled && !isBillingToday && '+ usage fees '}will be
charged on{' '}
<span className="text-scale-1200">
{billingDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
.
</p>
<p className="text-sm text-scale-1100">
You'll pay a monthly total of{' '}
<span className="text-scale-1200">
${totalMonthlyCost} {!isSpendCapEnabled && '+ usage fees '}
</span>{' '}
for each subsequent billing cycle.
</p>
</div>
) : (
<p className="text-sm text-scale-1100">No changes to current subscription</p>
)}
</div>
<div className="flex justify-end items-end relative -top-[8px]">
<p className="text-scale-1100 relative -top-[1px]">$</p>
<p className="text-2xl">
{!hasChangesToPlan ? '0.00' : amountDueImmediately.toFixed(2)}
{!hasChangesToPlan
? '0.00'
: (amountDueImmediately < 0 ? 0 : amountDueImmediately).toFixed(2)}
</p>
</div>
</div>
{hasCreditsBalance && (
{/* {hasCreditsBalance && (
<div className="flex items-start justify-between">
<p className="text-sm">Remaining credit balance</p>
<p className="text-sm">${remainingCreditBalance.toFixed(2)}</p>
</div>
)}
)} */}
</div>
{hasChangesToPlan && (
<div className="w-2/3">
<p className="text-sm text-scale-1100">
You'll pay a monthly total of{' '}
<span className="text-scale-1200">
${totalMonthlyCost} {!isSpendCapEnabled && '+ usage fees '}
</span>
starting on{' '}
{nextInvoiceDate.toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</p>
</div>
)}
</Loading>
<CostBreakdownModal
visible={showCostBreakdown}
totalDue={amountDueImmediately}
billingDate={billingDate}
costBreakdown={subscriptionPreview?.cost_breakdown || []}
onCancel={() => setShowCostBreakdown(false)}
/>
</>
)
}
@@ -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<Props> = ({
(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)'
@@ -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 (
<BillingLayout>