Feat: Base for new billing settings (#4115)

This commit is contained in:
Mike
2025-03-05 18:16:05 +01:00
committed by GitHub
parent 709b87b0fa
commit a13145332b
15 changed files with 938 additions and 489 deletions
@@ -0,0 +1,23 @@
<!-- TODO: Implement final values, links and copy -->
<template>
<div
class="border border-outline-3 rounded-lg divide-outline-3 divide-x flex flex-col md:flex-row"
>
<div class="p-6 w-1/2">
<h3 class="text-body font-medium">Unlimited projects and models</h3>
<p class="text-body-2xs text-foreground-2 mt-1">plus X per seat / month</p>
<p class="max-w-60 text-foreground-2 text-body-2xs mt-6">
Add more projects and models to your workspace.
</p>
<FormButton class="mt-6" color="outline" size="sm">Contact us</FormButton>
</div>
<div class="p-6 w-1/2">
<h3 class="text-body font-medium">Extra data regions</h3>
<p class="text-body-2xs text-foreground-2 mt-1">Talk to us!</p>
<p class="max-w-60 text-foreground-2 text-body-2xs mt-6">
Add more available data regions. Learn more about Speckle Data Regions.
</p>
<FormButton class="mt-6" color="outline" size="sm">Contact us</FormButton>
</div>
</div>
</template>
@@ -0,0 +1,471 @@
<!-- "Old" billing page, the component for the new workspace plans is in PageNew.vue -->
<template>
<section>
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
<FormButton
v-if="isWorkspaceNewPlansEnabled && isServerAdmin"
size="lg"
class="!bg-pink-500 !border-pink-700 mb-4"
@click="handleUpgradeClick"
>
𝓒𝓱𝓪𝓷𝓰𝓮 𝓽𝓸 𝓷𝓮𝔀 𝓹𝓵𝓪𝓷 💸
</FormButton>
<SettingsSectionHeader title="Billing" text="Your workspace billing details" />
<template v-if="isBillingIntegrationEnabled">
<div class="flex flex-col gap-y-4 md:gap-y-6">
<BillingAlert
v-if="workspace && workspace?.plan?.status !== WorkspacePlanStatuses.Valid"
:workspace="workspace"
/>
<SettingsSectionHeader title="Billing summary" subheading class="pt-4" />
<div class="border border-outline-3 rounded-lg">
<div
class="grid grid-cols-1 lg:grid-cols-3 divide-y divide-outline-3 lg:divide-y-0 lg:divide-x"
>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryPlanHeading }}
</h3>
<div class="flex gap-x-2">
<p class="text-heading-lg text-foreground">
Workspace
<span class="capitalize">
{{ currentPlan?.name ?? WorkspacePlans.Starter }}
</span>
</p>
<div>
<CommonBadge v-if="showStatusBadge" rounded>TRIAL</CommonBadge>
</div>
</div>
<p v-if="isPurchasablePlan" class="text-body-xs text-foreground-2">
<span v-if="statusIsTrial">
<span class="line-through mr-1">
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month
</span>
Free
</span>
<span
v-else-if="currentPlan?.status === WorkspacePlanStatuses.Expired"
>
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month
</span>
<span v-else>
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month, billed
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'annually'
: 'monthly'
}}
</span>
</p>
</div>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<template v-if="isPurchasablePlan || statusIsTrial">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryBillHeading }}
</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ summaryBillValue }} per
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'year'
: 'month'
}}
</p>
<p class="text-body-xs text-foreground-2 flex gap-x-1 items-center">
{{ summaryBillDescription }}
<InformationCircleIcon
v-tippy="billTooltip"
class="w-4 h-4 text-foreground cursor-pointer"
/>
</p>
</template>
<template v-else>
<h3 class="text-body-xs text-foreground-2 pb-1">Expected bill</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ isAcademiaPlan ? 'Free' : 'Not applicable' }}
</p>
</template>
</div>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryDateHeading }}
</h3>
<p class="text-heading-lg text-foreground capitalize">
{{ isPurchasablePlan ? nextPaymentDue : 'Never' }}
</p>
<p
v-if="showSummaryDateDescription"
class="text-body-xs text-foreground-2"
>
<span v-if="statusIsTrial">Subscribe before this date</span>
<span v-else>
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'Annual'
: 'Monthly'
}}
billing period
</span>
</p>
</div>
</div>
<div
v-if="isActivePlan && isPurchasablePlan"
class="flex flex-row gap-x-4 p-5 items-center border-t border-outline-3"
>
<div class="text-body-xs gap-y-2 flex-1">
<p class="font-medium text-foreground">Billing portal</p>
<p class="text-foreground-2">
View invoices, edit payment details, and manage your subscription.
</p>
</div>
<FormButton color="outline" @click="billingPortalRedirect(workspace?.id)">
Open billing portal
</FormButton>
</div>
</div>
<template v-if="isPurchasablePlan || statusIsTrial">
<SettingsSectionHeader
:title="pricingTableHeading"
subheading
class="pt-4"
/>
<SettingsWorkspacesBillingPricingTable
:workspace-id="workspace?.id"
:current-plan="currentPlan"
:active-billing-interval="subscription?.billingInterval"
:is-admin="isAdmin"
@on-plan-selected="onPlanSelected"
/>
</template>
</div>
<div v-if="isInvoicedPlan" class="mt-8 text-foreground-2 text-body-xs">
Need help?
<a
class="text-foreground hover:underline"
href="mailto:billing@speckle.systems"
@click="
mixpanel.track('Workspace Support Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
Contact us
</a>
</div>
<div
v-else-if="isPurchasablePlan"
class="mt-8 text-center text-foreground-2 text-body-xs"
>
Need help?
<NuxtLink
class="text-foreground"
:to="guideBillingUrl"
target="_blank"
@click="
mixpanel.track('Workspace Docs Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
<span class="hover:underline">Read the docs</span>
</NuxtLink>
or
<a
class="text-foreground hover:underline"
href="mailto:billing@speckle.systems"
@click="
mixpanel.track('Workspace Support Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
contact support
</a>
</div>
<SettingsWorkspacesBillingUpgradeDialog
v-if="selectedPlanName && selectedPlanCycle && workspace?.id"
v-model:open="isUpgradeDialogOpen"
:plan="selectedPlanName"
:billing-interval="selectedPlanCycle"
:workspace-id="workspace.id"
/>
</template>
<template v-else>Coming soon</template>
</div>
</section>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { graphql } from '~/lib/common/generated/gql'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { settingsWorkspaceBillingQuery } from '~/lib/settings/graphql/queries'
import { useIsBillingIntegrationEnabled } from '~/composables/globals'
import {
WorkspacePlans,
WorkspacePlanStatuses,
BillingInterval,
WorkspacePaymentMethod,
type PaidWorkspacePlans
} from '~/lib/common/generated/gql/graphql'
import { useBillingActions } from '~/lib/billing/composables/actions'
import type { PaidWorkspacePlansOld } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { InformationCircleIcon } from '@heroicons/vue/24/outline'
import { isPaidPlan } from '@/lib/billing/helpers/types'
import { useMixpanel } from '~/lib/core/composables/mp'
import { guideBillingUrl } from '~/lib/common/helpers/route'
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
import { formatPrice } from '~/lib/billing/helpers/prices'
graphql(`
fragment SettingsWorkspacesBilling_Workspace on Workspace {
...BillingAlert_Workspace
id
role
plan {
name
status
createdAt
paymentMethod
}
subscription {
billingInterval
currentBillingCycleEnd
seats {
guest
plan
}
}
team {
items {
id
role
}
}
}
`)
const slug = computed(() => (route.params.slug as string) || '')
const { prices } = useWorkspacePlanPrices()
const { isAdmin: isServerAdmin } = useActiveUser()
const route = useRoute()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const { result: workspaceResult } = useQuery(
settingsWorkspaceBillingQuery,
() => ({
slug: slug.value
}),
() => ({
enabled: isBillingIntegrationEnabled
})
)
const { billingPortalRedirect, redirectToCheckout } = useBillingActions()
const mixpanel = useMixpanel()
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
const selectedPlanName = ref<PaidWorkspacePlansOld>()
const selectedPlanCycle = ref<BillingInterval>()
const isUpgradeDialogOpen = ref(false)
const seatPrices = computed(() => ({
[WorkspacePlans.Starter]: prices.value?.[WorkspacePlans.Starter],
[WorkspacePlans.Plus]: prices.value?.[WorkspacePlans.Plus],
[WorkspacePlans.Business]: prices.value?.[WorkspacePlans.Business]
}))
const workspace = computed(() => workspaceResult.value?.workspaceBySlug)
const currentPlan = computed(() => workspace.value?.plan)
const subscription = computed(() => workspace.value?.subscription)
const statusIsTrial = computed(
() =>
currentPlan.value?.status === WorkspacePlanStatuses.Trial ||
!currentPlan.value?.status
)
const isActivePlan = computed(
() =>
currentPlan.value &&
currentPlan.value?.status !== WorkspacePlanStatuses.Trial &&
currentPlan.value?.status !== WorkspacePlanStatuses.Canceled &&
currentPlan.value?.status !== WorkspacePlanStatuses.Expired
)
const isAcademiaPlan = computed(
() => currentPlan.value?.name === WorkspacePlans.Academia
)
const isPurchasablePlan = computed(() => isPaidPlan(currentPlan.value?.name))
const seatPrice = computed(() =>
currentPlan.value && subscription.value
? seatPrices.value?.[currentPlan.value.name as keyof typeof seatPrices.value]?.[
subscription.value.billingInterval
]
: seatPrices.value?.[WorkspacePlans.Starter]?.[BillingInterval.Monthly]
)
const nextPaymentDue = computed(() =>
isPurchasablePlan.value
? subscription.value?.currentBillingCycleEnd
? dayjs(subscription.value?.currentBillingCycleEnd).format('MMMM D, YYYY')
: dayjs(currentPlan.value?.createdAt).add(31, 'days').format('MMMM D, YYYY')
: 'Never'
)
const isInvoicedPlan = computed(
() => currentPlan.value?.paymentMethod === WorkspacePaymentMethod.Invoice
)
const isAdmin = computed(() => workspace.value?.role === Roles.Workspace.Admin)
const guestSeatCount = computed(() =>
isActivePlan.value
? workspace.value?.subscription?.seats.guest ?? 0
: workspace.value?.team.items.filter((user) => user.role === Roles.Workspace.Guest)
.length ?? 0
)
const memberSeatCount = computed(() =>
isActivePlan.value
? workspace.value?.subscription?.seats.plan ?? 0
: workspace.value
? workspace.value.team.items.length - guestSeatCount.value
: 0
)
const summaryBillValue = computed(() => {
if (!seatPrice.value) return 'loading'
const guestPrice =
seatPrice.value[Roles.Workspace.Guest].amount * guestSeatCount.value
const memberPrice =
seatPrice.value[Roles.Workspace.Member].amount * memberSeatCount.value
const totalPrice = guestPrice + memberPrice
const isAnnual = subscription.value?.billingInterval === BillingInterval.Yearly
return isPurchasablePlan.value ? `£${isAnnual ? totalPrice * 12 : totalPrice}` : '£0'
})
const summaryBillDescription = computed(() => {
const memberText =
memberSeatCount.value > 1 ? `${memberSeatCount.value} members` : '1 member'
const guestText =
guestSeatCount.value > 1 ? `${guestSeatCount.value} guests` : '1 guest'
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
})
const billTooltip = computed(() => {
if (!seatPrice.value) return undefined
const memberText = `${memberSeatCount.value} member${
memberSeatCount.value === 1 ? '' : 's'
} at ${formatPrice(seatPrice.value[Roles.Workspace.Member])}/month`
const guestText = `${guestSeatCount.value} guest${
guestSeatCount.value === 1 ? '' : 's'
} at ${formatPrice(seatPrice.value[Roles.Workspace.Guest])}/month`
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
})
const summaryPlanHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
return 'Trial plan'
case WorkspacePlanStatuses.Expired:
case WorkspacePlanStatuses.Canceled:
return 'Plan'
default:
return 'Current plan'
}
})
const summaryBillHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
case WorkspacePlanStatuses.Canceled:
return 'Expected bill'
default:
return subscription.value?.billingInterval === BillingInterval.Yearly
? 'Annual bill'
: 'Monthly bill'
}
})
const summaryDateHeading = computed(() => {
if (statusIsTrial.value && isPurchasablePlan.value) {
return 'Trial ends'
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Expired) {
return 'Trial expired at'
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Canceled) {
return 'Cancels'
} else {
return 'Next payment due'
}
})
const showSummaryDateDescription = computed(() => {
return statusIsTrial.value && isPurchasablePlan.value
})
const pricingTableHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
return 'Start your subscription'
case WorkspacePlanStatuses.Canceled:
return 'Restart your subscription'
default:
return 'Upgrade your plan'
}
})
const showStatusBadge = computed(() => {
return (
(statusIsTrial.value ||
currentPlan.value?.status === WorkspacePlanStatuses.Expired) &&
isPurchasablePlan.value
)
})
const onPlanSelected = (plan: {
name: PaidWorkspacePlansOld
cycle: BillingInterval
}) => {
const { name, cycle } = plan
if (!isPaidPlan(name) || !workspace.value?.id) return
if (
statusIsTrial.value ||
currentPlan.value?.status === WorkspacePlanStatuses.Expired ||
currentPlan.value?.status === WorkspacePlanStatuses.Canceled
) {
mixpanel.track('Workspace Subscribe Button Clicked', {
plan,
cycle,
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
redirectToCheckout({
plan: name as unknown as PaidWorkspacePlans,
cycle,
workspaceId: workspace.value?.id
})
} else {
selectedPlanName.value = name
selectedPlanCycle.value = cycle
isUpgradeDialogOpen.value = true
}
}
const handleUpgradeClick = () => {
if (!workspace.value?.id) return
// Temporary hack to change workspace plans to the new free plan
mutateWorkspacePlan({
input: {
workspaceId: workspace.value?.id,
plan: WorkspacePlans.Free,
status: WorkspacePlanStatuses.Valid
}
})
}
</script>
@@ -0,0 +1,93 @@
<!-- This is a temporary component and will replace SettingsWorkspacesBillingPage post-migration -->
<template>
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0 flex flex-col gap-y-2 md:gap-y-4">
<SettingsSectionHeader
title="Billing and plans"
text="Update your payment information or switch plans according to your needs"
/>
<div class="flex flex-col gap-y-6 md:gap-y-10">
<section class="flex flex-col gap-y-4 md:gap-y-6">
<SettingsSectionHeader title="Summary" subheading />
<SettingsWorkspacesBillingSummary :workspace-id="workspace?.id" />
</section>
<section class="flex flex-col gap-y-4 md:gap-y-6">
<SettingsSectionHeader title="Usage" subheading />
<SettingsWorkspacesBillingUsage />
</section>
<section class="flex flex-col gap-y-4 md:gap-y-6">
<SettingsSectionHeader title="Add-ons" subheading />
<SettingsWorkspacesBillingAddOns />
</section>
<!-- Temporary until we can test with real upgrades -->
<section v-if="isServerAdmin" class="flex flex-col gap-y-4 md:gap-y-6">
<SettingsSectionHeader title="Upgrade plan" subheading />
<div class="flex gap-x-4">
<FormButton
size="lg"
class="!bg-pink-500 !border-pink-700 mb-4"
@click="handleUpgradeClick(WorkspacePlans.Free)"
>
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖋𝖗𝖊𝖊 𝖕𝖑𝖆𝖓
</FormButton>
<FormButton
size="lg"
class="!bg-pink-500 !border-pink-700 mb-4"
@click="handleUpgradeClick(WorkspacePlans.Team)"
>
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖙𝖊𝖆𝖒 𝖕𝖑𝖆𝖓
</FormButton>
<FormButton
size="lg"
class="!bg-pink-500 !border-pink-700 mb-4"
@click="handleUpgradeClick(WorkspacePlans.Pro)"
>
𝕮𝖍𝖆𝖓𝖌𝖊 𝖙𝖔 𝖕𝖗𝖔 𝖕𝖑𝖆𝖓
</FormButton>
</div>
</section>
</div>
</div>
</template>
<script lang="ts" setup>
import { useQuery, useMutation } from '@vue/apollo-composable'
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
import { settingsWorkspaceBillingQueryNew } from '~/lib/settings/graphql/queries'
import { WorkspacePlans, PaidWorkspacePlanStatuses } from '@speckle/shared'
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const { isAdmin: isServerAdmin } = useActiveUser()
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
const { result: workspaceResult } = useQuery(
settingsWorkspaceBillingQueryNew,
() => ({
slug: slug.value
}),
() => ({
enabled: isBillingIntegrationEnabled
})
)
const workspace = computed(() => workspaceResult.value?.workspaceBySlug)
// Temporary hack to change workspace plans to the new free plan
const handleUpgradeClick = (plan: WorkspacePlans) => {
if (!workspaceResult.value?.workspaceBySlug.id) return
mutateWorkspacePlan({
input: {
workspaceId: workspaceResult.value.workspaceBySlug.id,
plan,
status: PaidWorkspacePlanStatuses.Valid
}
})
// Reload to show the new plan, will be gone soon
window.location.reload()
}
</script>
@@ -0,0 +1,80 @@
<!-- TODO: Some content still missing, needs to be updated as functionality is added -->
<template>
<div class="border border-outline-3 rounded-lg">
<div
class="grid grid-cols-1 lg:grid-cols-3 divide-y divide-outline-3 lg:divide-y-0 lg:divide-x"
>
<div class="p-5 pt-4 flex flex-col">
<h3 class="text-body-xs text-foreground-2 pb-4">Current plan</h3>
<p class="text-heading-lg text-foreground capitalize">
{{ plan?.name }}
</p>
</div>
<div class="p-5 pt-4 flex flex-col">
<h3 class="text-body-xs text-foreground-2 pb-4">
<template v-if="isPurchasablePlan">
<span class="capitalize">{{ billingInterval }}</span>
bill
</template>
<template v-else>Bill</template>
</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ totalCostFormatted }}
<span v-if="isPurchasablePlan">per {{ billingInterval }}</span>
</p>
<NuxtLink
v-if="showBillingPortalLink"
class="text-body-xs text-foreground-2 underline hover:text-foreground cursor-pointer mt-1"
@click="billingPortalRedirect(workspaceId)"
>
View cost breakdown
</NuxtLink>
</div>
<div class="p-5 pt-4 flex flex-col">
<h3 class="text-body-xs text-foreground-2 pb-4">Billing period</h3>
<p class="text-heading-lg text-foreground capitalize">
{{ isPurchasablePlan ? billingInterval : 'Not applicable' }}
</p>
</div>
</div>
<div
v-if="showBillingPortalLink"
class="flex flex-row gap-x-4 p-5 items-center border-t border-outline-3"
>
<div class="text-body-xs gap-y-2 flex-1">
<p class="font-medium text-foreground">Billing portal</p>
<p class="text-foreground-2">
View invoices, edit payment details, and manage your subscription.
</p>
</div>
<FormButton color="outline" @click="billingPortalRedirect(workspaceId)">
Open billing portal
</FormButton>
</div>
</div>
</template>
<script setup lang="ts">
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
import { useBillingActions } from '~/lib/billing/composables/actions'
import type { MaybeNullOrUndefined } from '@speckle/shared'
defineProps<{
workspaceId?: MaybeNullOrUndefined<string>
}>()
const { billingPortalRedirect } = useBillingActions()
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const { plan, isPurchasablePlan, isActivePlan, totalCostFormatted, billingInterval } =
useWorkspacePlan(slug.value)
const showBillingPortalLink = computed(
() => isActivePlan.value && isPurchasablePlan.value
)
</script>
@@ -0,0 +1,34 @@
<template>
<div class="border border-outline-3 rounded-lg p-6 flex items-center justify-between">
<div class="flex-1 flex flex-col">
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
{{ text }}
</p>
<CommonProgressBar
class="max-w-72 w-full"
:current-value="currentValue"
:max-value="maxValue"
/>
</div>
<FormButton color="outline">{{ buttonText }}</FormButton>
</div>
</template>
<script lang="ts" setup>
import { CommonProgressBar } from '@speckle/ui-components'
type UsageType = 'seat' | 'project' | 'model'
const props = defineProps<{
buttonText: string
currentValue: number
maxValue: number
type: UsageType
}>()
const text = computed(() => {
return `${props.currentValue} ${props.type}${
props.currentValue === 1 ? '' : 's'
} used / ${props.maxValue} included`
})
</script>
@@ -0,0 +1,25 @@
<!-- TODO: Update these with real limits once available -->
<template>
<div class="flex flex-col gap-y-4">
<SettingsWorkspacesBillingUsageCard
button-text="Manage seats"
:current-value="7"
:max-value="8"
type="seat"
/>
<SettingsWorkspacesBillingUsageCard
button-text="Manage projects"
:current-value="7"
:max-value="50"
type="project"
/>
<SettingsWorkspacesBillingUsageCard
button-text="Manage models"
:current-value="50"
:max-value="50"
type="model"
/>
</div>
</template>
<script lang="ts" setup></script>
@@ -14,6 +14,7 @@ import { settingsBillingCancelCheckoutSessionMutation } from '~/lib/settings/gra
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import { useMixpanel } from '~/lib/core/composables/mp'
import { graphql } from '~~/lib/common/generated/gql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment BillingActions_Workspace on Workspace {
@@ -48,7 +49,7 @@ export const useBillingActions = () => {
settingsBillingCancelCheckoutSessionMutation
)
const billingPortalRedirect = async (workspaceId?: string) => {
const billingPortalRedirect = async (workspaceId: MaybeNullOrUndefined<string>) => {
if (!workspaceId) return
mixpanel.track('Workspace Billing Portal Button Clicked', {
@@ -48,6 +48,7 @@ export const useWorkspacePlanPrices = () => {
const prices = computed(() => {
const base = result.value?.serverInfo?.workspaces?.planPrices
if (!base) return undefined
const guestSeatPrices = base.find((p) => p.id === 'guest')
return base.reduce((acc, price) => {
@@ -118,6 +118,7 @@ type Documents = {
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": typeof types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": typeof types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": typeof types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": typeof types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersChangeRoleDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
@@ -305,6 +306,7 @@ type Documents = {
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": typeof types.SettingsSidebarAutomateFunctionsDocument,
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": typeof types.SettingsWorkspaceGeneralDocument,
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": typeof types.SettingsWorkspaceBillingDocument,
"\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": typeof types.SettingsWorkspaceBillingNewDocument,
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": typeof types.SettingsWorkspaceBillingCustomerPortalDocument,
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": typeof types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersDocument,
@@ -346,6 +348,7 @@ type Documents = {
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": typeof types.DiscoverableList_DiscoverableFragmentDoc,
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": typeof types.DiscoverableList_RequestsFragmentDoc,
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": typeof types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n }\n subscription {\n billingInterval\n }\n }\n": typeof types.WorkspacesPlan_WorkspaceFragmentDoc,
"\n subscription OnWorkspaceProjectsUpdate($slug: String!) {\n workspaceProjectsUpdated(workspaceId: null, workspaceSlug: $slug) {\n projectId\n workspaceId\n type\n project {\n ...ProjectDashboardItem\n }\n }\n }\n ": typeof types.OnWorkspaceProjectsUpdateDocument,
"\n fragment WorkspaceHasCustomDataResidency_Workspace on Workspace {\n id\n defaultRegion {\n id\n name\n }\n }\n": typeof types.WorkspaceHasCustomDataResidency_WorkspaceFragmentDoc,
"\n query CheckProjectWorkspaceDataResidency($projectId: String!) {\n project(id: $projectId) {\n id\n workspace {\n ...WorkspaceHasCustomDataResidency_Workspace\n }\n }\n }\n": typeof types.CheckProjectWorkspaceDataResidencyDocument,
@@ -378,8 +381,9 @@ type Documents = {
"\n query WorkspaceSsoCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceSsoStatus_Workspace\n }\n activeUser {\n ...WorkspaceSsoStatus_User\n }\n }\n": typeof types.WorkspaceSsoCheckDocument,
"\n query WorkspaceWizard($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...WorkspaceWizard_Workspace\n }\n }\n": typeof types.WorkspaceWizardDocument,
"\n query WorkspaceWizardRegion {\n serverInfo {\n ...WorkspaceWizardStepRegion_ServerInfo\n }\n }\n": typeof types.WorkspaceWizardRegionDocument,
"\n query DiscoverableWorkspaces_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": typeof types.DiscoverableWorkspaces_ActiveUserDocument,
"\n query DiscoverableWorkspacesRequests_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": typeof types.DiscoverableWorkspacesRequests_ActiveUserDocument,
"\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": typeof types.DiscoverableWorkspacesDocument,
"\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": typeof types.DiscoverableWorkspacesRequestsDocument,
"\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n": typeof types.WorkspacePlanDocument,
"\n subscription onWorkspaceUpdated(\n $workspaceId: String\n $workspaceSlug: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceUpdated(workspaceId: $workspaceId, workspaceSlug: $workspaceSlug) {\n id\n workspace {\n id\n ...WorkspaceProjectList_Workspace\n }\n }\n }\n": typeof types.OnWorkspaceUpdatedDocument,
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": typeof types.LegacyBranchRedirectMetadataDocument,
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": typeof types.LegacyViewerCommitRedirectMetadataDocument,
@@ -395,7 +399,6 @@ type Documents = {
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n role\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": typeof types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n plan {\n status\n name\n }\n }\n": typeof types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests {\n items {\n id\n status\n }\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
@@ -508,6 +511,7 @@ const documents: Documents = {
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersChangeRoleDialog_Workspace on Workspace {\n id\n plan {\n status\n name\n }\n subscription {\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n }\n": types.SettingsWorkspacesMembersChangeRoleDialog_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
@@ -695,6 +699,7 @@ const documents: Documents = {
"\n query SettingsSidebarAutomateFunctions {\n activeUser {\n ...Sidebar_User\n }\n }\n": types.SettingsSidebarAutomateFunctionsDocument,
"\n query SettingsWorkspaceGeneral($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesGeneral_Workspace\n }\n }\n": types.SettingsWorkspaceGeneralDocument,
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
"\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.SettingsWorkspaceBillingNewDocument,
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
@@ -736,6 +741,7 @@ const documents: Documents = {
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": types.DiscoverableList_DiscoverableFragmentDoc,
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": types.DiscoverableList_RequestsFragmentDoc,
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n }\n subscription {\n billingInterval\n }\n }\n": types.WorkspacesPlan_WorkspaceFragmentDoc,
"\n subscription OnWorkspaceProjectsUpdate($slug: String!) {\n workspaceProjectsUpdated(workspaceId: null, workspaceSlug: $slug) {\n projectId\n workspaceId\n type\n project {\n ...ProjectDashboardItem\n }\n }\n }\n ": types.OnWorkspaceProjectsUpdateDocument,
"\n fragment WorkspaceHasCustomDataResidency_Workspace on Workspace {\n id\n defaultRegion {\n id\n name\n }\n }\n": types.WorkspaceHasCustomDataResidency_WorkspaceFragmentDoc,
"\n query CheckProjectWorkspaceDataResidency($projectId: String!) {\n project(id: $projectId) {\n id\n workspace {\n ...WorkspaceHasCustomDataResidency_Workspace\n }\n }\n }\n": types.CheckProjectWorkspaceDataResidencyDocument,
@@ -768,8 +774,9 @@ const documents: Documents = {
"\n query WorkspaceSsoCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceSsoStatus_Workspace\n }\n activeUser {\n ...WorkspaceSsoStatus_User\n }\n }\n": types.WorkspaceSsoCheckDocument,
"\n query WorkspaceWizard($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...WorkspaceWizard_Workspace\n }\n }\n": types.WorkspaceWizardDocument,
"\n query WorkspaceWizardRegion {\n serverInfo {\n ...WorkspaceWizardStepRegion_ServerInfo\n }\n }\n": types.WorkspaceWizardRegionDocument,
"\n query DiscoverableWorkspaces_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": types.DiscoverableWorkspaces_ActiveUserDocument,
"\n query DiscoverableWorkspacesRequests_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": types.DiscoverableWorkspacesRequests_ActiveUserDocument,
"\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": types.DiscoverableWorkspacesDocument,
"\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": types.DiscoverableWorkspacesRequestsDocument,
"\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n": types.WorkspacePlanDocument,
"\n subscription onWorkspaceUpdated(\n $workspaceId: String\n $workspaceSlug: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceUpdated(workspaceId: $workspaceId, workspaceSlug: $workspaceSlug) {\n id\n workspace {\n id\n ...WorkspaceProjectList_Workspace\n }\n }\n }\n": types.OnWorkspaceUpdatedDocument,
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": types.LegacyBranchRedirectMetadataDocument,
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": types.LegacyViewerCommitRedirectMetadataDocument,
@@ -785,7 +792,6 @@ const documents: Documents = {
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n role\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n plan {\n status\n name\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests {\n items {\n id\n status\n }\n totalCount\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
@@ -1224,6 +1230,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesGeneralEditAvata
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1972,6 +1982,10 @@ export function graphql(source: "\n query SettingsWorkspaceGeneral($slug: Strin
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n"): (typeof documents)["\n query SettingsWorkspaceBillingNew($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2136,6 +2150,10 @@ export function graphql(source: "\n fragment DiscoverableList_Requests on User
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n"): (typeof documents)["\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n }\n subscription {\n billingInterval\n }\n }\n"): (typeof documents)["\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n plan {\n status\n createdAt\n name\n }\n subscription {\n billingInterval\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2267,11 +2285,15 @@ export function graphql(source: "\n query WorkspaceWizardRegion {\n serverIn
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query DiscoverableWorkspaces_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n"): (typeof documents)["\n query DiscoverableWorkspaces_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n"];
export function graphql(source: "\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n"): (typeof documents)["\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query DiscoverableWorkspacesRequests_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n"): (typeof documents)["\n query DiscoverableWorkspacesRequests_ActiveUser {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n"];
export function graphql(source: "\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n"): (typeof documents)["\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n"): (typeof documents)["\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2332,10 +2354,6 @@ export function graphql(source: "\n fragment SettingsServerProjects_ProjectColl
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"): (typeof documents)["\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -33,6 +33,15 @@ export const settingsWorkspaceBillingQuery = graphql(`
}
`)
// TODO: Remove old one post-migration
export const settingsWorkspaceBillingQueryNew = graphql(`
query SettingsWorkspaceBillingNew($slug: String!) {
workspaceBySlug(slug: $slug) {
id
}
}
`)
export const settingsWorkspaceBillingCustomerPortalQuery = graphql(`
query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {
workspace(id: $workspaceId) {
@@ -0,0 +1,113 @@
import { graphql } from '~~/lib/common/generated/gql'
import { workspacePlanQuery } from '~~/lib/workspaces/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import {
isNewWorkspacePlan,
PaidWorkspacePlansNew,
UnpaidWorkspacePlans
} from '@speckle/shared'
import {
WorkspacePlanStatuses,
BillingInterval
} from '~/lib/common/generated/gql/graphql'
graphql(`
fragment WorkspacesPlan_Workspace on Workspace {
id
plan {
status
createdAt
name
}
subscription {
billingInterval
}
}
`)
export const useWorkspacePlan = (slug: string) => {
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const { result } = useQuery(
workspacePlanQuery,
() => ({
slug
}),
() => ({
enabled: isBillingIntegrationEnabled
})
)
const subscription = computed(() => result.value?.workspaceBySlug?.subscription)
const plan = computed(() => result.value?.workspaceBySlug?.plan)
const isNewPlan = computed(() =>
isNewWorkspacePlan(result.value?.workspaceBySlug?.plan?.name)
)
const statusIsExpired = computed(
() => plan.value?.status === WorkspacePlanStatuses.Expired
)
const statusIsCanceled = computed(
() => plan.value?.status === WorkspacePlanStatuses.Canceled
)
const statusIsCancelationScheduled = computed(
() => plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
)
const isPurchasablePlan = computed(() =>
Object.values(PaidWorkspacePlansNew).includes(
plan.value?.name as PaidWorkspacePlansNew
)
)
const isActivePlan = computed(
() =>
plan.value?.status === WorkspacePlanStatuses.Valid ||
plan.value?.status === WorkspacePlanStatuses.PaymentFailed ||
plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
)
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
const billingInterval = computed(() => subscription.value?.billingInterval)
const intervalIsYearly = computed(
() => billingInterval.value === BillingInterval.Yearly
)
// TODO: Replace with value from API call, this a placeholder value
const seatPrice = 15
const totalCost = computed(() => {
return isPurchasablePlan.value
? intervalIsYearly.value
? seatPrice * 12
: seatPrice
: 0
})
// TODO: Replace with value from BE once ready
const totalCostFormatted = computed(() => {
return isPurchasablePlan.value
? `£${totalCost.value}`
: isFreePlan.value
? 'Free'
: 'Not applicable'
})
return {
plan,
isNewPlan,
statusIsExpired,
statusIsCanceled,
isPurchasablePlan,
isActivePlan,
billingInterval,
intervalIsYearly,
totalCostFormatted,
statusIsCancelationScheduled
}
}
@@ -125,7 +125,7 @@ export const workspaceWizardRegionQuery = graphql(`
`)
export const discoverableWorkspacesQuery = graphql(`
query DiscoverableWorkspaces_ActiveUser {
query DiscoverableWorkspaces {
activeUser {
id
...DiscoverableList_Discoverable
@@ -134,10 +134,18 @@ export const discoverableWorkspacesQuery = graphql(`
`)
export const discoverableWorkspacesRequestsQuery = graphql(`
query DiscoverableWorkspacesRequests_ActiveUser {
query DiscoverableWorkspacesRequests {
activeUser {
id
...DiscoverableList_Requests
}
}
`)
export const workspacePlanQuery = graphql(`
query WorkspacePlan($slug: String!) {
workspaceBySlug(slug: $slug) {
...WorkspacesPlan_Workspace
}
}
`)
@@ -1,263 +1,11 @@
<template>
<section>
<div class="md:max-w-5xl md:mx-auto pb-6 md:pb-0">
<FormButton
v-if="isWorkspaceNewPlansEnabled && isServerAdmin"
size="lg"
class="!bg-pink-500 !border-pink-700 mb-4"
@click="handleUpgradeClick"
>
𝓒𝓱𝓪𝓷𝓰𝓮 𝓽𝓸 𝓷𝓮𝔀 𝓹𝓵𝓪𝓷 💸
</FormButton>
<SettingsSectionHeader title="Billing" text="Your workspace billing details" />
<template v-if="isBillingIntegrationEnabled">
<div class="flex flex-col gap-y-4 md:gap-y-6">
<BillingAlert
v-if="workspace && workspace?.plan?.status !== WorkspacePlanStatuses.Valid"
:workspace="workspace"
/>
<SettingsSectionHeader title="Billing summary" subheading class="pt-4" />
<div class="border border-outline-3 rounded-lg">
<div
class="grid grid-cols-1 lg:grid-cols-3 divide-y divide-outline-3 lg:divide-y-0 lg:divide-x"
>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryPlanHeading }}
</h3>
<div class="flex gap-x-2">
<p class="text-heading-lg text-foreground">
Workspace
<span class="capitalize">
{{ currentPlan?.name ?? WorkspacePlans.Starter }}
</span>
</p>
<div>
<CommonBadge v-if="showStatusBadge" rounded>TRIAL</CommonBadge>
</div>
</div>
<p v-if="isPurchasablePlan" class="text-body-xs text-foreground-2">
<span v-if="statusIsTrial">
<span class="line-through mr-1">
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month
</span>
Free
</span>
<span
v-else-if="currentPlan?.status === WorkspacePlanStatuses.Expired"
>
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month
</span>
<span v-else>
{{ formatPrice(seatPrice?.[Roles.Workspace.Member]) }} per
seat/month, billed
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'annually'
: 'monthly'
}}
</span>
</p>
</div>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<template v-if="isPurchasablePlan || statusIsTrial">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryBillHeading }}
</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ summaryBillValue }} per
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'year'
: 'month'
}}
</p>
<p class="text-body-xs text-foreground-2 flex gap-x-1 items-center">
{{ summaryBillDescription }}
<InformationCircleIcon
v-tippy="billTooltip"
class="w-4 h-4 text-foreground cursor-pointer"
/>
</p>
</template>
<template v-else>
<h3 class="text-body-xs text-foreground-2 pb-1">Expected bill</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ isAcademiaPlan ? 'Free' : 'Not applicable' }}
</p>
</template>
</div>
<div class="p-5 pt-4 flex flex-col gap-y-1">
<h3 class="text-body-xs text-foreground-2 pb-1">
{{ summaryDateHeading }}
</h3>
<p class="text-heading-lg text-foreground capitalize">
{{ isPurchasablePlan ? nextPaymentDue : 'Never' }}
</p>
<p
v-if="showSummaryDateDescription"
class="text-body-xs text-foreground-2"
>
<span v-if="statusIsTrial">Subscribe before this date</span>
<span v-else>
{{
subscription?.billingInterval === BillingInterval.Yearly
? 'Annual'
: 'Monthly'
}}
billing period
</span>
</p>
</div>
</div>
<div
v-if="isActivePlan && isPurchasablePlan"
class="flex flex-row gap-x-4 p-5 items-center border-t border-outline-3"
>
<div class="text-body-xs gap-y-2 flex-1">
<p class="font-medium text-foreground">Billing portal</p>
<p class="text-foreground-2">
View invoices, edit payment details, and manage your subscription.
</p>
</div>
<FormButton color="outline" @click="billingPortalRedirect(workspace?.id)">
Open billing portal
</FormButton>
</div>
</div>
<template v-if="isPurchasablePlan || statusIsTrial">
<SettingsSectionHeader
:title="pricingTableHeading"
subheading
class="pt-4"
/>
<SettingsWorkspacesBillingPricingTable
:workspace-id="workspace?.id"
:current-plan="currentPlan"
:active-billing-interval="subscription?.billingInterval"
:is-admin="isAdmin"
@on-plan-selected="onPlanSelected"
/>
</template>
</div>
<div v-if="isInvoicedPlan" class="mt-8 text-foreground-2 text-body-xs">
Need help?
<a
class="text-foreground hover:underline"
href="mailto:billing@speckle.systems"
@click="
mixpanel.track('Workspace Support Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
Contact us
</a>
</div>
<div
v-else-if="isPurchasablePlan"
class="mt-8 text-center text-foreground-2 text-body-xs"
>
Need help?
<NuxtLink
class="text-foreground"
:to="guideBillingUrl"
target="_blank"
@click="
mixpanel.track('Workspace Docs Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
<span class="hover:underline">Read the docs</span>
</NuxtLink>
or
<a
class="text-foreground hover:underline"
href="mailto:billing@speckle.systems"
@click="
mixpanel.track('Workspace Support Link Clicked', {
workspace_id: workspace?.id,
plan: currentPlan?.name
})
"
>
contact support
</a>
</div>
<SettingsWorkspacesBillingUpgradeDialog
v-if="selectedPlanName && selectedPlanCycle && workspace?.id"
v-model:open="isUpgradeDialogOpen"
:plan="selectedPlanName"
:billing-interval="selectedPlanCycle"
:workspace-id="workspace.id"
/>
</template>
<template v-else>Coming soon</template>
</div>
</section>
<div>
<SettingsWorkspacesBillingPageNew v-if="isWorkspaceNewPlansEnabled" />
<SettingsWorkspacesBillingPage v-else />
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { graphql } from '~/lib/common/generated/gql'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { settingsWorkspaceBillingQuery } from '~/lib/settings/graphql/queries'
import { useIsBillingIntegrationEnabled } from '~/composables/globals'
import {
WorkspacePlans,
WorkspacePlanStatuses,
BillingInterval,
WorkspacePaymentMethod,
type PaidWorkspacePlans
} from '~/lib/common/generated/gql/graphql'
import { useBillingActions } from '~/lib/billing/composables/actions'
import type { PaidWorkspacePlansOld } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { InformationCircleIcon } from '@heroicons/vue/24/outline'
import { isPaidPlan } from '@/lib/billing/helpers/types'
import { useMixpanel } from '~/lib/core/composables/mp'
import { guideBillingUrl } from '~/lib/common/helpers/route'
import { adminUpdateWorkspacePlanMutation } from '~/lib/billing/graphql/mutations'
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
import { formatPrice } from '~/lib/billing/helpers/prices'
graphql(`
fragment SettingsWorkspacesBilling_Workspace on Workspace {
...BillingAlert_Workspace
id
role
plan {
name
status
createdAt
paymentMethod
}
subscription {
billingInterval
currentBillingCycleEnd
seats {
guest
plan
}
}
team {
items {
id
role
}
}
}
`)
definePageMeta({
layout: 'settings'
})
@@ -266,213 +14,5 @@ useHead({
title: 'Settings | Workspace - Billing'
})
const slug = computed(() => (route.params.slug as string) || '')
const { prices } = useWorkspacePlanPrices()
const { isAdmin: isServerAdmin } = useActiveUser()
const route = useRoute()
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const { result: workspaceResult } = useQuery(
settingsWorkspaceBillingQuery,
() => ({
slug: slug.value
}),
() => ({
enabled: isBillingIntegrationEnabled
})
)
const { billingPortalRedirect, redirectToCheckout } = useBillingActions()
const mixpanel = useMixpanel()
const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation)
const selectedPlanName = ref<PaidWorkspacePlansOld>()
const selectedPlanCycle = ref<BillingInterval>()
const isUpgradeDialogOpen = ref(false)
const seatPrices = computed(() => ({
[WorkspacePlans.Starter]: prices.value?.[WorkspacePlans.Starter],
[WorkspacePlans.Plus]: prices.value?.[WorkspacePlans.Plus],
[WorkspacePlans.Business]: prices.value?.[WorkspacePlans.Business]
}))
const workspace = computed(() => workspaceResult.value?.workspaceBySlug)
const currentPlan = computed(() => workspace.value?.plan)
const subscription = computed(() => workspace.value?.subscription)
const statusIsTrial = computed(
() =>
currentPlan.value?.status === WorkspacePlanStatuses.Trial ||
!currentPlan.value?.status
)
const isActivePlan = computed(
() =>
currentPlan.value &&
currentPlan.value?.status !== WorkspacePlanStatuses.Trial &&
currentPlan.value?.status !== WorkspacePlanStatuses.Canceled &&
currentPlan.value?.status !== WorkspacePlanStatuses.Expired
)
const isAcademiaPlan = computed(
() => currentPlan.value?.name === WorkspacePlans.Academia
)
const isPurchasablePlan = computed(() => isPaidPlan(currentPlan.value?.name))
const seatPrice = computed(() =>
currentPlan.value && subscription.value
? seatPrices.value?.[currentPlan.value.name as keyof typeof seatPrices.value]?.[
subscription.value.billingInterval
]
: seatPrices.value?.[WorkspacePlans.Starter]?.[BillingInterval.Monthly]
)
const nextPaymentDue = computed(() =>
isPurchasablePlan.value
? subscription.value?.currentBillingCycleEnd
? dayjs(subscription.value?.currentBillingCycleEnd).format('MMMM D, YYYY')
: dayjs(currentPlan.value?.createdAt).add(31, 'days').format('MMMM D, YYYY')
: 'Never'
)
const isInvoicedPlan = computed(
() => currentPlan.value?.paymentMethod === WorkspacePaymentMethod.Invoice
)
const isAdmin = computed(() => workspace.value?.role === Roles.Workspace.Admin)
const guestSeatCount = computed(() =>
isActivePlan.value
? workspace.value?.subscription?.seats.guest ?? 0
: workspace.value?.team.items.filter((user) => user.role === Roles.Workspace.Guest)
.length ?? 0
)
const memberSeatCount = computed(() =>
isActivePlan.value
? workspace.value?.subscription?.seats.plan ?? 0
: workspace.value
? workspace.value.team.items.length - guestSeatCount.value
: 0
)
const summaryBillValue = computed(() => {
if (!seatPrice.value) return 'loading'
const guestPrice =
seatPrice.value[Roles.Workspace.Guest].amount * guestSeatCount.value
const memberPrice =
seatPrice.value[Roles.Workspace.Member].amount * memberSeatCount.value
const totalPrice = guestPrice + memberPrice
const isAnnual = subscription.value?.billingInterval === BillingInterval.Yearly
return isPurchasablePlan.value ? `£${isAnnual ? totalPrice * 12 : totalPrice}` : '£0'
})
const summaryBillDescription = computed(() => {
const memberText =
memberSeatCount.value > 1 ? `${memberSeatCount.value} members` : '1 member'
const guestText =
guestSeatCount.value > 1 ? `${guestSeatCount.value} guests` : '1 guest'
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
})
const billTooltip = computed(() => {
if (!seatPrice.value) return undefined
const memberText = `${memberSeatCount.value} member${
memberSeatCount.value === 1 ? '' : 's'
} at ${formatPrice(seatPrice.value[Roles.Workspace.Member])}/month`
const guestText = `${guestSeatCount.value} guest${
guestSeatCount.value === 1 ? '' : 's'
} at ${formatPrice(seatPrice.value[Roles.Workspace.Guest])}/month`
return `${memberText}${guestSeatCount.value > 0 ? `, ${guestText}` : ''}`
})
const summaryPlanHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
return 'Trial plan'
case WorkspacePlanStatuses.Expired:
case WorkspacePlanStatuses.Canceled:
return 'Plan'
default:
return 'Current plan'
}
})
const summaryBillHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
case WorkspacePlanStatuses.Canceled:
return 'Expected bill'
default:
return subscription.value?.billingInterval === BillingInterval.Yearly
? 'Annual bill'
: 'Monthly bill'
}
})
const summaryDateHeading = computed(() => {
if (statusIsTrial.value && isPurchasablePlan.value) {
return 'Trial ends'
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Expired) {
return 'Trial expired at'
} else if (currentPlan.value?.status === WorkspacePlanStatuses.Canceled) {
return 'Cancels'
} else {
return 'Next payment due'
}
})
const showSummaryDateDescription = computed(() => {
return statusIsTrial.value && isPurchasablePlan.value
})
const pricingTableHeading = computed(() => {
switch (currentPlan.value?.status) {
case WorkspacePlanStatuses.Trial:
case WorkspacePlanStatuses.Expired:
return 'Start your subscription'
case WorkspacePlanStatuses.Canceled:
return 'Restart your subscription'
default:
return 'Upgrade your plan'
}
})
const showStatusBadge = computed(() => {
return (
(statusIsTrial.value ||
currentPlan.value?.status === WorkspacePlanStatuses.Expired) &&
isPurchasablePlan.value
)
})
const onPlanSelected = (plan: {
name: PaidWorkspacePlansOld
cycle: BillingInterval
}) => {
const { name, cycle } = plan
if (!isPaidPlan(name) || !workspace.value?.id) return
if (
statusIsTrial.value ||
currentPlan.value?.status === WorkspacePlanStatuses.Expired ||
currentPlan.value?.status === WorkspacePlanStatuses.Canceled
) {
mixpanel.track('Workspace Subscribe Button Clicked', {
plan,
cycle,
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
redirectToCheckout({
plan: name as unknown as PaidWorkspacePlans,
cycle,
workspaceId: workspace.value?.id
})
} else {
selectedPlanName.value = name
selectedPlanCycle.value = cycle
isUpgradeDialogOpen.value = true
}
}
const handleUpgradeClick = () => {
if (!workspace.value?.id) return
// Temporary hack to change workspace plans to the new free plan
mutateWorkspacePlan({
input: {
workspaceId: workspace.value?.id,
plan: WorkspacePlans.Free,
status: WorkspacePlanStatuses.Valid
}
})
}
</script>
@@ -1,3 +1,5 @@
import type { MaybeNullOrUndefined } from '../../core/helpers/utilityTypes.js'
/**
* PLANS
*/
@@ -55,9 +57,21 @@ export const WorkspacePlans = <const>{
export type WorkspacePlans = (typeof WorkspacePlans)[keyof typeof WorkspacePlans]
// TODO: Remove this post workspace migration
export const WorkspaceGuestSeatType = 'guest'
export type WorkspaceGuestSeatType = typeof WorkspaceGuestSeatType
// TODO: Remove this post workspace migration, only needed temporarily to differiante between old and new
export const isNewWorkspacePlan = (
plan: MaybeNullOrUndefined<WorkspacePlans>
): boolean => {
return (
plan === PaidWorkspacePlansNew.Team ||
plan === PaidWorkspacePlansNew.Pro ||
plan === UnpaidWorkspacePlans.Free
)
}
/**
* BILLING INTERVALS
*/