feat: upgrade via pricing page (#28942)
* feat: upgrade via pricing page * reinstall cmdk * fix button size * improve search & autoclose * Update apps/www/lib/fetchWrapper.ts Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com> * Update apps/www/lib/fetchWrapper.ts Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com> * update telemetry events --------- Co-authored-by: Kevin Grüneberg <k.grueneberg1994@gmail.com>
This commit is contained in:
@@ -1,39 +1,8 @@
|
||||
import type { Session, User } from '@supabase/auth-js'
|
||||
import { gotrueClient } from 'common'
|
||||
import type { User } from '@supabase/auth-js'
|
||||
import { getAccessToken, gotrueClient } from 'common'
|
||||
|
||||
export const auth = gotrueClient
|
||||
|
||||
let currentSession: Session | null = null
|
||||
|
||||
auth.onAuthStateChange((event, session) => {
|
||||
currentSession = session
|
||||
})
|
||||
|
||||
/**
|
||||
* Grabs the currently available access token, or calls getSession.
|
||||
*/
|
||||
export async function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return undefined
|
||||
|
||||
const aboutToExpire = currentSession?.expires_at
|
||||
? currentSession.expires_at - Math.ceil(Date.now() / 1000) < 30
|
||||
: false
|
||||
|
||||
if (!currentSession || aboutToExpire) {
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await auth.getSession()
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return session?.access_token
|
||||
}
|
||||
|
||||
return currentSession.access_token
|
||||
}
|
||||
export { getAccessToken }
|
||||
|
||||
export const getAuthUser = async (token: String): Promise<any> => {
|
||||
try {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"ip-num": "^1.5.1",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-table": "^3.0.3",
|
||||
"mime-db": "^1.53.0",
|
||||
"mobx": "^6.10.2",
|
||||
|
||||
@@ -1,59 +1,74 @@
|
||||
import React, { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
import { plans } from 'shared-data/plans'
|
||||
import { pricing } from 'shared-data/pricing'
|
||||
import { Button, Select, cn } from 'ui'
|
||||
import { PricingTableRowDesktop, PricingTableRowMobile } from '~/components/Pricing/PricingTableRow'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import { Organization } from '~/data/organizations'
|
||||
import Solutions from '~/data/Solutions'
|
||||
import { pricing } from 'shared-data/pricing'
|
||||
import { plans } from 'shared-data/plans'
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
import UpgradePlan from './UpgradePlan'
|
||||
|
||||
const PricingComparisonTable = () => {
|
||||
const MobileHeader = ({
|
||||
description,
|
||||
priceDescription,
|
||||
price,
|
||||
plan,
|
||||
showDollarSign = true,
|
||||
from = false,
|
||||
organizations,
|
||||
hasExistingOrganizations,
|
||||
}: {
|
||||
description: string
|
||||
priceDescription: string
|
||||
price: string
|
||||
plan: string
|
||||
showDollarSign?: boolean
|
||||
from?: boolean
|
||||
organizations?: Organization[]
|
||||
hasExistingOrganizations?: boolean
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const [activeMobilePlan, setActiveMobilePlan] = useState('Free')
|
||||
|
||||
const sendTelemetryEvent = async (event: TelemetryEvent) => {
|
||||
await Telemetry.sendEvent(event, telemetryProps, router)
|
||||
}
|
||||
|
||||
const MobileHeader = ({
|
||||
description,
|
||||
priceDescription,
|
||||
price,
|
||||
plan,
|
||||
showDollarSign = true,
|
||||
from = false,
|
||||
}: {
|
||||
description: string
|
||||
priceDescription: string
|
||||
price: string
|
||||
plan: string
|
||||
showDollarSign?: boolean
|
||||
from?: boolean
|
||||
}) => {
|
||||
const selectedPlan = plans.find((p) => p.name === plan)!
|
||||
const selectedPlan = plans.find((p) => p.name === plan)!
|
||||
const isUpgradablePlan = selectedPlan.name === 'Pro' || selectedPlan.name === 'Team'
|
||||
|
||||
return (
|
||||
<div className="mt-8 px-4 mobile-header">
|
||||
<h2 className="text-foreground text-3xl font-medium uppercase font-mono">{plan}</h2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
{from && <span className="text-foreground text-base">From</span>}
|
||||
{showDollarSign ? (
|
||||
<span className="h1 font-mono">
|
||||
{plan !== 'Enterprise' ? '$' : ''}
|
||||
{price}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-foreground-light">{price}</span>
|
||||
)}
|
||||
return (
|
||||
<div className="mt-8 px-4 mobile-header">
|
||||
<h2 className="text-foreground text-3xl font-medium uppercase font-mono">{plan}</h2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
{from && <span className="text-foreground text-base">From</span>}
|
||||
{showDollarSign ? (
|
||||
<span className="h1 font-mono">
|
||||
{plan !== 'Enterprise' ? '$' : ''}
|
||||
{price}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-foreground-light">{price}</span>
|
||||
)}
|
||||
|
||||
<p className="p">{priceDescription}</p>
|
||||
</div>
|
||||
<p className="p">{description}</p>
|
||||
<p className="p">{priceDescription}</p>
|
||||
</div>
|
||||
<p className="p">{description}</p>
|
||||
{isUpgradablePlan && hasExistingOrganizations ? (
|
||||
<UpgradePlan
|
||||
organizations={organizations}
|
||||
onClick={() =>
|
||||
sendTelemetryEvent(
|
||||
gaEvents[`www_pricing_comparison_${plan.toLowerCase()}_mobile_upgrade`]
|
||||
)
|
||||
}
|
||||
size="medium"
|
||||
/>
|
||||
) : (
|
||||
<Button asChild size="medium" type={plan === 'Enterprise' ? 'default' : 'primary'} block>
|
||||
<Link
|
||||
href={selectedPlan.href}
|
||||
@@ -64,8 +79,26 @@ const PricingComparisonTable = () => {
|
||||
{selectedPlan.cta}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface PricingComparisonTableProps {
|
||||
organizations?: Organization[]
|
||||
hasExistingOrganizations?: boolean
|
||||
}
|
||||
|
||||
const PricingComparisonTable = ({
|
||||
organizations,
|
||||
hasExistingOrganizations,
|
||||
}: PricingComparisonTableProps) => {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const [activeMobilePlan, setActiveMobilePlan] = useState('Free')
|
||||
|
||||
const sendTelemetryEvent = async (event: TelemetryEvent) => {
|
||||
await Telemetry.sendEvent(event, telemetryProps, router)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -161,6 +194,8 @@ const PricingComparisonTable = () => {
|
||||
price={'25'}
|
||||
priceDescription={'/month + additional use'}
|
||||
description={'Everything you need to scale your project into production'}
|
||||
organizations={organizations}
|
||||
hasExistingOrganizations={hasExistingOrganizations}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
@@ -213,6 +248,8 @@ const PricingComparisonTable = () => {
|
||||
price={'599'}
|
||||
priceDescription={'/month + additional use'}
|
||||
description={'Collaborate with different permissions and access patterns'}
|
||||
organizations={organizations}
|
||||
hasExistingOrganizations={hasExistingOrganizations}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
@@ -327,54 +364,72 @@ const PricingComparisonTable = () => {
|
||||
/>
|
||||
</th>
|
||||
|
||||
{plans.map((plan) => (
|
||||
<th
|
||||
className="text-foreground w-1/4 px-0 text-left text-sm font-normal"
|
||||
scope="col"
|
||||
key={plan.name}
|
||||
>
|
||||
<span className="flex flex-col px-6 pr-2 pt-2 gap-1.5">
|
||||
<span className="flex flex-col xl:flex-row xl:items-end gap-1">
|
||||
<h3 className="text-lg xl:text-xl 2xl:text-2xl leading-5 uppercase font-mono font-normal flex items-center">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-lighter -my-1 xl:m-0',
|
||||
plan.name === 'Enterprise' && 'xl:opacity-0'
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground-lighter font-mono text-xl mr-1 tracking-tighter">
|
||||
{plan.name !== 'Enterprise' && '$'}
|
||||
{plan.priceMonthly}
|
||||
</span>
|
||||
{['Free', 'Pro', 'Team'].includes(plan.name) && (
|
||||
<span className="text-[13px] leading-4 mt-1">{plan.costUnit}</span>
|
||||
)}
|
||||
</p>
|
||||
</span>
|
||||
<span className="flex flex-col justify-between h-full pb-2">
|
||||
<Button
|
||||
asChild
|
||||
size="tiny"
|
||||
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
block
|
||||
>
|
||||
<Link
|
||||
href={plan.href}
|
||||
onClick={() =>
|
||||
sendTelemetryEvent(
|
||||
gaEvents[`www_pricing_comparison_${plan.name.toLowerCase()}`]
|
||||
)
|
||||
}
|
||||
{plans.map((plan) => {
|
||||
const isUpgradablePlan = plan.name === 'Pro' || plan.name === 'Team'
|
||||
|
||||
return (
|
||||
<th
|
||||
className="text-foreground w-1/4 px-0 text-left text-sm font-normal"
|
||||
scope="col"
|
||||
key={plan.name}
|
||||
>
|
||||
<span className="flex flex-col px-6 pr-2 pt-2 gap-1.5">
|
||||
<span className="flex flex-col xl:flex-row xl:items-end gap-1">
|
||||
<h3 className="text-lg xl:text-xl 2xl:text-2xl leading-5 uppercase font-mono font-normal flex items-center">
|
||||
{plan.name}
|
||||
</h3>
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-lighter -my-1 xl:m-0',
|
||||
plan.name === 'Enterprise' && 'xl:opacity-0'
|
||||
)}
|
||||
>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
</Button>
|
||||
<span className="text-foreground-lighter font-mono text-xl mr-1 tracking-tighter">
|
||||
{plan.name !== 'Enterprise' && '$'}
|
||||
{plan.priceMonthly}
|
||||
</span>
|
||||
{['Free', 'Pro', 'Team'].includes(plan.name) && (
|
||||
<span className="text-[13px] leading-4 mt-1">{plan.costUnit}</span>
|
||||
)}
|
||||
</p>
|
||||
</span>
|
||||
<span className="flex flex-col justify-between h-full pb-2">
|
||||
{isUpgradablePlan && hasExistingOrganizations ? (
|
||||
<UpgradePlan
|
||||
organizations={organizations}
|
||||
onClick={() =>
|
||||
sendTelemetryEvent(
|
||||
gaEvents[
|
||||
`www_pricing_comparison_${plan.name.toLowerCase()}_upgrade`
|
||||
]
|
||||
)
|
||||
}
|
||||
size="tiny"
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
size="tiny"
|
||||
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
block
|
||||
>
|
||||
<Link
|
||||
href={plan.href}
|
||||
onClick={() =>
|
||||
sendTelemetryEvent(
|
||||
gaEvents[`www_pricing_comparison_${plan.name.toLowerCase()}`]
|
||||
)
|
||||
}
|
||||
>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</th>
|
||||
))}
|
||||
</th>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-default divide-border divide-y first:divide-y-0">
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React, { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Button, cn, IconCheck } from 'ui'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
|
||||
import { useTelemetryProps } from 'common/hooks/useTelemetryProps'
|
||||
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import { pickFeatures, pickFooter, plans } from 'shared-data/plans'
|
||||
import { Button, cn, IconCheck } from 'ui'
|
||||
import { Organization } from '~/data/organizations'
|
||||
import gaEvents from '~/lib/gaEvents'
|
||||
import Telemetry, { TelemetryEvent } from '~/lib/telemetry'
|
||||
import UpgradePlan from './UpgradePlan'
|
||||
|
||||
const PricingPlans: FC = () => {
|
||||
interface PricingPlansProps {
|
||||
organizations?: Organization[]
|
||||
hasExistingOrganizations?: boolean
|
||||
}
|
||||
|
||||
const PricingPlans = ({ organizations, hasExistingOrganizations }: PricingPlansProps) => {
|
||||
const router = useRouter()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
|
||||
@@ -21,24 +27,29 @@ const PricingPlans: FC = () => {
|
||||
<div className="relative z-10 mx-auto w-full px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-md grid lg:max-w-none lg:grid-cols-2 xl:grid-cols-4 gap-4 xl:gap-0">
|
||||
{plans.map((plan) => {
|
||||
const isPromoPlan = plan.name === 'Pro'
|
||||
const isProPlan = plan.name === 'Pro'
|
||||
const isTeamPlan = plan.name === 'Team'
|
||||
const isUpgradablePlan = isProPlan || isTeamPlan
|
||||
const features = pickFeatures(plan)
|
||||
const footer = pickFooter(plan)
|
||||
|
||||
const sendPricingEvent = () => {
|
||||
sendTelemetryEvent(gaEvents[`www_pricing_hero_plan_${plan.name.toLowerCase()}`])
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`row-${plan.name}`}
|
||||
className={cn(
|
||||
'flex flex-col border xl:border-r-0 last:border-r bg-surface-75 rounded-xl xl:rounded-none first:rounded-l-xl last:rounded-r-xl',
|
||||
isPromoPlan && 'border-foreground-muted !border-2 !rounded-xl xl:-my-8',
|
||||
isProPlan && 'border-foreground-muted !border-2 !rounded-xl xl:-my-8',
|
||||
isTeamPlan && 'xl:border-l-0'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'px-8 xl:px-4 2xl:px-8 pt-6',
|
||||
isPromoPlan ? 'rounded-tr-[9px] rounded-tl-[9px]' : ''
|
||||
isProPlan ? 'rounded-tr-[9px] rounded-tl-[9px]' : ''
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -56,28 +67,25 @@ const PricingPlans: FC = () => {
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-light mb-4 text-sm 2xl:pr-4',
|
||||
isPromoPlan && 'xl:mb-12'
|
||||
isProPlan && 'xl:mb-12'
|
||||
)}
|
||||
>
|
||||
{plan.description}
|
||||
</p>
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href={plan.href}
|
||||
onClick={() =>
|
||||
sendTelemetryEvent(
|
||||
gaEvents[`www_pricing_hero_plan_${plan.name.toLowerCase()}`]
|
||||
)
|
||||
}
|
||||
{isUpgradablePlan && hasExistingOrganizations ? (
|
||||
<UpgradePlan organizations={organizations} onClick={sendPricingEvent} />
|
||||
) : (
|
||||
<Button
|
||||
block
|
||||
size="large"
|
||||
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
asChild
|
||||
>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
</Button>
|
||||
<Link href={plan.href} onClick={sendPricingEvent}>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
@@ -135,7 +143,7 @@ const PricingPlans: FC = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'border-default flex rounded-bl-[4px] rounded-br-[4px] flex-1 flex-col px-8 xl:px-4 2xl:px-8 py-6',
|
||||
isPromoPlan && 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]'
|
||||
isProPlan && 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]'
|
||||
)}
|
||||
>
|
||||
{plan.preface && (
|
||||
|
||||
158
apps/www/components/Pricing/UpgradePlan.tsx
Normal file
158
apps/www/components/Pricing/UpgradePlan.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Check, ChevronsUpDown, Plus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
cn,
|
||||
Command_Shadcn_,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
CommandSeparator_Shadcn_,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Popover_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Organization } from '~/data/organizations'
|
||||
|
||||
interface UpgradePlanProps {
|
||||
organizations?: Organization[]
|
||||
onClick?: () => void
|
||||
size?: ButtonProps['size']
|
||||
}
|
||||
|
||||
const UpgradePlan = ({ organizations = [], onClick, size = 'large' }: UpgradePlanProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button block size={size} type="primary" onClick={onClick}>
|
||||
Upgrade now
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader className="pb-3">
|
||||
<DialogTitle>Upgrade organization</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose the organization you want to upgrade to a paid plan.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSection className="py-2">
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="default"
|
||||
role="combobox"
|
||||
size={'small'}
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
||||
>
|
||||
{value === 'new-organization' ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
New organization
|
||||
</span>
|
||||
) : value ? (
|
||||
organizations.find((organization) => organization.slug === value)?.name
|
||||
) : (
|
||||
'Select an organization...'
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="w-[300px] p-0">
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Select organization..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No organizations found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{organizations.map((organization) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={organization.slug}
|
||||
value={organization.slug}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? '' : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
keywords={[organization.name]}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value === organization.slug ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{organization.name}
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandSeparator_Shadcn_ />
|
||||
<CommandGroup_Shadcn_>
|
||||
<CommandItem_Shadcn_
|
||||
value="new-organization"
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? '' : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
keywords={['Create a new organization']}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value === 'new-organization' ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<Plus className="h-4 w-4 mr-2" /> Create a new organization
|
||||
</CommandItem_Shadcn_>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</DialogSection>
|
||||
|
||||
<DialogSection>
|
||||
<DialogDescription className="text-xs">
|
||||
Upon continuing, you will be redirected to the organization's billing page where
|
||||
you can upgrade to a paid plan.
|
||||
</DialogDescription>
|
||||
</DialogSection>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button disabled={!value} asChild>
|
||||
<Link
|
||||
href={
|
||||
value === 'new-organization'
|
||||
? `/dashboard/new`
|
||||
: `/dashboard/org/${value}/billing?panel=subscriptionPlan`
|
||||
}
|
||||
>
|
||||
Continue
|
||||
</Link>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradePlan
|
||||
47
apps/www/data/organizations.ts
Normal file
47
apps/www/data/organizations.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { components } from 'api-types'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { API_URL } from '~/lib/constants'
|
||||
import { get } from '~/lib/fetchWrapper'
|
||||
|
||||
export type Organization = components['schemas']['OrganizationResponse']
|
||||
|
||||
export function useOrganizations() {
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
const controller = new AbortController()
|
||||
|
||||
get(`${API_URL}/organizations`, { signal: controller.signal })
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw res
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (isMounted) {
|
||||
setOrganizations(data)
|
||||
setIsLoading(false)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// eat all errors
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
controller.abort()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
organizations,
|
||||
isLoading,
|
||||
} as const
|
||||
}
|
||||
@@ -1,18 +1,46 @@
|
||||
import { getAccessToken } from 'common'
|
||||
|
||||
interface DataProps {
|
||||
[prop: string]: any
|
||||
}
|
||||
|
||||
export const post = (url: string, data: DataProps, options = {}) => {
|
||||
export async function get(url: string, options?: RequestInit) {
|
||||
const accessToken = await getAccessToken()
|
||||
|
||||
let headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
})
|
||||
|
||||
if (accessToken) {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export async function post(url: string, data: DataProps, options?: RequestInit) {
|
||||
const accessToken = await getAccessToken()
|
||||
|
||||
let headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
})
|
||||
|
||||
if (accessToken) {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
headers,
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
}).catch((error) => {
|
||||
console.error('Error at fetchWrapper - post:', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"globby": "^13.2.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"icons": "*",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"next": "^14.2.3",
|
||||
"next-contentlayer2": "0.4.6",
|
||||
|
||||
@@ -8,6 +8,7 @@ import ReactTooltip from 'react-tooltip'
|
||||
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import PricingPlans from '~/components/Pricing/PricingPlans'
|
||||
import { useOrganizations } from '~/data/organizations'
|
||||
|
||||
const PricingComputeSection = dynamic(() => import('~/components/Pricing/PricingComputeSection'))
|
||||
const PricingAddons = dynamic(() => import('~/components/Pricing/PricingAddons'))
|
||||
@@ -51,6 +52,9 @@ export default function IndexPage() {
|
||||
}
|
||||
}, [asPath])
|
||||
|
||||
const { isLoading, organizations } = useOrganizations()
|
||||
const hasExistingOrganizations = !isLoading && organizations.length > 0
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<NextSeo
|
||||
@@ -82,7 +86,10 @@ export default function IndexPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PricingPlans />
|
||||
<PricingPlans
|
||||
organizations={organizations}
|
||||
hasExistingOrganizations={hasExistingOrganizations}
|
||||
/>
|
||||
|
||||
<div className="text-center mt-10 xl:mt-16 mx-auto max-w-lg flex flex-col gap-8">
|
||||
<div className="flex justify-center gap-2">
|
||||
@@ -115,7 +122,10 @@ export default function IndexPage() {
|
||||
<PricingAddons />
|
||||
</div>
|
||||
|
||||
<PricingComparisonTable />
|
||||
<PricingComparisonTable
|
||||
organizations={organizations}
|
||||
hasExistingOrganizations={hasExistingOrganizations}
|
||||
/>
|
||||
|
||||
<div id="faq" className="border-t">
|
||||
<PricingFAQs />
|
||||
|
||||
35
package-lock.json
generated
35
package-lock.json
generated
@@ -94,6 +94,14 @@
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"apps/database-new/node_modules/lucide-react": {
|
||||
"version": "0.338.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.338.0.tgz",
|
||||
"integrity": "sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/sql-formatter": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-13.1.0.tgz",
|
||||
@@ -510,6 +518,14 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"apps/design-system/node_modules/lucide-react": {
|
||||
"version": "0.338.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.338.0.tgz",
|
||||
"integrity": "sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"apps/design-system/node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
@@ -1651,7 +1667,7 @@
|
||||
"ip-num": "^1.5.1",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-table": "^3.0.3",
|
||||
"mime-db": "^1.53.0",
|
||||
"mobx": "^6.10.2",
|
||||
@@ -2795,6 +2811,7 @@
|
||||
"globby": "^13.2.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"icons": "*",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"next": "^14.2.3",
|
||||
"next-contentlayer2": "0.4.6",
|
||||
@@ -27204,11 +27221,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.338.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.338.0.tgz",
|
||||
"integrity": "sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==",
|
||||
"version": "0.436.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.436.0.tgz",
|
||||
"integrity": "sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
@@ -44524,6 +44541,14 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/ui/node_modules/lucide-react": {
|
||||
"version": "0.338.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.338.0.tgz",
|
||||
"integrity": "sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"packages/ui/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"dev": true,
|
||||
|
||||
@@ -112,3 +112,35 @@ export const useIsLoggedIn = () => {
|
||||
|
||||
return user !== null
|
||||
}
|
||||
|
||||
let currentSession: Session | null = null
|
||||
|
||||
gotrueClient.onAuthStateChange((event, session) => {
|
||||
currentSession = session
|
||||
})
|
||||
|
||||
/**
|
||||
* Grabs the currently available access token, or calls getSession.
|
||||
*/
|
||||
export async function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return undefined
|
||||
|
||||
const aboutToExpire = currentSession?.expires_at
|
||||
? currentSession.expires_at - Math.ceil(Date.now() / 1000) < 30
|
||||
: false
|
||||
|
||||
if (!currentSession || aboutToExpire) {
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await gotrueClient.getSession()
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return session?.access_token
|
||||
}
|
||||
|
||||
return currentSession.access_token
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user