mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 13:19:09 +03:00
Merge pull request #14659 from supabase/poc/org-level-billing
This commit is contained in:
10 files changed
+535
-9
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
import { Badge, Button, IconPlus } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
@@ -28,7 +28,7 @@ const ProjectList: FC<Props> = ({ rewriteHref }) => {
|
||||
return (
|
||||
<>
|
||||
{organizations.list().map((org: Organization) => {
|
||||
const { id, name, slug } = org
|
||||
const { id, name, slug, subscription_id } = org
|
||||
const sortedProjects = projects.list(
|
||||
({ organization_id }: Project) => organization_id == id
|
||||
)
|
||||
@@ -40,7 +40,7 @@ const ProjectList: FC<Props> = ({ rewriteHref }) => {
|
||||
return (
|
||||
<div className="space-y-3" key={makeRandomString(5)}>
|
||||
<div className="flex space-x-4 items-center">
|
||||
<h4 className="text-lg">{name}</h4>
|
||||
<h4 className="text-lg flex items-center">{name}{subscription_id ? <Badge className='ml-3'>V2</Badge> : <></>}</h4>
|
||||
|
||||
{!!overdueInvoices.length && (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, IconEdit2, IconInfo, Input, Listbox } from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { API_URL, BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useElements, useStripe, PaymentElement } from '@stripe/react-stripe-js'
|
||||
import { PaymentMethod } from 'components/interfaces/Billing/Billing.types'
|
||||
import { getURL } from 'lib/helpers'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
const ORG_KIND_TYPES = {
|
||||
PERSONAL: 'Personal',
|
||||
EDUCATIONAL: 'Educational',
|
||||
STARTUP: 'Startup',
|
||||
AGENCY: 'Agency',
|
||||
COMPANY: 'Company',
|
||||
UNDISCLOSED: 'N/A',
|
||||
}
|
||||
const ORG_KIND_DEFAULT = 'PERSONAL'
|
||||
|
||||
const ORG_SIZE_TYPES = {
|
||||
'1': '1 - 10',
|
||||
'10': '10 - 49',
|
||||
'50': '50 - 99',
|
||||
'100': '100 - 299',
|
||||
'300': 'More than 300',
|
||||
}
|
||||
const ORG_SIZE_DEFAULT = '1'
|
||||
|
||||
interface NewOrgFormProps {
|
||||
onPaymentMethodReset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* No org selected yet, create a new one
|
||||
*/
|
||||
const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => {
|
||||
const { ui, app } = useStore()
|
||||
const router = useRouter()
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
|
||||
const [orgName, setOrgName] = useState('')
|
||||
const [orgKind, setOrgKind] = useState(ORG_KIND_DEFAULT)
|
||||
const [orgSize, setOrgSize] = useState(ORG_SIZE_DEFAULT)
|
||||
const [newOrgLoading, setNewOrgLoading] = useState(false)
|
||||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>()
|
||||
|
||||
const [dbPricingTierKey, setDbPricingTierKey] = useState('PRO')
|
||||
|
||||
function validateOrgName(name: any) {
|
||||
const value = name ? name.trim() : ''
|
||||
return value.length >= 1
|
||||
}
|
||||
|
||||
function onOrgNameChange(e: any) {
|
||||
setOrgName(e.target.value)
|
||||
}
|
||||
|
||||
function onOrgKindChange(value: any) {
|
||||
setOrgKind(value)
|
||||
}
|
||||
|
||||
function onOrgSizeChange(value: any) {
|
||||
setOrgSize(value)
|
||||
}
|
||||
|
||||
function onDbPricingPlanChange(value: string) {
|
||||
setDbPricingTierKey(value)
|
||||
}
|
||||
|
||||
async function createOrg(paymentMethodId: string) {
|
||||
const response = await post(
|
||||
`${API_URL}/organizations`,
|
||||
{
|
||||
name: orgName,
|
||||
kind: orgKind,
|
||||
payment_method: paymentMethodId,
|
||||
tier: 'tier_' + dbPricingTierKey.toLowerCase(),
|
||||
...(orgKind == 'COMPANY' ? { size: orgSize } : {}),
|
||||
},
|
||||
// Call new V2 endpoint from API
|
||||
{
|
||||
headers: {
|
||||
Version: '2',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to create organization: ${response.error?.message ?? response.error}`,
|
||||
})
|
||||
} else {
|
||||
const org = response
|
||||
app.onOrgAdded(org)
|
||||
router.push(`/new/${org.slug}`)
|
||||
}
|
||||
setNewOrgLoading(false)
|
||||
}
|
||||
|
||||
const handleSubmit = async (event: any) => {
|
||||
event.preventDefault()
|
||||
|
||||
const isOrgNameValid = validateOrgName(orgName)
|
||||
if (!isOrgNameValid) {
|
||||
ui.setNotification({ category: 'error', message: 'Organization name is empty' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!stripe || !elements) {
|
||||
console.error('Stripe.js has not loaded')
|
||||
return
|
||||
}
|
||||
setNewOrgLoading(true)
|
||||
|
||||
if (!paymentMethod) {
|
||||
const { error, setupIntent } = await stripe.confirmSetup({
|
||||
elements,
|
||||
redirect: 'if_required',
|
||||
confirmParams: {
|
||||
return_url: `${getURL()}/new-with-subscription`,
|
||||
expand: ['payment_method'],
|
||||
},
|
||||
})
|
||||
|
||||
if (error || !setupIntent.payment_method) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: error?.message ?? ' Failed to save card details',
|
||||
})
|
||||
setNewOrgLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const paymentMethodFromSetup = setupIntent.payment_method as PaymentMethod
|
||||
|
||||
setPaymentMethod(paymentMethodFromSetup)
|
||||
await createOrg(paymentMethodFromSetup.id)
|
||||
} else {
|
||||
await createOrg(paymentMethod.id)
|
||||
}
|
||||
}
|
||||
|
||||
const resetPaymentMethod = () => {
|
||||
setPaymentMethod(undefined)
|
||||
return onPaymentMethodReset()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Panel
|
||||
hideHeaderStyling
|
||||
title={
|
||||
<div key="panel-title">
|
||||
<h4>Create a new organization</h4>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div key="panel-footer" className="flex w-full items-center justify-between">
|
||||
<Button type="default" onClick={() => router.push('/projects')}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex items-center space-x-3">
|
||||
<p className="text-xs text-scale-900">You can rename your organization later</p>
|
||||
<Button
|
||||
block
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
loading={newOrgLoading}
|
||||
disabled={newOrgLoading}
|
||||
>
|
||||
Create organization
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Panel.Content>
|
||||
<InformationBox
|
||||
icon={<IconInfo size="large" strokeWidth={1.5} />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse
|
||||
title="Billed via organization"
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
This is heavy Work-In-Progress and not customer facing yet, use with caution!
|
||||
This organization will use the new org level billing, instead of having
|
||||
individual subscriptions per project. There are still a lot of open ends that
|
||||
may be restrictive for you, follow #team-billing for updates.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Panel.Content>
|
||||
|
||||
<Panel.Content className="pt-0">
|
||||
<p className="text-sm">This is your organization within Supabase.</p>
|
||||
<p className="text-sm text-scale-1100">
|
||||
For example, you can use the name of your company or department.
|
||||
</p>
|
||||
</Panel.Content>
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered">
|
||||
<Input
|
||||
autoFocus
|
||||
label="Name"
|
||||
type="text"
|
||||
layout="horizontal"
|
||||
placeholder="Organization name"
|
||||
descriptionText="What's the name of your company or team?"
|
||||
value={orgName}
|
||||
onChange={onOrgNameChange}
|
||||
/>
|
||||
</Panel.Content>
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered">
|
||||
<Listbox
|
||||
label="Type of organization"
|
||||
layout="horizontal"
|
||||
value={orgKind}
|
||||
onChange={onOrgKindChange}
|
||||
descriptionText="What would best describe your organization?"
|
||||
>
|
||||
{Object.entries(ORG_KIND_TYPES).map(([k, v]) => {
|
||||
return (
|
||||
<Listbox.Option key={k} label={v} value={k}>
|
||||
{v}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
</Panel.Content>
|
||||
|
||||
{orgKind == 'COMPANY' ? (
|
||||
<Panel.Content className="Form section-block--body has-inputs-centered">
|
||||
<Listbox
|
||||
label="Company size"
|
||||
layout="horizontal"
|
||||
value={orgSize}
|
||||
onChange={onOrgSizeChange}
|
||||
descriptionText="How many people are in your company?"
|
||||
>
|
||||
{Object.entries(ORG_SIZE_TYPES).map(([k, v]) => {
|
||||
return (
|
||||
<Listbox.Option key={k} label={v} value={k}>
|
||||
{v}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
</Panel.Content>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<Panel.Content>
|
||||
<Listbox
|
||||
label="Pricing Plan"
|
||||
layout="horizontal"
|
||||
value={dbPricingTierKey}
|
||||
// @ts-ignore
|
||||
onChange={onDbPricingPlanChange}
|
||||
// @ts-ignore
|
||||
descriptionText={
|
||||
<>
|
||||
Select a plan that suits your needs.
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://supabase.com/pricing"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{Object.entries(PRICING_TIER_LABELS_ORG).map(([k, v]) => {
|
||||
return (
|
||||
<Listbox.Option key={k} label={v} value={k}>
|
||||
{v}
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
</Listbox>
|
||||
</Panel.Content>
|
||||
|
||||
<Panel.Content>
|
||||
{paymentMethod ? (
|
||||
<div key={paymentMethod.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-8">
|
||||
<img
|
||||
alt="Card"
|
||||
src={`${BASE_PATH}/img/payment-methods/${paymentMethod.card.brand
|
||||
.replace(' ', '-')
|
||||
.toLowerCase()}.png`}
|
||||
width="32"
|
||||
/>
|
||||
<Input
|
||||
readOnly
|
||||
className="w-64"
|
||||
size="small"
|
||||
value={`•••• •••• •••• ${paymentMethod.card.last4}`}
|
||||
/>
|
||||
<p className="text-sm tabular-nums">
|
||||
Expires: {paymentMethod.card.exp_month}/{paymentMethod.card.exp_year}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<IconEdit2 />}
|
||||
onClick={() => resetPaymentMethod()}
|
||||
disabled={newOrgLoading}
|
||||
className="hover:border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PaymentElement />
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewOrgForm
|
||||
@@ -2,5 +2,6 @@ import GeneralSettings from './GeneralSettings/GeneralSettings'
|
||||
import TeamSettings from './TeamSettings/TeamSettings'
|
||||
import BillingSettings from './BillingSettings/BillingSettings'
|
||||
import InvoicesSettings from './InvoicesSettings/InvoicesSettings'
|
||||
import NewOrgForm from './NewOrg/NewOrgForm'
|
||||
|
||||
export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings }
|
||||
export { GeneralSettings, TeamSettings, BillingSettings, InvoicesSettings, NewOrgForm }
|
||||
@@ -37,7 +37,7 @@ const AccountLayout: FC<Props> = ({ children, title, breadcrumbs }) => {
|
||||
.map((organization) => ({
|
||||
isActive:
|
||||
router.pathname.startsWith('/org/') && ui.selectedOrganization?.slug === organization.slug,
|
||||
label: organization.name,
|
||||
label: organization.name + (organization.subscription_id ? ' (V2)' : ''),
|
||||
href: `/org/${organization.slug}/general`,
|
||||
key: organization.slug,
|
||||
}))
|
||||
|
||||
@@ -3,7 +3,7 @@ import { toJS } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Dropdown, IconPlus } from 'ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
|
||||
const OrgDropdown = () => {
|
||||
@@ -13,6 +13,8 @@ const OrgDropdown = () => {
|
||||
const sortedOrganizations: any[] = app.organizations.list()
|
||||
const selectedOrganization: any = ui.selectedOrganization
|
||||
|
||||
const orgCreationV2 = useFlag('orgcreationv2')
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
@@ -54,6 +56,14 @@ const OrgDropdown = () => {
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />} onClick={() => router.push(`/new`)}>
|
||||
New organization
|
||||
</Dropdown.Item>
|
||||
{orgCreationV2 && (
|
||||
<Dropdown.Item
|
||||
icon={<IconPlus size="tiny" />}
|
||||
onClick={() => router.push(`/new-with-subscription`)}
|
||||
>
|
||||
New organization V2
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
||||
import { toJS } from 'mobx'
|
||||
import { Button, Dropdown, IconPlus } from 'ui'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useFlag } from 'hooks'
|
||||
|
||||
const OrganizationDropdown = ({ organizations }) => {
|
||||
const router = useRouter()
|
||||
@@ -11,6 +12,8 @@ const OrganizationDropdown = ({ organizations }) => {
|
||||
a.name.localeCompare(b.name)
|
||||
)
|
||||
|
||||
const orgCreationV2 = useFlag('orgcreationv2')
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
@@ -33,6 +36,14 @@ const OrganizationDropdown = ({ organizations }) => {
|
||||
<Dropdown.Item icon={<IconPlus size="tiny" />} onClick={() => router.push(`/new`)}>
|
||||
New organization
|
||||
</Dropdown.Item>
|
||||
{orgCreationV2 && (
|
||||
<Dropdown.Item
|
||||
icon={<IconPlus size="tiny" />}
|
||||
onClick={() => router.push(`/new-with-subscription`)}
|
||||
>
|
||||
New organization V2
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -26,6 +26,11 @@ export const PRICING_TIER_LABELS = {
|
||||
PRO: 'Pro',
|
||||
}
|
||||
|
||||
export const PRICING_TIER_LABELS_ORG = {
|
||||
PRO: 'Pro - $25/month',
|
||||
TEAM: 'Team - $599/month',
|
||||
}
|
||||
|
||||
export const PRICING_TIER_PRODUCT_IDS = {
|
||||
FREE: 'tier_free',
|
||||
PRO: 'tier_pro',
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { API_URL, STRIPE_PUBLIC_KEY } from 'lib/constants'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { WizardLayout } from 'components/layouts'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { useIsHCaptchaLoaded } from 'stores/hcaptcha-loaded-store'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { NewOrgForm } from 'components/interfaces/Organization'
|
||||
import { useTheme } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
|
||||
|
||||
/**
|
||||
* No org selected yet, create a new one
|
||||
*/
|
||||
const Wizard: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const [intent, setIntent] = useState<any>()
|
||||
const captchaLoaded = useIsHCaptchaLoaded()
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
|
||||
|
||||
const orgCreationV2 = useFlag('orgcreationv2')
|
||||
|
||||
const { isDarkMode } = useTheme()
|
||||
|
||||
const captchaRefCallback = useCallback((node) => {
|
||||
setCaptchaRef(node)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (orgCreationV2 === false) {
|
||||
router.push('/new')
|
||||
}
|
||||
}, [orgCreationV2])
|
||||
|
||||
const setupIntent = async (hcaptchaToken: string | undefined) => {
|
||||
// Force a reload of Elements, necessary for Stripe
|
||||
setIntent(undefined)
|
||||
|
||||
const intent = await post(`${API_URL}/stripe/setup-intent`, {
|
||||
hcaptchaToken,
|
||||
})
|
||||
|
||||
if (intent.error) {
|
||||
return ui.setNotification({
|
||||
category: 'error',
|
||||
message: intent.error.message,
|
||||
error: intent.error,
|
||||
})
|
||||
} else {
|
||||
setIntent(intent)
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
clientSecret: intent ? intent.client_secret : '',
|
||||
appearance: { theme: isDarkMode ? 'night' : 'flat', labels: 'floating' },
|
||||
} as any
|
||||
|
||||
const loadPaymentForm = async () => {
|
||||
if (captchaRef && captchaLoaded) {
|
||||
let token = captchaToken
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
}
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
await setupIntent(token ?? undefined)
|
||||
resetCaptcha()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPaymentForm()
|
||||
}, [captchaRef, captchaLoaded])
|
||||
|
||||
const resetSetupIntent = () => {
|
||||
return loadPaymentForm()
|
||||
}
|
||||
|
||||
const onLocalCancel = () => {
|
||||
setIntent(undefined)
|
||||
}
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef?.resetCaptcha()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HCaptcha
|
||||
ref={captchaRefCallback}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
}}
|
||||
onClose={onLocalCancel}
|
||||
onExpire={() => {
|
||||
setCaptchaToken(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
{intent && (
|
||||
<Elements stripe={stripePromise} options={options}>
|
||||
<NewOrgForm onPaymentMethodReset={() => resetSetupIntent()} />
|
||||
</Elements>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Wizard.getLayout = (page) => (
|
||||
<WizardLayout organization={null} project={null}>
|
||||
{page}
|
||||
</WizardLayout>
|
||||
)
|
||||
|
||||
export default observer(Wizard)
|
||||
@@ -4,7 +4,17 @@ import { debounce, isUndefined, values } from 'lodash'
|
||||
import { toJS } from 'mobx'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import generator from 'generate-password'
|
||||
import { Button, Listbox, IconUsers, Input, Alert, IconHelpCircle, Toggle } from 'ui'
|
||||
import {
|
||||
Button,
|
||||
Listbox,
|
||||
IconUsers,
|
||||
Input,
|
||||
Alert,
|
||||
IconHelpCircle,
|
||||
Toggle,
|
||||
IconAlertCircle,
|
||||
IconInfo,
|
||||
} from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
@@ -35,6 +45,7 @@ import {
|
||||
EmptyPaymentMethodWarning,
|
||||
} from 'components/interfaces/Organization/NewProject'
|
||||
import SpendCapModal from 'components/interfaces/Billing/SpendCapModal'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
|
||||
const Wizard: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
@@ -63,7 +74,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
|
||||
const organizations = values(toJS(app.organizations.list()))
|
||||
const currentOrg = organizations.find((o: any) => o.slug === slug)
|
||||
const stripeCustomerId = currentOrg?.stripe_customer_id
|
||||
const billedViaOrg = Boolean(currentOrg?.subscription_id)
|
||||
|
||||
const availableRegions = getAvailableRegions()
|
||||
const isAdmin = checkPermissions(PermissionAction.CREATE, 'projects')
|
||||
@@ -290,6 +301,24 @@ const Wizard: NextPageWithLayout = () => {
|
||||
</Listbox>
|
||||
)}
|
||||
|
||||
{billedViaOrg && (
|
||||
<InformationBox
|
||||
icon={<IconInfo size="large" strokeWidth={1.5} />}
|
||||
defaultVisibility={true}
|
||||
hideCollapse
|
||||
title="Billed via organization"
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm leading-normal">
|
||||
This is heavy Work-In-Progress and not customer facing yet, use with
|
||||
caution! This organization uses the new org level billing, instead of having
|
||||
individual subscriptions per project.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAdmin && <NotOrganizationOwnerWarning />}
|
||||
</Panel.Content>
|
||||
|
||||
@@ -397,7 +426,7 @@ const Wizard: NextPageWithLayout = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
{isAdmin && !billedViaOrg && (
|
||||
<Panel.Content>
|
||||
<Listbox
|
||||
label="Pricing Plan"
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface Organization {
|
||||
is_owner?: boolean
|
||||
stripe_customer_id?: string
|
||||
opt_in_tags: string[]
|
||||
subscription_id?: string
|
||||
}
|
||||
|
||||
export interface ProjectBase {
|
||||
|
||||
Reference in new issue
Block a user