diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
index 8abb8f973..dd0a149f8 100644
--- a/packages/twenty-front/src/App.tsx
+++ b/packages/twenty-front/src/App.tsx
@@ -40,6 +40,7 @@ import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/we
import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew';
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
+import { SettingsBilling } from '~/pages/settings/SettingsBilling.tsx';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
@@ -114,6 +115,10 @@ export const App = () => {
path={SettingsPath.AccountsEmailsInboxSettings}
element={}
/>
+ }
+ />
}
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index ef4db9f3f..8b43fd60a 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -889,6 +889,13 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{
export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
+export type BillingPortalSessionQueryVariables = Exact<{
+ returnUrlPath?: InputMaybe;
+}>;
+
+
+export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url: string } };
+
export type CheckoutSessionMutationVariables = Exact<{
recurringInterval: Scalars['String'];
successUrlPath?: InputMaybe;
@@ -1588,6 +1595,41 @@ export function useValidatePasswordResetTokenLazyQuery(baseOptions?: Apollo.Lazy
export type ValidatePasswordResetTokenQueryHookResult = ReturnType;
export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType;
export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult;
+export const BillingPortalSessionDocument = gql`
+ query BillingPortalSession($returnUrlPath: String) {
+ billingPortalSession(returnUrlPath: $returnUrlPath) {
+ url
+ }
+}
+ `;
+
+/**
+ * __useBillingPortalSessionQuery__
+ *
+ * To run a query within a React component, call `useBillingPortalSessionQuery` and pass it any options that fit your needs.
+ * When your component renders, `useBillingPortalSessionQuery` 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 } = useBillingPortalSessionQuery({
+ * variables: {
+ * returnUrlPath: // value for 'returnUrlPath'
+ * },
+ * });
+ */
+export function useBillingPortalSessionQuery(baseOptions?: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(BillingPortalSessionDocument, options);
+ }
+export function useBillingPortalSessionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(BillingPortalSessionDocument, options);
+ }
+export type BillingPortalSessionQueryHookResult = ReturnType;
+export type BillingPortalSessionLazyQueryHookResult = ReturnType;
+export type BillingPortalSessionQueryResult = Apollo.QueryResult;
export const CheckoutSessionDocument = gql`
mutation CheckoutSession($recurringInterval: String!, $successUrlPath: String) {
checkoutSession(
diff --git a/packages/twenty-front/src/modules/billing/assets/cover-dark.png b/packages/twenty-front/src/modules/billing/assets/cover-dark.png
new file mode 100644
index 000000000..d824a6704
Binary files /dev/null and b/packages/twenty-front/src/modules/billing/assets/cover-dark.png differ
diff --git a/packages/twenty-front/src/modules/billing/assets/cover-light.png b/packages/twenty-front/src/modules/billing/assets/cover-light.png
new file mode 100644
index 000000000..31c4e56dd
Binary files /dev/null and b/packages/twenty-front/src/modules/billing/assets/cover-light.png differ
diff --git a/packages/twenty-front/src/modules/billing/components/ManageYourSubscription.tsx b/packages/twenty-front/src/modules/billing/components/ManageYourSubscription.tsx
new file mode 100644
index 000000000..e95067d18
--- /dev/null
+++ b/packages/twenty-front/src/modules/billing/components/ManageYourSubscription.tsx
@@ -0,0 +1,24 @@
+import { IconCreditCard } from '@/ui/display/icon';
+import { Button } from '@/ui/input/button/components/Button';
+import { useBillingPortalSessionQuery } from '~/generated/graphql.tsx';
+export const ManageYourSubscription = () => {
+ const { data, loading } = useBillingPortalSessionQuery({
+ variables: {
+ returnUrlPath: '/settings/billing',
+ },
+ });
+ const handleButtonClick = () => {
+ if (data) {
+ window.location.replace(data.billingPortalSession.url);
+ }
+ };
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/billing/components/SettingsBillingCoverImage.tsx b/packages/twenty-front/src/modules/billing/components/SettingsBillingCoverImage.tsx
new file mode 100644
index 000000000..b48ced04d
--- /dev/null
+++ b/packages/twenty-front/src/modules/billing/components/SettingsBillingCoverImage.tsx
@@ -0,0 +1,22 @@
+import styled from '@emotion/styled';
+
+import DarkCoverImage from '@/billing/assets/cover-dark.png';
+import LightCoverImage from '@/billing/assets/cover-light.png';
+
+const StyledCoverImageContainer = styled.div`
+ align-items: center;
+ background-image: ${({ theme }) =>
+ theme.name === 'light'
+ ? `url('${LightCoverImage.toString()}')`
+ : `url('${DarkCoverImage.toString()}')`};
+ background-size: contain;
+ background-repeat: no-repeat;
+ box-sizing: border-box;
+ display: flex;
+ height: 162px;
+ justify-content: center;
+ position: relative;
+`;
+export const SettingsBillingCoverImage = () => {
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/billing/graphql/billingPortalSession.ts b/packages/twenty-front/src/modules/billing/graphql/billingPortalSession.ts
new file mode 100644
index 000000000..ba4f9ff3a
--- /dev/null
+++ b/packages/twenty-front/src/modules/billing/graphql/billingPortalSession.ts
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const BILLING_PORTAL_SESSION = gql`
+ query BillingPortalSession($returnUrlPath: String) {
+ billingPortalSession(returnUrlPath: $returnUrlPath) {
+ url
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/billing/graphql/checkout-session.ts b/packages/twenty-front/src/modules/billing/graphql/checkoutSession.ts
similarity index 100%
rename from packages/twenty-front/src/modules/billing/graphql/checkout-session.ts
rename to packages/twenty-front/src/modules/billing/graphql/checkoutSession.ts
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
index 90888ac86..20923bd3c 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx
@@ -11,6 +11,7 @@ import {
IconCalendarEvent,
IconCode,
IconColorSwatch,
+ IconCurrencyDollar,
IconDoorEnter,
IconHierarchy2,
IconMail,
@@ -34,6 +35,7 @@ export const SettingsNavigationDrawerItems = () => {
}, [signOut, navigate]);
const isCalendarEnabled = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
+ const isSelfBillingEnabled = useIsFeatureEnabled('IS_SELF_BILLING_ENABLED');
return (
<>
@@ -86,6 +88,12 @@ export const SettingsNavigationDrawerItems = () => {
path={SettingsPath.WorkspaceMembersPage}
Icon={IconUsers}
/>
+
(
+
+
+
+
+
+
+
+);
diff --git a/packages/twenty-server/src/core/billing/billing.service.ts b/packages/twenty-server/src/core/billing/billing.service.ts
index cd7486ea7..ee8406d08 100644
--- a/packages/twenty-server/src/core/billing/billing.service.ts
+++ b/packages/twenty-server/src/core/billing/billing.service.ts
@@ -111,9 +111,14 @@ export class BillingService {
where: { workspaceId },
});
+ const frontBaseUrl = this.environmentService.getFrontBaseUrl();
+ const returnUrl = returnUrlPath
+ ? frontBaseUrl + returnUrlPath
+ : frontBaseUrl;
+
const session = await this.stripeService.createBillingPortalSession(
billingSubscription.stripeCustomerId,
- returnUrlPath,
+ returnUrl,
);
assert(session.url, 'Error: missing billingPortal.session.url');
diff --git a/packages/twenty-server/src/core/billing/stripe/stripe.service.ts b/packages/twenty-server/src/core/billing/stripe/stripe.service.ts
index f7fbf24b2..2c68c7a6c 100644
--- a/packages/twenty-server/src/core/billing/stripe/stripe.service.ts
+++ b/packages/twenty-server/src/core/billing/stripe/stripe.service.ts
@@ -44,11 +44,11 @@ export class StripeService {
async createBillingPortalSession(
stripeCustomerId: string,
- returnUrlPath?: string,
+ returnUrl?: string,
): Promise {
return await this.stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
- return_url: returnUrlPath ?? this.environmentService.getFrontBaseUrl(),
+ return_url: returnUrl ?? this.environmentService.getFrontBaseUrl(),
});
}