Feat: Update add-on cards (#4382)

This commit is contained in:
Mike
2025-04-11 13:28:12 +02:00
committed by GitHub
parent 5358db9815
commit 080482febd
6 changed files with 171 additions and 53 deletions
@@ -1,47 +0,0 @@
<!-- TODO: Implement final values, links and copy -->
<template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
<CommonCard class="flex flex-col gap-y-3 p-6">
<h3 class="text-body font-medium">Unlimited projects and models</h3>
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
Add more projects and models to your workspace.
</p>
<FormButton color="outline" size="sm">Contact us</FormButton>
</CommonCard>
<CommonCard class="flex flex-col gap-y-3 p-6">
<h3 class="text-body font-medium">Extra data regions</h3>
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
Access to almost all data residency regions.
</p>
<FormButton v-if="planIsBusiness" color="outline" size="sm">
Contact us
</FormButton>
<p v-else class="font-medium text-body-xs">Available only on Business plan</p>
</CommonCard>
<CommonCard class="flex flex-col gap-y-3 p-6">
<h3 class="text-body font-medium">Priority support</h3>
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
Private support channel for your workspace.
</p>
<FormButton v-if="planIsBusiness" color="outline" size="sm">
Contact us
</FormButton>
<p v-else class="font-medium text-body-xs">Available only on Business plan</p>
</CommonCard>
</div>
</template>
<script lang="ts" setup>
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
import { PaidWorkspacePlansNew } from '@speckle/shared'
const props = defineProps<{
slug: string
}>()
const { plan } = useWorkspacePlan(props.slug)
const planIsBusiness = computed(() => plan.value?.name === PaidWorkspacePlansNew.Pro)
</script>
@@ -0,0 +1,85 @@
<template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
<SettingsWorkspacesBillingAddOnsCard
title="Unlimited projects and models"
info="Add unlimited projects and models to your workspace."
disclaimer="Only on Starter & Business plans"
:buttons="[unlimitedAddOnButton]"
>
<template #subtitle>
<p class="text-body pt-1">
<span class="font-medium">$0</span>
per editor/month
</p>
<div class="flex items-center gap-x-2 mt-3 px-1">
<FormSwitch
v-model="isYearlyIntervalSelected"
:show-label="false"
name="billing-interval"
/>
<span class="text-body-2xs">Billed yearly</span>
<CommonBadge rounded color-classes="text-foreground-2 bg-primary-muted">
-10%
</CommonBadge>
</div>
</template>
</SettingsWorkspacesBillingAddOnsCard>
<SettingsWorkspacesBillingAddOnsCard
title="Extra data regions"
subtitle="Talk to us"
info="Access to almost all data residency regions."
disclaimer="Only on Business plan"
:buttons="[contactButton]"
/>
<SettingsWorkspacesBillingAddOnsCard
title="Priority support"
subtitle="Talk to us"
info="Private support channel for your workspace."
disclaimer="Only on Business plan"
:buttons="[contactButton]"
/>
</div>
</template>
<script lang="ts" setup>
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
import { isPaidPlan } from '@speckle/shared'
const props = defineProps<{
slug: string
}>()
const isYearlyIntervalSelected = ref(false)
const { billingInterval, isBusinessPlan, plan } = useWorkspacePlan(props.slug)
const contactButton = computed(() => ({
text: 'Contact us',
id: 'contact-us',
disabled: !isBusinessPlan.value,
disabledMessage: 'Only available on the Business plan',
onClick: () => {
// TODO: Implement contact us
}
}))
const unlimitedAddOnButton = computed(() => ({
text: 'Buy add-on',
id: 'buy-add-on',
disabled: plan.value?.name ? !isPaidPlan(plan.value.name) : false,
disabledMessage: 'Only available on Starter and Business plans',
onClick: () => {
// TODO: Implement checkout
}
}))
watch(
() => billingInterval.value,
(newVal) => {
isYearlyIntervalSelected.value = newVal === BillingInterval.Yearly
},
{ immediate: true }
)
</script>
@@ -0,0 +1,45 @@
<template>
<CommonCard class="flex flex-col gap-y-4 p-6">
<div class="flex flex-col">
<h3 class="text-body font-medium">{{ title }}</h3>
<p v-if="subtitle" class="text-foreground-3 text-body-sm pt-1">{{ subtitle }}</p>
<slot name="subtitle" />
</div>
<p class="flex-1 mb-3">
<span v-if="info" class="text-foreground-2 text-body-xs">{{ info }}</span>
<slot name="info" />
</p>
<div class="flex items-center gap-x-2">
<div
v-for="(button, index) in buttons"
:key="button.id || index"
v-tippy="button.disabledMessage"
>
<FormButton
v-bind="button.props || {}"
:disabled="button.props?.disabled || button.disabled"
size="sm"
color="outline"
@click="($event) => button.onClick?.($event)"
>
{{ button.text }}
</FormButton>
</div>
<p v-if="disclaimer" class="font-medium text-foreground-2 text-body-2xs">
{{ disclaimer }}
</p>
</div>
</CommonCard>
</template>
<script lang="ts" setup>
import type { LayoutDialogButton } from '@speckle/ui-components'
defineProps<{
title: string
subtitle?: string
info?: string
buttons?: LayoutDialogButton[]
disclaimer?: string
}>()
</script>
@@ -62,6 +62,11 @@ export const useWorkspacePlan = (slug: string) => {
const plan = computed(() => result.value?.workspaceBySlug?.plan)
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
const isBusinessPlan = computed(
() =>
plan.value?.name === PaidWorkspacePlansNew.Pro ||
plan.value?.name === PaidWorkspacePlansNew.ProUnlimited
)
const isUnlimitedPlan = computed(
() => plan.value?.name === UnpaidWorkspacePlans.Unlimited
)
@@ -129,6 +134,7 @@ export const useWorkspacePlan = (slug: string) => {
seats,
hasAvailableEditorSeats,
editorSeatPriceFormatted,
isUnlimitedPlan
isUnlimitedPlan,
isBusinessPlan
}
}
@@ -48,11 +48,15 @@ const buildWorkspaceSUT = (
getWorkspaceSsoProvider: async () => ({
providerId: 'provider-id'
}),
getWorkspaceSsoSession: async () => ({
userId: 'user-id',
providerId: 'provider-id',
validUntil: new Date()
}),
getWorkspaceSsoSession: async () => {
const validUntil = new Date()
validUntil.setDate(validUntil.getDate() + 7)
return {
userId: 'user-id',
providerId: 'provider-id',
validUntil
}
},
...overrides
})
@@ -146,6 +146,31 @@ export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => {
}
}
export const isPaidPlan = (plan: WorkspacePlans): boolean => {
switch (plan) {
case 'team':
case 'teamUnlimited':
case 'pro':
case 'proUnlimited':
return true
case 'free':
case 'starter':
case 'plus':
case 'business':
case 'starterInvoiced':
case 'plusInvoiced':
case 'businessInvoiced':
case 'teamUnlimitedInvoiced':
case 'proUnlimitedInvoiced':
case 'unlimited':
case 'academia':
return false
default:
throwUncoveredError(plan)
}
}
/**
* BILLING INTERVALS
*/