mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
Update payment summary panel and add cost breakdown modal
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user