diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 7cb42f737fa..bcdfe78e5ab 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1597,6 +1597,24 @@ export enum EngineComponentKey { USE_AS_DRAFT_WORKFLOW_VERSION = 'USE_AS_DRAFT_WORKFLOW_VERSION' } +export type EnterpriseLicenseInfoDto = { + __typename?: 'EnterpriseLicenseInfoDTO'; + expiresAt?: Maybe; + isValid: Scalars['Boolean']; + licensee?: Maybe; + subscriptionId?: Maybe; +}; + +export type EnterpriseSubscriptionStatusDto = { + __typename?: 'EnterpriseSubscriptionStatusDTO'; + cancelAt?: Maybe; + currentPeriodEnd?: Maybe; + expiresAt?: Maybe; + isCancellationScheduled: Scalars['Boolean']; + licensee?: Maybe; + status: Scalars['String']; +}; + export type EventLogDateRangeInput = { end?: InputMaybe; start?: InputMaybe; @@ -2483,6 +2501,7 @@ export type Mutation = { initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning; installApplication: Scalars['Boolean']; installMarketplaceApp: Scalars['Boolean']; + refreshEnterpriseValidityToken: Scalars['Boolean']; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewApplicationToken: ApplicationTokenPair; @@ -2497,6 +2516,7 @@ export type Mutation = { saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess; sendInvitations: SendInvitations; setAdminAiModelEnabled: Scalars['Boolean']; + setEnterpriseKey: EnterpriseLicenseInfoDto; setMeteredSubscriptionPrice: BillingUpdate; signIn: AvailableWorkspacesAndAccessTokens; signUp: AvailableWorkspacesAndAccessTokens; @@ -3160,6 +3180,11 @@ export type MutationSetAdminAiModelEnabledArgs = { }; +export type MutationSetEnterpriseKeyArgs = { + enterpriseKey: Scalars['String']; +}; + + export type MutationSetMeteredSubscriptionPriceArgs = { priceId: Scalars['String']; }; @@ -3975,6 +4000,9 @@ export type Query = { commandMenuItems: Array; currentUser: User; currentWorkspace: Workspace; + enterpriseCheckoutSession?: Maybe; + enterprisePortalSession?: Maybe; + enterpriseSubscriptionStatus?: Maybe; eventLogs: EventLogQueryResult; field: Field; fields: FieldConnection; @@ -4117,6 +4145,16 @@ export type QueryCommandMenuItemArgs = { }; +export type QueryEnterpriseCheckoutSessionArgs = { + billingInterval?: InputMaybe; +}; + + +export type QueryEnterprisePortalSessionArgs = { + returnUrlPath?: InputMaybe; +}; + + export type QueryEventLogsArgs = { input: EventLogQueryInput; }; @@ -5543,6 +5581,8 @@ export type Workspace = { fastModel: Scalars['String']; featureFlags?: Maybe>; hasValidEnterpriseKey: Scalars['Boolean']; + hasValidEnterpriseValidityToken: Scalars['Boolean']; + hasValidSignedEnterpriseKey: Scalars['Boolean']; id: Scalars['UUID']; inviteHash?: Maybe; isCustomDomainEnabled: Scalars['Boolean']; @@ -6823,6 +6863,37 @@ export type GetEmailingDomainsQueryVariables = Exact<{ [key: string]: never; }>; export type GetEmailingDomainsQuery = { __typename?: 'Query', getEmailingDomains: Array<{ __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null }> }; +export type RefreshEnterpriseValidityTokenMutationVariables = Exact<{ [key: string]: never; }>; + + +export type RefreshEnterpriseValidityTokenMutation = { __typename?: 'Mutation', refreshEnterpriseValidityToken: boolean }; + +export type SetEnterpriseKeyMutationVariables = Exact<{ + enterpriseKey: Scalars['String']; +}>; + + +export type SetEnterpriseKeyMutation = { __typename?: 'Mutation', setEnterpriseKey: { __typename?: 'EnterpriseLicenseInfoDTO', isValid: boolean, licensee?: string | null, expiresAt?: string | null, subscriptionId?: string | null } }; + +export type EnterpriseCheckoutSessionQueryVariables = Exact<{ + billingInterval?: InputMaybe; +}>; + + +export type EnterpriseCheckoutSessionQuery = { __typename?: 'Query', enterpriseCheckoutSession?: string | null }; + +export type EnterprisePortalSessionQueryVariables = Exact<{ + returnUrlPath?: InputMaybe; +}>; + + +export type EnterprisePortalSessionQuery = { __typename?: 'Query', enterprisePortalSession?: string | null }; + +export type EnterpriseSubscriptionStatusQueryVariables = Exact<{ [key: string]: never; }>; + + +export type EnterpriseSubscriptionStatusQuery = { __typename?: 'Query', enterpriseSubscriptionStatus?: { __typename?: 'EnterpriseSubscriptionStatusDTO', status: string, licensee?: string | null, expiresAt?: string | null, cancelAt?: string | null, currentPeriodEnd?: string | null, isCancellationScheduled: boolean } | null }; + export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{ input: UpdateLabPublicFeatureFlagInput; }>; @@ -6991,7 +7062,7 @@ export type BillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscri export type CurrentBillingSubscriptionFragmentFragment = { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, userWorkspaceId?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodSummary', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, userWorkspaceId?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodSummary', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, hasValidSignedEnterpriseKey: boolean, hasValidEnterpriseValidityToken: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -7010,7 +7081,7 @@ export type DeleteUserWorkspaceMutation = { __typename?: 'Mutation', deleteUserF export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, userWorkspaceId?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodSummary', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, hasPassword: boolean, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, userWorkspaceId?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, numberFormat?: WorkspaceMemberNumberFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, userWorkspaceId?: string | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: string, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', id: string, permissionFlags?: Array | null, objectsPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null, rowLevelPermissionPredicates?: Array<{ __typename?: 'RowLevelPermissionPredicate', id: string, fieldMetadataId: string, objectMetadataId: string, operand: RowLevelPermissionPredicateOperand, subFieldName?: string | null, workspaceMemberFieldMetadataId?: string | null, workspaceMemberSubFieldName?: string | null, rowLevelPermissionPredicateGroupId?: string | null, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, value?: any | null }> | null, rowLevelPermissionPredicateGroups?: Array<{ __typename?: 'RowLevelPermissionPredicateGroup', id: string, parentRowLevelPermissionPredicateGroupId?: string | null, logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator, positionInRowLevelPermissionPredicateGroup?: number | null, roleId: string, objectMetadataId: string }> | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodSummary', twoFactorAuthenticationMethodId: string, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, isGoogleAuthBypassEnabled: boolean, isMicrosoftAuthBypassEnabled: boolean, isPasswordAuthBypassEnabled: boolean, subdomain: string, customDomain?: string | null, hasValidEnterpriseKey: boolean, hasValidSignedEnterpriseKey: boolean, hasValidEnterpriseValidityToken: boolean, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, fastModel: string, smartModel: string, aiAdditionalInstructions?: string | null, autoEnableNewAiModels: boolean, disabledAiModelIds?: Array | null, enabledAiModelIds?: Array | null, useRecommendedModels: boolean, isTwoFactorAuthenticationEnforced: boolean, trashRetentionDays: number, eventLogRetentionDays: number, editableProfileFields?: Array | null, workspaceCustomApplication?: { __typename?: 'Application', id: string } | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, currentPeriodEnd?: string | null, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }>, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: string, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, stripePriceId: string, billingProduct: { __typename?: 'BillingLicensedProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } | { __typename?: 'BillingMeteredProduct', name: string, description: string, images?: Array | null, metadata: { __typename?: 'BillingProductMetadata', productKey: BillingProductKey, planKey: BillingPlanKey, priceUsageBased: BillingUsageType } } }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: string, status: SubscriptionStatus, metadata: any, phases: Array<{ __typename?: 'BillingSubscriptionSchedulePhase', start_date: number, end_date: number, items: Array<{ __typename?: 'BillingSubscriptionSchedulePhaseItem', price: string, quantity?: number | null }> }> }>, billingEntitlements: Array<{ __typename?: 'BillingEntitlement', key: BillingEntitlementKey, value: boolean }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, canBeAssignedToUsers: boolean, canBeAssignedToAgents: boolean, canBeAssignedToApiKeys: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; export type ViewFieldFragmentFragment = { __typename?: 'CoreViewField', id: string, fieldMetadataId: string, viewId: string, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }; @@ -8329,6 +8400,8 @@ export const UserQueryFragmentFragmentDoc = gql` subdomain customDomain hasValidEnterpriseKey + hasValidSignedEnterpriseKey + hasValidEnterpriseValidityToken workspaceCustomApplication { id } @@ -14525,6 +14598,177 @@ export function useGetEmailingDomainsLazyQuery(baseOptions?: Apollo.LazyQueryHoo export type GetEmailingDomainsQueryHookResult = ReturnType; export type GetEmailingDomainsLazyQueryHookResult = ReturnType; export type GetEmailingDomainsQueryResult = Apollo.QueryResult; +export const RefreshEnterpriseValidityTokenDocument = gql` + mutation RefreshEnterpriseValidityToken { + refreshEnterpriseValidityToken +} + `; +export type RefreshEnterpriseValidityTokenMutationFn = Apollo.MutationFunction; + +/** + * __useRefreshEnterpriseValidityTokenMutation__ + * + * To run a mutation, you first call `useRefreshEnterpriseValidityTokenMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useRefreshEnterpriseValidityTokenMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [refreshEnterpriseValidityTokenMutation, { data, loading, error }] = useRefreshEnterpriseValidityTokenMutation({ + * variables: { + * }, + * }); + */ +export function useRefreshEnterpriseValidityTokenMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(RefreshEnterpriseValidityTokenDocument, options); + } +export type RefreshEnterpriseValidityTokenMutationHookResult = ReturnType; +export type RefreshEnterpriseValidityTokenMutationResult = Apollo.MutationResult; +export type RefreshEnterpriseValidityTokenMutationOptions = Apollo.BaseMutationOptions; +export const SetEnterpriseKeyDocument = gql` + mutation SetEnterpriseKey($enterpriseKey: String!) { + setEnterpriseKey(enterpriseKey: $enterpriseKey) { + isValid + licensee + expiresAt + subscriptionId + } +} + `; +export type SetEnterpriseKeyMutationFn = Apollo.MutationFunction; + +/** + * __useSetEnterpriseKeyMutation__ + * + * To run a mutation, you first call `useSetEnterpriseKeyMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSetEnterpriseKeyMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [setEnterpriseKeyMutation, { data, loading, error }] = useSetEnterpriseKeyMutation({ + * variables: { + * enterpriseKey: // value for 'enterpriseKey' + * }, + * }); + */ +export function useSetEnterpriseKeyMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SetEnterpriseKeyDocument, options); + } +export type SetEnterpriseKeyMutationHookResult = ReturnType; +export type SetEnterpriseKeyMutationResult = Apollo.MutationResult; +export type SetEnterpriseKeyMutationOptions = Apollo.BaseMutationOptions; +export const EnterpriseCheckoutSessionDocument = gql` + query EnterpriseCheckoutSession($billingInterval: String) { + enterpriseCheckoutSession(billingInterval: $billingInterval) +} + `; + +/** + * __useEnterpriseCheckoutSessionQuery__ + * + * To run a query within a React component, call `useEnterpriseCheckoutSessionQuery` and pass it any options that fit your needs. + * When your component renders, `useEnterpriseCheckoutSessionQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEnterpriseCheckoutSessionQuery({ + * variables: { + * billingInterval: // value for 'billingInterval' + * }, + * }); + */ +export function useEnterpriseCheckoutSessionQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(EnterpriseCheckoutSessionDocument, options); + } +export function useEnterpriseCheckoutSessionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(EnterpriseCheckoutSessionDocument, options); + } +export type EnterpriseCheckoutSessionQueryHookResult = ReturnType; +export type EnterpriseCheckoutSessionLazyQueryHookResult = ReturnType; +export type EnterpriseCheckoutSessionQueryResult = Apollo.QueryResult; +export const EnterprisePortalSessionDocument = gql` + query EnterprisePortalSession($returnUrlPath: String) { + enterprisePortalSession(returnUrlPath: $returnUrlPath) +} + `; + +/** + * __useEnterprisePortalSessionQuery__ + * + * To run a query within a React component, call `useEnterprisePortalSessionQuery` and pass it any options that fit your needs. + * When your component renders, `useEnterprisePortalSessionQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEnterprisePortalSessionQuery({ + * variables: { + * returnUrlPath: // value for 'returnUrlPath' + * }, + * }); + */ +export function useEnterprisePortalSessionQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(EnterprisePortalSessionDocument, options); + } +export function useEnterprisePortalSessionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(EnterprisePortalSessionDocument, options); + } +export type EnterprisePortalSessionQueryHookResult = ReturnType; +export type EnterprisePortalSessionLazyQueryHookResult = ReturnType; +export type EnterprisePortalSessionQueryResult = Apollo.QueryResult; +export const EnterpriseSubscriptionStatusDocument = gql` + query EnterpriseSubscriptionStatus { + enterpriseSubscriptionStatus { + status + licensee + expiresAt + cancelAt + currentPeriodEnd + isCancellationScheduled + } +} + `; + +/** + * __useEnterpriseSubscriptionStatusQuery__ + * + * To run a query within a React component, call `useEnterpriseSubscriptionStatusQuery` and pass it any options that fit your needs. + * When your component renders, `useEnterpriseSubscriptionStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEnterpriseSubscriptionStatusQuery({ + * variables: { + * }, + * }); + */ +export function useEnterpriseSubscriptionStatusQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(EnterpriseSubscriptionStatusDocument, options); + } +export function useEnterpriseSubscriptionStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(EnterpriseSubscriptionStatusDocument, options); + } +export type EnterpriseSubscriptionStatusQueryHookResult = ReturnType; +export type EnterpriseSubscriptionStatusLazyQueryHookResult = ReturnType; +export type EnterpriseSubscriptionStatusQueryResult = Apollo.QueryResult; export const UpdateLabPublicFeatureFlagDocument = gql` mutation UpdateLabPublicFeatureFlag($input: UpdateLabPublicFeatureFlagInput!) { updateLabPublicFeatureFlag(input: $input) { diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 2ab887ee2cd..4ac58703266 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -58,6 +58,9 @@ const mockWorkspace = { isPasswordAuthBypassEnabled: false, isMicrosoftAuthBypassEnabled: false, hasValidEnterpriseKey: false, + hasActivatedAndValidEnterpriseKey: false, + hasValidSignedEnterpriseKey: false, + hasValidEnterpriseValidityToken: false, subdomain: 'test', customDomain: 'test.com', workspaceUrls: { diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 8a940c97cfa..0f33609edb0 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -1,10 +1,11 @@ import { lazy, Suspense } from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Navigate, Route, Routes } from 'react-router-dom'; import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain'; import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; import { FeatureFlagKey, PermissionFlagType, @@ -662,6 +663,15 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( {isAdminPageEnabled && ( <> } /> + + } + /> } diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index 9f3067bba89..9b70764f3ed 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -27,6 +27,8 @@ export type CurrentWorkspace = Pick< | 'isPasswordAuthBypassEnabled' | 'isCustomDomainEnabled' | 'hasValidEnterpriseKey' + | 'hasValidSignedEnterpriseKey' + | 'hasValidEnterpriseValidityToken' | 'subdomain' | 'customDomain' | 'workspaceUrls' diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx index 57dce27f327..d4ced55118d 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx @@ -15,7 +15,6 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledText = styled.div` overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; `; const StyledCloseButtonContainer = styled.div` diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx index aaa5492fef8..265752ce51d 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx @@ -6,6 +6,7 @@ import { InformationBannerBillingSubscriptionPaused } from '@/information-banner import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod'; import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo'; import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription'; +import { InformationBannerLegacyEnterpriseKey } from '@/information-banner/components/enterprise/InformationBannerLegacyEnterpriseKey'; import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases'; import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions'; import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap'; @@ -52,6 +53,7 @@ export const InformationBannerWrapper = () => { return ( + {isAccountSyncEnabled && ( )} diff --git a/packages/twenty-front/src/modules/information-banner/components/enterprise/InformationBannerLegacyEnterpriseKey.tsx b/packages/twenty-front/src/modules/information-banner/components/enterprise/InformationBannerLegacyEnterpriseKey.tsx new file mode 100644 index 00000000000..f2f2d81cc4c --- /dev/null +++ b/packages/twenty-front/src/modules/information-banner/components/enterprise/InformationBannerLegacyEnterpriseKey.tsx @@ -0,0 +1,45 @@ +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { InformationBanner } from '@/information-banner/components/InformationBanner'; +import { informationBannerIsOpenComponentState } from '@/information-banner/states/informationBannerIsOpenComponentState'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; +import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState'; +import { useLingui } from '@lingui/react/macro'; +import { useNavigate } from 'react-router-dom'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; +import { IconKey } from 'twenty-ui/display'; + +const COMPONENT_INSTANCE_ID = 'information-banner-legacy-enterprise-key'; + +export const InformationBannerLegacyEnterpriseKey = () => { + const { t } = useLingui(); + const navigate = useNavigate(); + const currentWorkspace = useAtomStateValue(currentWorkspaceState); + + const setInformationBannerIsOpen = useSetAtomComponentState( + informationBannerIsOpenComponentState, + COMPONENT_INSTANCE_ID, + ); + + const hasLegacyKey = + currentWorkspace?.hasValidEnterpriseKey === true && + currentWorkspace?.hasValidSignedEnterpriseKey !== true; + + if (!hasLegacyKey) { + return null; + } + + return ( + + navigate(getSettingsPath(SettingsPath.AdminPanelEnterprise)) + } + onClose={() => setInformationBannerIsOpen(false)} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index aa1701e8e87..5dd088d6305 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -181,6 +181,8 @@ export const responseData = { activationStatus: 'active', isPublicInviteLinkEnabled: false, hasValidEnterpriseKey: false, + hasValidSignedEnterpriseKey: false, + hasValidEnterpriseValidityToken: false, isGoogleAuthEnabled: true, isMicrosoftAuthEnabled: false, isPasswordAuthEnabled: true, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts index 95fa2f72730..a9f28316bb6 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromObjectMetadata.test.ts @@ -33,6 +33,8 @@ describe('useColumnDefinitionsFromObjectMetadata', () => { subdomain: 'test', activationStatus: WorkspaceActivationStatus.ACTIVE, hasValidEnterpriseKey: false, + hasValidSignedEnterpriseKey: false, + hasValidEnterpriseValidityToken: false, metadataVersion: 1, isPublicInviteLinkEnabled: false, isGoogleAuthEnabled: true, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index 7ca51933284..b4ae97cc4e0 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,23 +1,27 @@ import { currentUserState } from '@/auth/states/currentUserState'; +import { billingState } from '@/client-config/states/billingState'; import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent'; import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; +import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { t } from '@lingui/core/macro'; import { IconApps, IconHeart, + IconKey, IconSettings2, IconSparkles, IconVariable, } from 'twenty-ui/display'; -import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; export const SettingsAdminContent = () => { const currentUser = useAtomStateValue(currentUserState); + const billing = useAtomStateValue(billingState); const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel; const canImpersonate = currentUser?.canImpersonate; + const isBillingEnabled = billing?.isBillingEnabled; const tabs = [ { id: SETTINGS_ADMIN_TABS.GENERAL, @@ -49,6 +53,16 @@ export const SettingsAdminContent = () => { Icon: IconHeart, disabled: !canAccessFullAdminPanel, }, + ...(!isBillingEnabled + ? [ + { + id: SETTINGS_ADMIN_TABS.ENTERPRISE, + title: t`Enterprise`, + Icon: IconKey, + disabled: !canAccessFullAdminPanel && !canImpersonate, + }, + ] + : []), ]; return ( diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx index 2063680883d..884d479c3fb 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminTabContent.tsx @@ -5,9 +5,16 @@ import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-vari import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus'; +import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; +import { lazy, Suspense } from 'react'; +const SettingsEnterprise = lazy(() => + import('~/pages/settings/enterprise/SettingsEnterprise').then((module) => ({ + default: module.SettingsEnterprise, + })), +); export const SettingsAdminTabContent = () => { const activeTabId = useAtomComponentStateValue( activeTabIdComponentState, @@ -25,6 +32,12 @@ export const SettingsAdminTabContent = () => { return ; case SETTINGS_ADMIN_TABS.HEALTH_STATUS: return ; + case SETTINGS_ADMIN_TABS.ENTERPRISE: + return ( + }> + + + ); default: return null; } diff --git a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts index 3cf4c9d64a4..d8e5d2b60f0 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminTabs.ts @@ -4,4 +4,5 @@ export const SETTINGS_ADMIN_TABS = { AI: 'ai', CONFIG_VARIABLES: 'config-variables', HEALTH_STATUS: 'health-status', + ENTERPRISE: 'enterprise', }; diff --git a/packages/twenty-front/src/modules/settings/enterprise/components/EnterprisePlanModal.tsx b/packages/twenty-front/src/modules/settings/enterprise/components/EnterprisePlanModal.tsx new file mode 100644 index 00000000000..b0ec5ec5836 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/components/EnterprisePlanModal.tsx @@ -0,0 +1,178 @@ +import { SubTitle } from '@/auth/components/SubTitle'; +import { Title } from '@/auth/components/Title'; +import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit'; +import { ENTERPRISE_CHECKOUT_SESSION } from '@/settings/enterprise/graphql/queries/enterpriseCheckoutSession'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { useLazyQuery } from '@apollo/client'; +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; +import { Loader } from 'twenty-ui/feedback'; +import { CardPicker, MainButton } from 'twenty-ui/input'; +import { ModalContent } from 'twenty-ui/layout'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; + +export const ENTERPRISE_PLAN_MODAL_ID = 'enterprise-plan-modal'; + +type BillingInterval = 'monthly' | 'yearly'; + +const MONTHLY_PRICE = 25; +const YEARLY_PRICE = 19; + +const StyledSubscriptionContainer = styled.div` + background-color: ${themeCssVariables.background.secondary}; + border: 1px solid ${themeCssVariables.border.color.medium}; + border-radius: ${themeCssVariables.border.radius.md}; + display: flex; + flex-direction: column; + margin: ${themeCssVariables.spacing[8]} 0 ${themeCssVariables.spacing[2]}; + width: 100%; +`; + +const StyledPriceContainer = styled.div` + align-items: center; + border-bottom: 1px solid ${themeCssVariables.border.color.light}; + display: flex; + flex-direction: column; + margin: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]} 0 + ${themeCssVariables.spacing[4]}; + padding-bottom: ${themeCssVariables.spacing[3]}; +`; + +const StyledPrice = styled.span` + color: ${themeCssVariables.font.color.primary}; + font-size: ${themeCssVariables.font.size.xxl}; + font-weight: ${themeCssVariables.font.weight.semiBold}; + margin-bottom: ${themeCssVariables.spacing[1]}; +`; + +const StyledPriceUnit = styled.span` + color: ${themeCssVariables.font.color.light}; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.medium}; +`; + +const StyledBenefitsContainer = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 16px; + padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[3]}; + width: 100%; +`; + +const StyledIntervalContainer = styled.div` + display: flex; + flex-direction: row; + gap: ${themeCssVariables.spacing[2]}; + margin-bottom: ${themeCssVariables.spacing[8]}; + width: 100%; +`; + +const StyledIntervalTitle = styled.div` + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.md}; +`; + +export const EnterprisePlanModal = () => { + const { t } = useLingui(); + const { closeModal } = useModal(); + const { enqueueErrorSnackBar } = useSnackBar(); + const [selectedInterval, setSelectedInterval] = + useState('monthly'); + const [isLoading, setIsLoading] = useState(false); + + const [fetchCheckoutSession] = useLazyQuery(ENTERPRISE_CHECKOUT_SESSION); + + const benefits = [ + t`SSO (SAML / OIDC)`, + t`Row-level security`, + t`Audit logs`, + t`Custom objects`, + t`API & Webhooks`, + ]; + + const price = selectedInterval === 'monthly' ? MONTHLY_PRICE : YEARLY_PRICE; + const priceUnit = + selectedInterval === 'monthly' + ? t`seat / month` + : t`seat / month - billed yearly`; + + const handleContinue = async () => { + setIsLoading(true); + + try { + const { data } = await fetchCheckoutSession({ + variables: { billingInterval: selectedInterval }, + }); + + const checkoutUrl = data?.enterpriseCheckoutSession; + + if (checkoutUrl !== null && checkoutUrl !== undefined) { + window.open(checkoutUrl, '_blank', 'noopener'); + closeModal(ENTERPRISE_PLAN_MODAL_ID); + } else { + enqueueErrorSnackBar({ + message: t`Could not open Stripe. Please contact support.`, + }); + } + } catch { + enqueueErrorSnackBar({ + message: t`Error opening Stripe`, + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + {t`Get Enterprise`} + {t`Enjoy a 30-day free trial`} + + + + {`$${price}`} + {priceUnit} + + + {benefits.map((benefit) => ( + {benefit} + ))} + + + + + setSelectedInterval('monthly')} + > + {t`Monthly subscription`} + + setSelectedInterval('yearly')} + > + {t`Yearly subscription`} + + + + isLoading && } + disabled={isLoading} + /> + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/refreshEnterpriseValidityToken.ts b/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/refreshEnterpriseValidityToken.ts new file mode 100644 index 00000000000..41c05fa7690 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/refreshEnterpriseValidityToken.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const REFRESH_ENTERPRISE_VALIDITY_TOKEN = gql` + mutation RefreshEnterpriseValidityToken { + refreshEnterpriseValidityToken + } +`; diff --git a/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/setEnterpriseKey.ts b/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/setEnterpriseKey.ts new file mode 100644 index 00000000000..4603befb694 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/graphql/mutations/setEnterpriseKey.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +export const SET_ENTERPRISE_KEY = gql` + mutation SetEnterpriseKey($enterpriseKey: String!) { + setEnterpriseKey(enterpriseKey: $enterpriseKey) { + isValid + licensee + expiresAt + subscriptionId + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseCheckoutSession.ts b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseCheckoutSession.ts new file mode 100644 index 00000000000..9eee1314932 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseCheckoutSession.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const ENTERPRISE_CHECKOUT_SESSION = gql` + query EnterpriseCheckoutSession($billingInterval: String) { + enterpriseCheckoutSession(billingInterval: $billingInterval) + } +`; diff --git a/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterprisePortalSession.ts b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterprisePortalSession.ts new file mode 100644 index 00000000000..51180557a9b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterprisePortalSession.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const ENTERPRISE_PORTAL_SESSION = gql` + query EnterprisePortalSession($returnUrlPath: String) { + enterprisePortalSession(returnUrlPath: $returnUrlPath) + } +`; diff --git a/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseSubscriptionStatus.ts b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseSubscriptionStatus.ts new file mode 100644 index 00000000000..709bed3d153 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/enterprise/graphql/queries/enterpriseSubscriptionStatus.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const ENTERPRISE_SUBSCRIPTION_STATUS = gql` + query EnterpriseSubscriptionStatus { + enterpriseSubscriptionStatus { + status + licensee + expiresAt + cancelAt + currentPeriodEnd + isCancellationScheduled + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection.tsx index 258dacaf888..b555ca6d41b 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/record-level-permissions/components/SettingsRolePermissionsObjectLevelRecordLevelSection.tsx @@ -17,7 +17,6 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings'; const StyledContent = styled.div` padding-bottom: ${themeCssVariables.spacing[2]}; - padding-top: ${themeCssVariables.spacing[4]}; `; const StyledCardContainer = styled.div` @@ -70,18 +69,22 @@ export const SettingsRolePermissionsObjectLevelRecordLevelSection = ({ navigateSettings(SettingsPath.Billing)} - /> - ) + + + +
+ Next steps: +
    +
  1. Copy the enterprise key above
  2. +
  3. + Open your Twenty self-hosted instance Settings → + Enterprise +
  4. +
  5. Paste the key and click Activate
  6. +
+
+ + )} + + + ); +} diff --git a/packages/twenty-website/src/app/api/enterprise/activate/route.ts b/packages/twenty-website/src/app/api/enterprise/activate/route.ts new file mode 100644 index 00000000000..eb629cb6ca9 --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/activate/route.ts @@ -0,0 +1,62 @@ +import { signEnterpriseKey } from '@/shared/enterprise/enterprise-jwt'; +import { getStripeClient } from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function GET(request: Request) { + try { + const url = new URL(request.url); + const sessionId = url.searchParams.get('session_id'); + + if (!sessionId) { + return new Response( + JSON.stringify({ error: 'Missing session_id parameter' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const stripe = getStripeClient(); + + const session = await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['subscription', 'customer'], + }); + + if (session.payment_status !== 'paid') { + return new Response( + JSON.stringify({ error: 'Payment not completed' }), + { status: 402, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const subscription = session.subscription; + + if (!subscription || typeof subscription === 'string') { + return new Response( + JSON.stringify({ error: 'Subscription not found' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const customer = session.customer; + const licensee = + customer && typeof customer !== 'string' && !customer.deleted + ? (customer.name ?? customer.email ?? 'Unknown') + : 'Unknown'; + + const enterpriseKey = signEnterpriseKey(subscription.id, licensee); + + return Response.json({ + enterpriseKey, + licensee, + subscriptionId: subscription.id, + }); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Activation error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/app/api/enterprise/checkout/route.ts b/packages/twenty-website/src/app/api/enterprise/checkout/route.ts new file mode 100644 index 00000000000..302a95f39fd --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/checkout/route.ts @@ -0,0 +1,47 @@ +import { + getEnterprisePriceId, + getStripeClient, +} from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + try { + const stripe = getStripeClient(); + const body = await request.json(); + const billingInterval = body.billingInterval === 'yearly' ? 'yearly' : 'monthly'; + const priceId = getEnterprisePriceId(billingInterval); + const successUrl = + body.successUrl ?? + `${process.env.NEXT_PUBLIC_WEBSITE_URL}/enterprise/activate?session_id={CHECKOUT_SESSION_ID}`; + + + const session = await stripe.checkout.sessions.create({ + mode: 'subscription', + line_items: [ + { + price: priceId, + quantity: body.seatCount ?? 1, + }, + ], + success_url: successUrl, + subscription_data: { + trial_period_days: 30, + metadata: { + source: 'enterprise-self-hosted', + }, + }, + }); + + return Response.json({ url: session.url }); + } catch (error: unknown) { + console.error(error); + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Checkout error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/app/api/enterprise/portal/route.ts b/packages/twenty-website/src/app/api/enterprise/portal/route.ts new file mode 100644 index 00000000000..f9e4ac3a1aa --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/portal/route.ts @@ -0,0 +1,57 @@ +import { verifyEnterpriseKey } from '@/shared/enterprise/enterprise-jwt'; +import { + getStripeClient +} from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { enterpriseKey, returnUrl } = body; + + if (!enterpriseKey || typeof enterpriseKey !== 'string') { + return new Response( + JSON.stringify({ error: 'Missing enterpriseKey' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const payload = verifyEnterpriseKey(enterpriseKey); + + if (!payload) { + return new Response( + JSON.stringify({ error: 'Invalid enterprise key' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const stripe = getStripeClient(); + const subscription = await stripe.subscriptions.retrieve(payload.sub); + + const customerId = + typeof subscription.customer === 'string' + ? subscription.customer + : subscription.customer.id; + + const frontendUrl = process.env.NEXT_PUBLIC_WEBSITE_URL; + const fullReturnUrl = returnUrl + ? `${frontendUrl}${returnUrl}` + : frontendUrl; + + const session = await stripe.billingPortal.sessions.create({ + customer: customerId, + return_url: fullReturnUrl, + }); + + return Response.json({ url: session.url }); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Portal error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/app/api/enterprise/seats/route.ts b/packages/twenty-website/src/app/api/enterprise/seats/route.ts new file mode 100644 index 00000000000..367bcd114d7 --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/seats/route.ts @@ -0,0 +1,89 @@ +import { verifyEnterpriseKey } from '@/shared/enterprise/enterprise-jwt'; +import { getStripeClient } from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { enterpriseKey, seatCount } = body; + + if (!enterpriseKey || typeof enterpriseKey !== 'string') { + return new Response( + JSON.stringify({ error: 'Missing enterpriseKey' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + if (typeof seatCount !== 'number' || seatCount < 1) { + return new Response( + JSON.stringify({ error: 'Invalid seatCount' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const payload = verifyEnterpriseKey(enterpriseKey); + + if (!payload) { + return new Response( + JSON.stringify({ error: 'Invalid enterprise key' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const stripe = getStripeClient(); + + const subscription = await stripe.subscriptions.retrieve(payload.sub); + + const NON_UPDATABLE_STATUSES = [ + 'canceled', + 'incomplete_expired', + ]; + + if ( + NON_UPDATABLE_STATUSES.includes(subscription.status) || + subscription.cancel_at_period_end + ) { + return Response.json({ + success: false, + reason: 'Subscription is canceled or scheduled for cancellation', + seatCount: subscription.items.data[0]?.quantity ?? 0, + subscriptionId: payload.sub, + }); + } + + if (!subscription.items.data[0]) { + return new Response( + JSON.stringify({ error: 'No subscription item found' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const subscriptionItemId = subscription.items.data[0].id; + + await stripe.subscriptions.update(payload.sub, { + items: [ + { + id: subscriptionItemId, + quantity: seatCount, + }, + ], + proration_behavior: 'create_prorations', + }); + + return Response.json({ + success: true, + seatCount, + subscriptionId: payload.sub, + }); + } catch (error: unknown) { + console.error(error); + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Seat update error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/app/api/enterprise/status/route.ts b/packages/twenty-website/src/app/api/enterprise/status/route.ts new file mode 100644 index 00000000000..fb4006df598 --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/status/route.ts @@ -0,0 +1,58 @@ +import { verifyEnterpriseKey } from '@/shared/enterprise/enterprise-jwt'; +import { getStripeClient } from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { enterpriseKey } = body; + + if (!enterpriseKey || typeof enterpriseKey !== 'string') { + return new Response( + JSON.stringify({ error: 'Missing enterpriseKey' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const payload = verifyEnterpriseKey(enterpriseKey); + + if (!payload) { + return new Response( + JSON.stringify({ error: 'Invalid enterprise key' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const stripe = getStripeClient(); + const subscription = await stripe.subscriptions.retrieve(payload.sub); + + const rawCancelAt = subscription.cancel_at; + const rawCancelAtPeriodEnd = subscription.cancel_at_period_end; + const rawCurrentPeriodEnd = (subscription as any).current_period_end as + | number + | null; + + const effectiveCancelAt = + rawCancelAt ?? (rawCancelAtPeriodEnd ? rawCurrentPeriodEnd : null); + + const isCancellationScheduled = + subscription.status !== 'canceled' && effectiveCancelAt !== null; + + return Response.json({ + subscriptionId: subscription.id, + status: subscription.status, + cancelAt: effectiveCancelAt, + currentPeriodEnd: rawCurrentPeriodEnd, + isCancellationScheduled, + }); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Status error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/app/api/enterprise/validate/route.ts b/packages/twenty-website/src/app/api/enterprise/validate/route.ts new file mode 100644 index 00000000000..c020e9ed3fa --- /dev/null +++ b/packages/twenty-website/src/app/api/enterprise/validate/route.ts @@ -0,0 +1,73 @@ +import { + signValidityToken, + verifyEnterpriseKey, +} from '@/shared/enterprise/enterprise-jwt'; +import { getStripeClient } from '@/shared/enterprise/stripe-client'; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { enterpriseKey } = body; + + if (!enterpriseKey || typeof enterpriseKey !== 'string') { + return new Response( + JSON.stringify({ error: 'Missing enterpriseKey' }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const payload = verifyEnterpriseKey(enterpriseKey); + + if (!payload) { + return new Response( + JSON.stringify({ error: 'Invalid enterprise key' }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const stripe = getStripeClient(); + + const subscription = await stripe.subscriptions.retrieve(payload.sub); + + const activeStatuses = ['active', 'trialing']; + + if (!activeStatuses.includes(subscription.status)) { + return new Response( + JSON.stringify({ + error: 'Subscription is not active', + status: subscription.status, + }), + { status: 403, headers: { 'Content-Type': 'application/json' } }, + ); + } + + const rawCancelAt = subscription.cancel_at; + const rawCancelAtPeriodEnd = subscription.cancel_at_period_end; + const rawCurrentPeriodEnd = (subscription as { current_period_end?: number }) + .current_period_end; + const effectiveCancelAt = + rawCancelAt ?? + (rawCancelAtPeriodEnd && rawCurrentPeriodEnd ? rawCurrentPeriodEnd : null); + + const validityToken = signValidityToken(payload.sub, { + subscriptionCancelAt: effectiveCancelAt, + }); + + return Response.json({ + validityToken, + licensee: payload.licensee, + subscriptionId: payload.sub, + subscriptionStatus: subscription.status, + }); + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : 'Unknown error'; + + return new Response( + JSON.stringify({ error: `Validation error: ${message}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } }, + ); + } +} diff --git a/packages/twenty-website/src/shared/enterprise/enterprise-jwt.ts b/packages/twenty-website/src/shared/enterprise/enterprise-jwt.ts new file mode 100644 index 00000000000..168ebf4fd25 --- /dev/null +++ b/packages/twenty-website/src/shared/enterprise/enterprise-jwt.ts @@ -0,0 +1,188 @@ +import * as crypto from 'crypto'; + +export type EnterpriseKeyPayload = { + sub: string; + licensee: string; + iat: number; +}; + +export type EnterpriseValidityPayload = { + sub: string; + status: 'valid'; + iat: number; + exp: number; +}; + +const ALGORITHM = 'RS256'; +const DEFAULT_VALIDITY_TOKEN_DURATION_DAYS= 30; + +const getValidityTokenDurationDays = (): number => { + const value = process.env.ENTERPRISE_VALIDITY_TOKEN_DURATION_DAYS; + + if (value === undefined || value === '') { + return DEFAULT_VALIDITY_TOKEN_DURATION_DAYS; + } + + const parsed = parseInt(value, 10); + + if (Number.isNaN(parsed) || parsed < 1) { + return DEFAULT_VALIDITY_TOKEN_DURATION_DAYS; + } + + return parsed; +}; + +export type SignValidityTokenOptions = { + subscriptionCancelAt: number | null; +}; + +const computeValidityExp = ( + nowSeconds: number, + durationDays: number, + subscriptionCancelAt: number | null, +): number => { + const defaultExp = nowSeconds + durationDays * 24 * 60 * 60; + + if (subscriptionCancelAt === null || subscriptionCancelAt <= 0) { + return defaultExp; + } + + return Math.min(defaultExp, subscriptionCancelAt); +}; + +const getPrivateKey = (): string => { + const key = process.env.ENTERPRISE_JWT_PRIVATE_KEY; + + if (!key) { + throw new Error('ENTERPRISE_JWT_PRIVATE_KEY is not configured'); + } + + return key.replace(/\\n/g, '\n'); +}; + +const base64UrlEncode = (data: string): string => { + return Buffer.from(data) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +}; + +const base64UrlDecode = (data: string): string => { + const padded = data + '='.repeat((4 - (data.length % 4)) % 4); + const base64 = padded.replace(/-/g, '+').replace(/_/g, '/'); + + return Buffer.from(base64, 'base64').toString('utf-8'); +}; + +const signJwt = ( + payload: Record, + privateKey: string, +): string => { + const header = { alg: ALGORITHM, typ: 'JWT' }; + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(payload)); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + const signature = crypto + .sign('sha256', Buffer.from(signingInput), { + key: privateKey, + padding: crypto.constants.RSA_PKCS1_PADDING, + }) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + return `${signingInput}.${signature}`; +}; + +export const verifyJwt = >( + token: string, + publicKey: string, +): T | null => { + try { + const parts = token.split('.'); + + if (parts.length !== 3) { + return null; + } + + const [encodedHeader, encodedPayload, signature] = parts; + const signingInput = `${encodedHeader}.${encodedPayload}`; + + const signatureBuffer = Buffer.from( + signature.replace(/-/g, '+').replace(/_/g, '/') + + '='.repeat((4 - (signature.length % 4)) % 4), + 'base64', + ); + + const isValid = crypto.verify( + 'sha256', + Buffer.from(signingInput), + { + key: publicKey, + padding: crypto.constants.RSA_PKCS1_PADDING, + }, + signatureBuffer, + ); + + if (!isValid) { + return null; + } + + return JSON.parse(base64UrlDecode(encodedPayload)) as T; + } catch { + return null; + } +}; + +export const signEnterpriseKey = ( + subscriptionId: string, + licensee: string, +): string => { + const payload: EnterpriseKeyPayload = { + sub: subscriptionId, + licensee, + iat: Math.floor(Date.now() / 1000), + }; + + return signJwt(payload, getPrivateKey()); +}; + +export const signValidityToken = ( + subscriptionId: string, + options?: SignValidityTokenOptions, +): string => { + const now = Math.floor(Date.now() / 1000); + const durationDays = getValidityTokenDurationDays(); + const subscriptionCancelAt = options?.subscriptionCancelAt ?? null; + const exp = computeValidityExp(now, durationDays, subscriptionCancelAt); + + const payload: EnterpriseValidityPayload = { + sub: subscriptionId, + status: 'valid', + iat: now, + exp, + }; + + return signJwt(payload, getPrivateKey()); +}; + +export const verifyEnterpriseKey = ( + token: string, +): EnterpriseKeyPayload | null => { + const publicKey = getPublicKey(); + + return verifyJwt(token, publicKey); +}; + +const getPublicKey = (): string => { + const key = process.env.ENTERPRISE_JWT_PUBLIC_KEY; + + if (!key) { + throw new Error('ENTERPRISE_JWT_PUBLIC_KEY is not configured'); + } + + return key.replace(/\\n/g, '\n'); +}; diff --git a/packages/twenty-website/src/shared/enterprise/stripe-client.ts b/packages/twenty-website/src/shared/enterprise/stripe-client.ts new file mode 100644 index 00000000000..133801c30e1 --- /dev/null +++ b/packages/twenty-website/src/shared/enterprise/stripe-client.ts @@ -0,0 +1,34 @@ +import Stripe from 'stripe'; + +let stripeInstance: Stripe | null = null; + +export const getStripeClient = (): Stripe => { + if (!stripeInstance) { + const secretKey = process.env.STRIPE_SECRET_KEY; + + if (!secretKey) { + throw new Error('STRIPE_SECRET_KEY is not configured'); + } + + stripeInstance = new Stripe(secretKey, {}); + } + + return stripeInstance; +}; + +export const getEnterprisePriceId = ( + billingInterval: 'monthly' | 'yearly' = 'monthly', +): string => { + const envKey = + billingInterval === 'yearly' + ? 'STRIPE_ENTERPRISE_YEARLY_PRICE_ID' + : 'STRIPE_ENTERPRISE_MONTHLY_PRICE_ID'; + + const priceId = process.env[envKey]; + + if (!priceId) { + throw new Error(`${envKey} is not configured`); + } + + return priceId; +};