diff --git a/packages/frontend-2/components/pricingTable/Plan.vue b/packages/frontend-2/components/pricingTable/Plan.vue index a1d090349..e8a1774bf 100644 --- a/packages/frontend-2/components/pricingTable/Plan.vue +++ b/packages/frontend-2/components/pricingTable/Plan.vue @@ -5,8 +5,7 @@

- Workspace - {{ plan }} + {{ formatName(plan) }}

{{ badgeText }} @@ -92,10 +91,9 @@ import { WorkspacePlanStatuses, BillingInterval } from '~/lib/common/generated/gql/graphql' -import { startCase } from 'lodash' import { XMarkIcon } from '@heroicons/vue/24/outline' import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices' -import { formatPrice } from '~/lib/billing/helpers/prices' +import { formatPrice, formatName } from '~/lib/billing/helpers/plan' import { useBillingActions } from '~/lib/billing/composables/actions' defineEmits<{ @@ -108,11 +106,12 @@ const props = defineProps<{ currentPlan: MaybeNullOrUndefined isAdmin: boolean activeBillingInterval: MaybeNullOrUndefined + hasSubscription: boolean workspaceId: MaybeNullOrUndefined }>() const { pricesNew } = useWorkspacePlanPrices() -const { upgradePlan } = useBillingActions() +const { upgradePlan, redirectToCheckout } = useBillingActions() const isYearlyIntervalSelected = ref(props.yearlyIntervalSelected) @@ -204,19 +203,20 @@ const isSelectable = computed(() => { }) const buttonColor = computed(() => { - if (props.currentPlan?.status === WorkspacePlanStatuses.Expired) { - return props.plan === WorkspacePlans.Starter ? 'primary' : 'outline' + if (props.currentPlan?.name === WorkspacePlans.Free) { + return props.plan === WorkspacePlans.Pro ? 'primary' : 'outline' } return 'outline' }) const buttonText = computed(() => { - // Allow selection during trial, expired or canceled state + // Allow if current plan is Free, or the current plan is expired/canceled if ( + props.currentPlan?.name === WorkspacePlans.Free || props.currentPlan?.status === WorkspacePlanStatuses.Expired || props.currentPlan?.status === WorkspacePlanStatuses.Canceled ) { - return `Subscribe to ${startCase(props.plan)}` + return `Subscribe to ${formatName(props.plan)}` } // Current plan case if (isCurrentPlan.value) { @@ -234,7 +234,7 @@ const buttonText = computed(() => { return 'Change to annual plan' } // Upgrade case - return canUpgradeToPlan.value ? `Upgrade to ${startCase(props.plan)}` : '' + return canUpgradeToPlan.value ? `Upgrade to ${formatName(props.plan)}` : '' }) const badgeText = computed(() => @@ -243,7 +243,9 @@ const badgeText = computed(() => const handleUpgradeClick = () => { if (!props.workspaceId) return - if (props.plan === WorkspacePlans.Team || props.plan === WorkspacePlans.Pro) { + if (props.plan !== WorkspacePlans.Team && props.plan !== WorkspacePlans.Pro) return + + if (props.hasSubscription) { upgradePlan({ plan: props.plan, cycle: isYearlyIntervalSelected.value @@ -251,6 +253,14 @@ const handleUpgradeClick = () => { : BillingInterval.Monthly, workspaceId: props.workspaceId }) + } else { + redirectToCheckout({ + plan: props.plan, + cycle: isYearlyIntervalSelected.value + ? BillingInterval.Yearly + : BillingInterval.Monthly, + workspaceId: props.workspaceId + }) } } diff --git a/packages/frontend-2/components/pricingTable/PricingTable.vue b/packages/frontend-2/components/pricingTable/PricingTable.vue index 3a80c33ff..ff0131aca 100644 --- a/packages/frontend-2/components/pricingTable/PricingTable.vue +++ b/packages/frontend-2/components/pricingTable/PricingTable.vue @@ -9,6 +9,7 @@ :active-billing-interval="billingInterval" :is-admin="isAdmin" :workspace-id="props.workspaceId" + :has-subscription="!!subscription" @on-yearly-interval-selected="onYearlyIntervalSelected" />
@@ -34,7 +35,11 @@ const props = defineProps<{ workspaceId: MaybeNullOrUndefined }>() -const { billingInterval, plan: currentPlan } = useWorkspacePlan(props.slug) +const { + billingInterval, + plan: currentPlan, + subscription +} = useWorkspacePlan(props.slug) const isYearlySelected = ref(false) diff --git a/packages/frontend-2/components/settings/workspaces/billing/Page.vue b/packages/frontend-2/components/settings/workspaces/billing/Page.vue index 8643e9bbb..0d992f3a8 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/Page.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/Page.vue @@ -229,7 +229,7 @@ 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' +import { formatPrice } from '~/lib/billing/helpers/plan' graphql(` fragment SettingsWorkspacesBilling_Workspace on Workspace { diff --git a/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue b/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue index dd7c9f56f..d14a9ed83 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/PricingTable/Plan.vue @@ -120,7 +120,7 @@ import { startCase, isFunction } from 'lodash' import { XMarkIcon } from '@heroicons/vue/24/outline' import type { SetupContext } from 'vue' import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices' -import { formatPrice } from '~/lib/billing/helpers/prices' +import { formatPrice } from '~/lib/billing/helpers/plan' const emit = defineEmits<{ (e: 'onYearlyIntervalSelected', value: boolean): void diff --git a/packages/frontend-2/components/settings/workspaces/billing/Summary.vue b/packages/frontend-2/components/settings/workspaces/billing/Summary.vue index ad3f1b9b8..89955aaf3 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/Summary.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/Summary.vue @@ -7,7 +7,7 @@

Current plan

- {{ plan?.name }} + {{ formatName(plan?.name) }}

@@ -62,6 +62,7 @@ import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan' import { useBillingActions } from '~/lib/billing/composables/actions' import type { MaybeNullOrUndefined } from '@speckle/shared' +import { formatName } from '~/lib/billing/helpers/plan' defineProps<{ workspaceId?: MaybeNullOrUndefined diff --git a/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue b/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue index 58647bf8c..b51575834 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue @@ -31,7 +31,7 @@ import type { PaidWorkspacePlansOld } from '@speckle/shared' import { Roles } from '@speckle/shared' import { isPaidPlan } from '~/lib/billing/helpers/types' import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices' -import { formatPrice } from '~/lib/billing/helpers/prices' +import { formatPrice } from '~/lib/billing/helpers/plan' const props = defineProps<{ plan: PaidWorkspacePlansOld diff --git a/packages/frontend-2/lib/billing/helpers/plan.ts b/packages/frontend-2/lib/billing/helpers/plan.ts new file mode 100644 index 000000000..404a43689 --- /dev/null +++ b/packages/frontend-2/lib/billing/helpers/plan.ts @@ -0,0 +1,29 @@ +import { isInteger } from 'lodash-es' +import { WorkspacePlans } from '@speckle/shared' + +export const formatPrice = (price?: { amount: number; currencySymbol: string }) => { + if (!price) return '' + return `${price.currencySymbol}${ + isInteger(price.amount) ? price.amount : price.amount.toFixed(2) + }` +} + +// Internal plan names dont match the names we use in the product +export const formatName = (plan?: WorkspacePlans) => { + if (!plan) return '' + + const formattedPlanNames: Record = { + [WorkspacePlans.Unlimited]: 'Unlimited', + [WorkspacePlans.Academia]: 'Academia', + [WorkspacePlans.StarterInvoiced]: 'Starter (invoiced)', + [WorkspacePlans.PlusInvoiced]: 'Plus (Invoiced)', + [WorkspacePlans.BusinessInvoiced]: 'Business (Invoiced)', + [WorkspacePlans.Starter]: 'Starter', + [WorkspacePlans.Plus]: 'Plus', + [WorkspacePlans.Business]: 'Business', + [WorkspacePlans.Free]: 'Free', + [WorkspacePlans.Team]: 'Starter', + [WorkspacePlans.Pro]: 'Business' + } + return formattedPlanNames[plan] +} diff --git a/packages/frontend-2/lib/billing/helpers/prices.ts b/packages/frontend-2/lib/billing/helpers/prices.ts deleted file mode 100644 index 961d2efe6..000000000 --- a/packages/frontend-2/lib/billing/helpers/prices.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isInteger } from 'lodash-es' - -export const formatPrice = (price?: { amount: number; currencySymbol: string }) => { - if (!price) return '' - return `${price.currencySymbol}${ - isInteger(price.amount) ? price.amount : price.amount.toFixed(2) - }` -} diff --git a/packages/frontend-2/lib/workspaces/composables/plan.ts b/packages/frontend-2/lib/workspaces/composables/plan.ts index a54460a0d..993848a9d 100644 --- a/packages/frontend-2/lib/workspaces/composables/plan.ts +++ b/packages/frontend-2/lib/workspaces/composables/plan.ts @@ -78,7 +78,6 @@ export const useWorkspacePlan = (slug: string) => { const intervalIsYearly = computed( () => billingInterval.value === BillingInterval.Yearly ) - // TODO: Replace with value from API call, this a placeholder value const seatPrice = 15 @@ -109,6 +108,7 @@ export const useWorkspacePlan = (slug: string) => { billingInterval, intervalIsYearly, totalCostFormatted, - statusIsCancelationScheduled + statusIsCancelationScheduled, + subscription } } diff --git a/packages/shared/src/workspaces/helpers/plans.ts b/packages/shared/src/workspaces/helpers/plans.ts index 1e5050751..481f0c40f 100644 --- a/packages/shared/src/workspaces/helpers/plans.ts +++ b/packages/shared/src/workspaces/helpers/plans.ts @@ -38,12 +38,12 @@ export type PaidWorkspacePlans = export const UnpaidWorkspacePlans = { // Old - Unlimited: 'unlimited', - Academia: 'academia', StarterInvoiced: 'starterInvoiced', PlusInvoiced: 'plusInvoiced', BusinessInvoiced: 'businessInvoiced', // New + Unlimited: 'unlimited', + Academia: 'academia', Free: 'free' }