mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 07:39:07 +03:00
feat: change payment method (subscription v2)
This commit is contained in:
1 parent
fb7a35fff8
commit
3668d37619
6 files changed
+227
-7
No files matched your search
@@ -11,7 +11,7 @@ const ProjectUpdateDisabledTooltip = ({
|
||||
}: PropsWithChildren<ProjectUpdateDisabledTooltipProps>) => {
|
||||
return (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
{projectUpdateDisabled ? (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { ProjectSubscriptionResponse } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import { Button, IconCreditCard, IconFileText, Modal } from 'ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { useState } from 'react'
|
||||
import PaymentMethodSelection from './PaymentMethodSelection'
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
SubscriptionTier,
|
||||
updateSubscriptionTier,
|
||||
} from 'data/subscriptions/project-subscription-update-mutation'
|
||||
import { useStore } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
|
||||
export interface SubscriptionTierProps {
|
||||
subscription: ProjectSubscriptionResponse
|
||||
}
|
||||
|
||||
const SubscriptionPaymentMethod = ({ subscription }: SubscriptionTierProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { ui } = useStore()
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showPaymentEditModal, setShowPaymentEditModal] = useState(false)
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(
|
||||
subscription.payment_method_id || ''
|
||||
)
|
||||
|
||||
const onConfirmPaymentMethod = async () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
if (selectedPaymentMethod) {
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
if (!selectedPaymentMethod) {
|
||||
return ui.setNotification({ category: 'error', message: 'Please select a payment method' })
|
||||
}
|
||||
|
||||
const selectedTier =
|
||||
subscription.plan.id === 'pro' && subscription.usage_billing_enabled
|
||||
? 'tier_payg'
|
||||
: `tier_${subscription.plan.id}`
|
||||
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
await updateSubscriptionTier({
|
||||
projectRef,
|
||||
tier: selectedTier as SubscriptionTier,
|
||||
paymentMethod: selectedPaymentMethod,
|
||||
})
|
||||
|
||||
setShowPaymentEditModal(false)
|
||||
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully updated payment method!`,
|
||||
})
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Unable to update subscription: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
if (subscription.payment_method_type === 'none') return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel>
|
||||
<Panel.Content className="flex justify-between lg:items-center flex-col md:flex-row space-y-6 md:space-y-0">
|
||||
{subscription.payment_method_type === 'card' && (
|
||||
<>
|
||||
<div className="flex space-x-3 items-center font-mono tracking-wide text-scale-1000">
|
||||
{subscription.payment_method_card_details?.brand ? (
|
||||
<img
|
||||
alt="Credit card brand"
|
||||
src={`${BASE_PATH}/img/payment-methods/${subscription.payment_method_card_details?.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
) : (
|
||||
<IconCreditCard />
|
||||
)}
|
||||
<span>
|
||||
**** **** ****{' '}
|
||||
{subscription?.payment_method_card_details?.last_4_digits || '****'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-3 text-scale-1000">
|
||||
<span>
|
||||
Expires {subscription?.payment_method_card_details?.expiry_month || '-'}/
|
||||
{subscription?.payment_method_card_details?.expiry_year?.toString()?.slice(-2) ||
|
||||
'-'}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{subscription?.payment_method_type === 'invoice' && (
|
||||
<>
|
||||
<div className="flex space-x-3">
|
||||
<div >
|
||||
<p className="text-scale-1100">Payment via invoice</p>
|
||||
<p className="text-sm text-scale-1000">You get a monthly invoice and payment link via email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={subscription?.payment_method_type !== 'card'}
|
||||
onClick={() => setShowPaymentEditModal(true)}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
{subscription?.payment_method_type !== 'card' && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
Please email support@supbase.io to change your payment method.
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
|
||||
<Modal
|
||||
loading={isSubmitting}
|
||||
alignFooter="right"
|
||||
className="!w-[450px]"
|
||||
visible={showPaymentEditModal}
|
||||
onCancel={() => setShowPaymentEditModal(false)}
|
||||
onConfirm={onConfirmPaymentMethod}
|
||||
overlayClassName="pointer-events-none"
|
||||
header={`Change payment method`}
|
||||
>
|
||||
<Modal.Content>
|
||||
<div className="py-6 space-y-2">
|
||||
<p className="text-sm">
|
||||
Upon clicking confirm, your next and future invoices will be deducted from the
|
||||
selected payment method. There are no immediate charges.
|
||||
</p>
|
||||
<div className="!mt-6">
|
||||
<PaymentMethodSelection
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={setSelectedPaymentMethod}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionPaymentMethod
|
||||
@@ -4,16 +4,19 @@ import SparkBar from 'components/ui/SparkBar'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
import { Alert, Button } from 'ui'
|
||||
import { Alert, Button, IconExternalLink } from 'ui'
|
||||
import TierUpdateSidePanel from './TierUpdateSidePanel'
|
||||
import Link from 'next/link'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import ProjectUpdateDisabledTooltip from '../../ProjectUpdateDisabledTooltip'
|
||||
import SubscriptionPaymentMethod from './SubscriptionPaymentMethod'
|
||||
|
||||
export interface SubscriptionTierProps {}
|
||||
|
||||
const SubscriptionTier = ({}: SubscriptionTierProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { ui } = useStore()
|
||||
const orgSlug = ui.selectedOrganization?.slug ?? ''
|
||||
const snap = useSubscriptionPageStateSnapshot()
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { data: subscription, isLoading } = useProjectSubscriptionV2Query({ projectRef })
|
||||
@@ -32,8 +35,33 @@ const SubscriptionTier = ({}: SubscriptionTierProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-12 gap-6" id="plan">
|
||||
<div className="col-span-12 lg:col-span-5">
|
||||
<p className="text-base sticky top-16">Subscription plan</p>
|
||||
<div className="col-span-12 lg:col-span-5 space-y-6">
|
||||
<div className="sticky space-y-6 top-16">
|
||||
<p className="text-base">Subscription plan</p>
|
||||
<div className="text-sm text-scale-1000">
|
||||
To manage your billing address, emails or tax IDs, head to your{' '}
|
||||
<Link href={`/org/${orgSlug}/billing`}>
|
||||
<a rel="noreferrer">
|
||||
<p className="text-sm text-green-900 transition hover:text-green-1000">
|
||||
organization settings
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-scale-1100">More information</p>
|
||||
<div>
|
||||
<Link href="https://supabase.com/pricing">
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
|
||||
<p className="text-sm">Pricing</p>
|
||||
<IconExternalLink size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="col-span-12 lg:col-span-7 space-y-2">
|
||||
@@ -87,7 +115,7 @@ const SubscriptionTier = ({}: SubscriptionTierProps) => {
|
||||
{!subscription?.usage_billing_enabled && (
|
||||
<Alert withIcon variant="info" title="This project is limited by the included usage">
|
||||
<p className="text-sm text-scale-1000">
|
||||
When this project exceeds its included usage quotas, it may become unresponsive.
|
||||
When this project exceeds its included usage quotas, it may become unresponsive.{' '}
|
||||
{currentPlan?.id === 'free'
|
||||
? 'If you wish to exceed the included usage, you should upgrade to a paid plan.'
|
||||
: 'You can change the Cost Control settings if you plan on exceeding the included usage quotas.'}
|
||||
@@ -106,6 +134,8 @@ const SubscriptionTier = ({}: SubscriptionTierProps) => {
|
||||
labelBottomClass="!text-scale-1000 pb-1"
|
||||
labelTop={`${daysToCycleEnd} Days left`}
|
||||
/>
|
||||
|
||||
{subscription && <SubscriptionPaymentMethod subscription={subscription} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -165,6 +165,7 @@ const PaymentMethods: FC<Props> = ({
|
||||
<div key={paymentMethod.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<img
|
||||
alt="Credit card brand"
|
||||
src={`${BASE_PATH}/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
|
||||
@@ -3,10 +3,12 @@ import { put } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { subscriptionKeys } from './keys'
|
||||
|
||||
export type SubscriptionTier = 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise'
|
||||
|
||||
export type ProjectSubscriptionUpdateVariables = {
|
||||
projectRef: string
|
||||
paymentMethod?: string
|
||||
tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team'
|
||||
tier: SubscriptionTier
|
||||
}
|
||||
|
||||
export type ProjectSubscriptionUpdateResponse = {
|
||||
|
||||
@@ -36,6 +36,14 @@ export type ProjectSubscriptionResponse = {
|
||||
freeUnits: number
|
||||
}
|
||||
}[]
|
||||
payment_method_id?: string
|
||||
payment_method_type: 'invoice' | 'card' | 'none'
|
||||
payment_method_card_details?: {
|
||||
last_4_digits: string
|
||||
brand: string
|
||||
expiry_month: number
|
||||
expiry_year: number
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectSubscription(
|
||||
|
||||
Reference in new issue
Block a user