feat: change payment method (subscription v2)

This commit is contained in:
Kevin Grüneberg committed 2023-06-06 11:46:30 +02:00
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">
@@ -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(