From a3ce9fad12a3c8e664dc5d1af8e66e3fef9b2d54 Mon Sep 17 00:00:00 2001 From: Mike Date: Wed, 9 Apr 2025 21:45:28 +0200 Subject: [PATCH] Feat: Update pricing plan features (#4357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Update pricing plan features * Clean up old stuff * Add features to old plans * pls dont complain anymore * pls work * fix(shared): add back features to the unlimited plan --------- Co-authored-by: Gergő Jedlicska --- .../components/pricingTable/Plan.vue | 60 +++++++----- .../components/pricingTable/PlanFeature.vue | 29 ++++++ .../settings/workspaces/billing/Page.vue | 50 +++++----- .../workspaces/billing/UpgradeDialog.vue | 5 +- .../lib/billing/helpers/constants.ts | 48 ---------- .../frontend-2/lib/billing/helpers/types.ts | 6 -- .../lib/common/generated/gql/gql.ts | 36 +++---- .../lib/common/generated/gql/graphql.ts | 12 +-- .../lib/workspaces/composables/plan.ts | 6 -- .../requireDiscoverableWorkspaces.ts | 3 +- .../settings/workspaces/[slug]/general.vue | 17 ++-- .../core/tests/unit/services/versions.spec.ts | 4 +- .../shared/src/workspaces/helpers/features.ts | 94 +++++++------------ 13 files changed, 156 insertions(+), 214 deletions(-) create mode 100644 packages/frontend-2/components/pricingTable/PlanFeature.vue delete mode 100644 packages/frontend-2/lib/billing/helpers/constants.ts delete mode 100644 packages/frontend-2/lib/billing/helpers/types.ts diff --git a/packages/frontend-2/components/pricingTable/Plan.vue b/packages/frontend-2/components/pricingTable/Plan.vue index 8184bca5f..1c7cf3864 100644 --- a/packages/frontend-2/components/pricingTable/Plan.vue +++ b/packages/frontend-2/components/pricingTable/Plan.vue @@ -56,29 +56,45 @@ @@ -96,7 +112,6 @@ import { WorkspacePlanStatuses, BillingInterval } from '~/lib/common/generated/gql/graphql' -import { XMarkIcon } from '@heroicons/vue/24/outline' import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices' import { formatPrice, formatName } from '~/lib/billing/helpers/plan' import { useBillingActions } from '~/lib/billing/composables/actions' @@ -122,6 +137,7 @@ const { upgradePlan, redirectToCheckout } = useBillingActions() const isYearlyIntervalSelected = ref(props.yearlyIntervalSelected) +const planLimits = computed(() => WorkspacePlanConfigs[props.plan].limits) const planFeatures = computed(() => WorkspacePlanConfigs[props.plan].features) const planPrice = computed(() => { if (props.plan === WorkspacePlans.Team || props.plan === WorkspacePlans.Pro) { diff --git a/packages/frontend-2/components/pricingTable/PlanFeature.vue b/packages/frontend-2/components/pricingTable/PlanFeature.vue new file mode 100644 index 000000000..64d9e3749 --- /dev/null +++ b/packages/frontend-2/components/pricingTable/PlanFeature.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/frontend-2/components/settings/workspaces/billing/Page.vue b/packages/frontend-2/components/settings/workspaces/billing/Page.vue index c677411d1..5466afa7c 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/Page.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/Page.vue @@ -4,12 +4,6 @@ title="Billing and plans" text="Update your payment information or switch plans according to your needs" /> - - - -
@@ -36,31 +30,29 @@
- +
+ + +
@@ -88,7 +80,7 @@ const route = useRoute() const slug = computed(() => (route.params.slug as string) || '') const { isAdmin: isServerAdmin } = useActiveUser() const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled() -const { isPurchasablePlan, isNewPlan } = useWorkspacePlan(slug.value) +const { isPurchasablePlan } = useWorkspacePlan(slug.value) const { mutate: mutateWorkspacePlan } = useMutation(adminUpdateWorkspacePlanMutation) const { result: workspaceResult } = useQuery( settingsWorkspaceBillingQuery, diff --git a/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue b/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue index c1edf0836..a30d01e23 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/UpgradeDialog.vue @@ -27,8 +27,7 @@ import { } from '~/lib/common/generated/gql/graphql' import { useBillingActions } from '~/lib/billing/composables/actions' import { startCase } from 'lodash' -import type { PaidWorkspacePlansOld } from '@speckle/shared' -import { isPaidPlan } from '~/lib/billing/helpers/types' +import { type PaidWorkspacePlansOld, isSelfServeAvailablePlan } from '@speckle/shared' import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices' import { formatPrice } from '~/lib/billing/helpers/plan' @@ -43,7 +42,7 @@ const { upgradePlan } = useBillingActions() const { prices } = useWorkspacePlanPrices() const seatPrice = computed(() => { - if (isPaidPlan(props.plan)) { + if (isSelfServeAvailablePlan(props.plan)) { const planPrices = prices.value?.[props.plan] const price = planPrices?.[props.billingInterval] diff --git a/packages/frontend-2/lib/billing/helpers/constants.ts b/packages/frontend-2/lib/billing/helpers/constants.ts deleted file mode 100644 index 2754d67de..000000000 --- a/packages/frontend-2/lib/billing/helpers/constants.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - PaidWorkspacePlansOld, - Roles, - WorkspacePlanBillingIntervals, - type WorkspacePlanPriceStructure -} from '@speckle/shared' - -// TODO: Read these from API, especially for new plans -export const WorkspaceOldPaidPlanPrices: { - [plan in PaidWorkspacePlansOld]: WorkspacePlanPriceStructure -} = { - [PaidWorkspacePlansOld.Starter]: { - [WorkspacePlanBillingIntervals.Monthly]: { - [Roles.Workspace.Guest]: 15, - [Roles.Workspace.Member]: 15, - [Roles.Workspace.Admin]: 15 - }, - [WorkspacePlanBillingIntervals.Yearly]: { - [Roles.Workspace.Guest]: 12, - [Roles.Workspace.Member]: 12, - [Roles.Workspace.Admin]: 12 - } - }, - [PaidWorkspacePlansOld.Plus]: { - [WorkspacePlanBillingIntervals.Monthly]: { - [Roles.Workspace.Guest]: 15, - [Roles.Workspace.Member]: 50, - [Roles.Workspace.Admin]: 50 - }, - [WorkspacePlanBillingIntervals.Yearly]: { - [Roles.Workspace.Guest]: 12, - [Roles.Workspace.Member]: 40, - [Roles.Workspace.Admin]: 40 - } - }, - [PaidWorkspacePlansOld.Business]: { - [WorkspacePlanBillingIntervals.Monthly]: { - [Roles.Workspace.Guest]: 15, - [Roles.Workspace.Member]: 75, - [Roles.Workspace.Admin]: 75 - }, - [WorkspacePlanBillingIntervals.Yearly]: { - [Roles.Workspace.Guest]: 12, - [Roles.Workspace.Member]: 60, - [Roles.Workspace.Admin]: 60 - } - } -} diff --git a/packages/frontend-2/lib/billing/helpers/types.ts b/packages/frontend-2/lib/billing/helpers/types.ts deleted file mode 100644 index 26a3432c9..000000000 --- a/packages/frontend-2/lib/billing/helpers/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { WorkspacePlans } from '@speckle/shared' -import { PaidWorkspacePlans } from '@speckle/shared' - -// Check if the plan matches PaidWorkspacePlans -export const isPaidPlan = (plan?: WorkspacePlans): boolean => - plan ? (Object.values(PaidWorkspacePlans) as string[]).includes(plan) : false diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index b718dfa07..73681ed84 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -45,14 +45,14 @@ type Documents = { "\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc, "\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc, "\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc, + "\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc, + "\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc, + "\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc, "\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": typeof types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc, "\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc, - "\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc, - "\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc, - "\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc, "\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc, "\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc, "\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc, @@ -454,14 +454,14 @@ const documents: Documents = { "\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc, "\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc, "\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc, + "\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc, + "\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc, + "\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc, "\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc, "\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc, "\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc, - "\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc, - "\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc, - "\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc, "\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc, "\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc, "\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc, @@ -970,6 +970,18 @@ export function graphql(source: "\n fragment FormSelectProjects_Project on Proj * 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 FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\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 HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\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 HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\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 HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -990,18 +1002,6 @@ export function graphql(source: "\n fragment HeaderWorkspaceSwitcherHeaderExpir * 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 HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\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 HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\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 HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\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 HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 09f46c01f..40097b20b 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -5098,6 +5098,12 @@ export type FormSelectProjects_ProjectFragment = { __typename?: 'Project', id: s export type FormUsersSelectItemFragment = { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }; +export type HeaderNavShare_ProjectFragment = { __typename?: 'Project', id: string, visibility: SimpleProjectVisibility, role?: string | null }; + +export type HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragment = { __typename?: 'PendingStreamCollaborator', id: string, projectId: string, projectName: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, user?: { __typename?: 'LimitedUser', id: string } | null }; + +export type HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragment = { __typename?: 'PendingWorkspaceCollaborator', id: string, workspaceId: string, workspaceName: string, token?: string | null, workspaceSlug: string, invitedBy: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, user?: { __typename?: 'LimitedUser', id: string } | null }; + export type HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, logo?: string | null, role?: string | null, domainBasedMembershipProtectionEnabled: boolean, plan?: { __typename?: 'WorkspacePlan', name: WorkspacePlans } | null, team: { __typename?: 'WorkspaceCollaboratorCollection', totalCount: number }, domains?: Array<{ __typename?: 'WorkspaceDomain', domain: string, id: string }> | null }; export type HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, logo?: string | null, role?: string | null, slug: string, creationState?: { __typename?: 'WorkspaceCreationState', completed: boolean } | null, plan?: { __typename?: 'WorkspacePlan', name: WorkspacePlans } | null }; @@ -5108,12 +5114,6 @@ export type HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragment = { export type HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, logo?: string | null, role?: string | null, domainBasedMembershipProtectionEnabled: boolean, plan?: { __typename?: 'WorkspacePlan', name: WorkspacePlans } | null, team: { __typename?: 'WorkspaceCollaboratorCollection', totalCount: number }, domains?: Array<{ __typename?: 'WorkspaceDomain', domain: string, id: string }> | null }; -export type HeaderNavShare_ProjectFragment = { __typename?: 'Project', id: string, visibility: SimpleProjectVisibility, role?: string | null }; - -export type HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragment = { __typename?: 'PendingStreamCollaborator', id: string, projectId: string, projectName: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, user?: { __typename?: 'LimitedUser', id: string } | null }; - -export type HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragment = { __typename?: 'PendingWorkspaceCollaborator', id: string, workspaceId: string, workspaceName: string, token?: string | null, workspaceSlug: string, invitedBy: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, user?: { __typename?: 'LimitedUser', id: string } | null }; - export type InviteDialogWorkspace_WorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, domainBasedMembershipProtectionEnabled: boolean, domains?: Array<{ __typename?: 'WorkspaceDomain', domain: string, id: string }> | null }; export type InviteDialogProject_ProjectFragment = { __typename?: 'Project', id: string, name: string, role?: string | null, workspace?: { __typename?: 'Workspace', id: string, name: string, role?: string | null, domainBasedMembershipProtectionEnabled: boolean, domains?: Array<{ __typename?: 'WorkspaceDomain', domain: string, id: string }> | null, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', role: string, id: string, user: { __typename?: 'LimitedUser', id: string, name: string, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null, role?: string | null } }> } } | null, invitedTeam?: Array<{ __typename?: 'PendingStreamCollaborator', id: string, title: string, role: string, inviteId: string, user?: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } | null }> | null, team: Array<{ __typename?: 'ProjectCollaborator', role: string, seatType?: WorkspaceSeatType | null, user: { __typename?: 'LimitedUser', id: string, role?: string | null, name: string, avatar?: string | null } }> }; diff --git a/packages/frontend-2/lib/workspaces/composables/plan.ts b/packages/frontend-2/lib/workspaces/composables/plan.ts index 2ee2bc885..97200dfd2 100644 --- a/packages/frontend-2/lib/workspaces/composables/plan.ts +++ b/packages/frontend-2/lib/workspaces/composables/plan.ts @@ -2,7 +2,6 @@ import { graphql } from '~~/lib/common/generated/gql' import { workspacePlanQuery } from '~~/lib/workspaces/graphql/queries' import { useQuery } from '@vue/apollo-composable' import { - isNewWorkspacePlan, PaidWorkspacePlansNew, UnpaidWorkspacePlans, WorkspacePlans, @@ -62,10 +61,6 @@ export const useWorkspacePlan = (slug: string) => { const subscription = computed(() => result.value?.workspaceBySlug?.subscription) const plan = computed(() => result.value?.workspaceBySlug?.plan) - // Plan type information - const isNewPlan = computed(() => - isNewWorkspacePlan(result.value?.workspaceBySlug?.plan?.name) - ) const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free) const isUnlimitedPlan = computed( () => plan.value?.name === UnpaidWorkspacePlans.Unlimited @@ -117,7 +112,6 @@ export const useWorkspacePlan = (slug: string) => { return { plan, - isNewPlan, statusIsExpired, statusIsCanceled, isPurchasablePlan, diff --git a/packages/frontend-2/middleware/requireDiscoverableWorkspaces.ts b/packages/frontend-2/middleware/requireDiscoverableWorkspaces.ts index 35836ca16..0b7b4c4e0 100644 --- a/packages/frontend-2/middleware/requireDiscoverableWorkspaces.ts +++ b/packages/frontend-2/middleware/requireDiscoverableWorkspaces.ts @@ -8,7 +8,6 @@ import { homeRoute, workspaceCreateRoute } from '~~/lib/common/helpers/route' */ export default defineNuxtRouteMiddleware(async (to) => { const isWorkspacesEnabled = useIsWorkspacesEnabled() - const isNewPlansEnabled = useWorkspaceNewPlansEnabled() if (!isWorkspacesEnabled.value) return @@ -31,7 +30,7 @@ export default defineNuxtRouteMiddleware(async (to) => { return navigateTo(workspaceCreateRoute()) } - if (isNewPlansEnabled && isMemberOfWorkspace) { + if (isMemberOfWorkspace) { return navigateTo(homeRoute) } }) diff --git a/packages/frontend-2/pages/settings/workspaces/[slug]/general.vue b/packages/frontend-2/pages/settings/workspaces/[slug]/general.vue index 95f07f223..9a0449a99 100644 --- a/packages/frontend-2/pages/settings/workspaces/[slug]/general.vue +++ b/packages/frontend-2/pages/settings/workspaces/[slug]/general.vue @@ -149,7 +149,6 @@ import { Roles } from '@speckle/shared' import { workspaceRoute } from '~/lib/common/helpers/route' import { useRoute } from 'vue-router' import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql' -import { isPaidPlan } from '~/lib/billing/helpers/types' import { useWorkspaceSsoStatus } from '~/lib/workspaces/composables/sso' graphql(` @@ -217,15 +216,13 @@ const canDeleteWorkspace = computed( !needsSsoLogin.value && (!isBillingIntegrationEnabled || !( - ( - [ - WorkspacePlanStatuses.Valid, - WorkspacePlanStatuses.PaymentFailed, - WorkspacePlanStatuses.CancelationScheduled - ] as string[] - ).includes( - workspaceResult.value?.workspaceBySlug?.plan?.status as WorkspacePlanStatuses - ) && isPaidPlan(workspaceResult.value?.workspaceBySlug?.plan?.name) + [ + WorkspacePlanStatuses.Valid, + WorkspacePlanStatuses.PaymentFailed, + WorkspacePlanStatuses.CancelationScheduled + ] as string[] + ).includes( + workspaceResult.value?.workspaceBySlug?.plan?.status as WorkspacePlanStatuses )) ) const deleteWorkspaceTooltip = computed(() => { diff --git a/packages/server/modules/core/tests/unit/services/versions.spec.ts b/packages/server/modules/core/tests/unit/services/versions.spec.ts index 827a9e913..316a2ead1 100644 --- a/packages/server/modules/core/tests/unit/services/versions.spec.ts +++ b/packages/server/modules/core/tests/unit/services/versions.spec.ts @@ -22,7 +22,7 @@ describe('Module @core', () => { }) it('should return null if version is outside of workspace limit', async () => { const getWorkspaceLimits = (() => ({ - versionsHistory: { value: 1, unit: 'week' } + versionsHistory: { value: 7, unit: 'day' } })) as unknown as GetWorkspaceLimits const project = { workspaceId: createRandomString() } const tenDaysAgo = new Date() @@ -40,7 +40,7 @@ describe('Module @core', () => { }) it('should return version referencedObject if version is inside of workspace limit', async () => { const getWorkspaceLimits = (() => ({ - versionsHistory: { value: 1, unit: 'week' } + versionsHistory: { value: 7, unit: 'day' } })) as unknown as GetWorkspaceLimits const project = { workspaceId: createRandomString() } const twoDaysAgo = new Date() diff --git a/packages/shared/src/workspaces/helpers/features.ts b/packages/shared/src/workspaces/helpers/features.ts index 06727f38d..f61799b6a 100644 --- a/packages/shared/src/workspaces/helpers/features.ts +++ b/packages/shared/src/workspaces/helpers/features.ts @@ -7,21 +7,16 @@ import { WorkspacePlans } from './plans.js' -type StringTemplate = (data: Data) => string - /** * WORKSPACE FEATURES */ export const WorkspacePlanFeatures = { // Core features pretty much available to everyone - Workspace: 'workspace', - RoleManagement: 'roleManagement', - GuestUsers: 'guestUsers', - PrivateAutomateFunctions: 'privateAutomateFunctions', + AutomateBeta: 'automateBeta', + DomainDiscoverability: 'domainDiscoverability', // Optional/plan specific DomainSecurity: 'domainBasedSecurityPolicies', - PrioritySupport: 'prioritySupport', SSO: 'oidcSso', CustomDataRegion: 'workspaceDataRegionSpecificity' } @@ -30,24 +25,13 @@ export type WorkspacePlanFeatures = (typeof WorkspacePlanFeatures)[keyof typeof WorkspacePlanFeatures] export const WorkspacePlanFeaturesMetadata = ({ - // Old - [WorkspacePlanFeatures.Workspace]: { - displayName: 'Workspace', - description: 'A shared space for your team and projects' + [WorkspacePlanFeatures.AutomateBeta]: { + displayName: 'Automate beta access', + description: 'Some automate text' }, - [WorkspacePlanFeatures.RoleManagement]: { - displayName: 'Role management', - description: "Control individual members' access and edit rights" - }, - [WorkspacePlanFeatures.GuestUsers]: { - displayName: 'Guest users', - description: (params: { price: number | string }) => - `Give guests access to specific projects in the workspace at ${params.price}/month/guest` - }, - [WorkspacePlanFeatures.PrivateAutomateFunctions]: { - displayName: 'Private automate functions', - description: - 'Create and manage private automation functions securely within your workspace' + [WorkspacePlanFeatures.DomainDiscoverability]: { + displayName: 'Domain discoverability', + description: 'Some domain discoverability text' }, [WorkspacePlanFeatures.DomainSecurity]: { displayName: 'Domain security', @@ -60,17 +44,12 @@ export const WorkspacePlanFeaturesMetadata = ({ [WorkspacePlanFeatures.CustomDataRegion]: { displayName: 'Custom data residency', description: 'Store the workspace data in a custom region' - }, - [WorkspacePlanFeatures.PrioritySupport]: { - displayName: 'Priority support', - description: 'Personal and fast support' } }) satisfies Record< WorkspacePlanFeatures, { displayName: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - description: string | StringTemplate + description: string } > @@ -97,63 +76,46 @@ export type WorkspacePlanConfig = } const baseFeatures = [ - WorkspacePlanFeatures.Workspace, - WorkspacePlanFeatures.RoleManagement, - WorkspacePlanFeatures.GuestUsers, - WorkspacePlanFeatures.PrivateAutomateFunctions + WorkspacePlanFeatures.AutomateBeta, + WorkspacePlanFeatures.DomainDiscoverability ] as const -const teamFeatures = [...baseFeatures] - -const proFeatures = [ - ...teamFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.PrioritySupport -] - export const WorkspacePaidPlanConfigs: { [plan in PaidWorkspacePlans]: WorkspacePlanConfig } = { // Old [PaidWorkspacePlans.Starter]: { plan: PaidWorkspacePlans.Starter, - features: [...baseFeatures, WorkspacePlanFeatures.DomainSecurity], + features: [...baseFeatures], limits: unlimited }, [PaidWorkspacePlans.Plus]: { plan: PaidWorkspacePlans.Plus, - features: [ - ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, - WorkspacePlanFeatures.SSO - ], + features: [...baseFeatures, WorkspacePlanFeatures.SSO], limits: unlimited }, [PaidWorkspacePlans.Business]: { plan: PaidWorkspacePlans.Business, features: [ ...baseFeatures, - WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.PrioritySupport + WorkspacePlanFeatures.CustomDataRegion ], limits: unlimited }, [PaidWorkspacePlans.Team]: { plan: PaidWorkspacePlans.Team, - features: teamFeatures, + features: [...baseFeatures], limits: { projectCount: 5, modelCount: 25, versionsHistory: { value: 30, unit: 'day' } } }, + // New [PaidWorkspacePlans.TeamUnlimited]: { plan: PaidWorkspacePlans.TeamUnlimited, - features: teamFeatures, + features: [...baseFeatures], limits: { projectCount: null, modelCount: null, @@ -162,7 +124,12 @@ export const WorkspacePaidPlanConfigs: { }, [PaidWorkspacePlans.Pro]: { plan: PaidWorkspacePlans.Pro, - features: proFeatures, + features: [ + ...baseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion + ], limits: { projectCount: 10, modelCount: 50, @@ -171,7 +138,12 @@ export const WorkspacePaidPlanConfigs: { }, [PaidWorkspacePlans.ProUnlimited]: { plan: PaidWorkspacePlans.ProUnlimited, - features: proFeatures, + features: [ + ...baseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion + ], limits: { projectCount: null, modelCount: null, @@ -190,8 +162,7 @@ export const WorkspaceUnpaidPlanConfigs: { ...baseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.PrioritySupport + WorkspacePlanFeatures.CustomDataRegion ], limits: unlimited }, @@ -201,8 +172,7 @@ export const WorkspaceUnpaidPlanConfigs: { ...baseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, - WorkspacePlanFeatures.CustomDataRegion, - WorkspacePlanFeatures.PrioritySupport + WorkspacePlanFeatures.CustomDataRegion ], limits: unlimited }, @@ -233,7 +203,7 @@ export const WorkspaceUnpaidPlanConfigs: { limits: { projectCount: 1, modelCount: 5, - versionsHistory: { value: 1, unit: 'week' } + versionsHistory: { value: 7, unit: 'day' } } } }