Pass Billing Checkout var in url to bypass credit card (#9283)

This commit is contained in:
Félix Malfait
2024-12-31 14:48:00 +01:00
committed by GitHub
parent 45f14c8020
commit 97f5a5b8a5
123 changed files with 524 additions and 173 deletions

View File

@ -31,6 +31,13 @@ We felt the need for a CRM platform that empowers rather than constrains. We bel
<br>
# Demo
<!--
You can use the following url to sign up to the cloud version without providing a credit card:
<a href="https://demo.twenty.com/?billingCheckoutSession={"plan":"PRO","recurringInterval":"MONTHLY","requirePaymentMethod":false,"skipPlanPage":true}">https://demo.twenty.com/?billingCheckoutSession={"plan":"PRO","recurringInterval":"MONTHLY","requirePaymentMethod":false,"skipPlanPage":true}</a>
-->
Go to <a href="https://demo.twenty.com/">demo.twenty.com</a> and login with the following credentials:
```

View File

@ -48,6 +48,7 @@
"buffer": "^6.0.3",
"docx": "^9.1.0",
"file-saver": "^2.0.5",
"recoil-sync": "^0.2.0",
"transliteration": "^2.3.5"
},
"devDependencies": {

View File

@ -115,6 +115,13 @@ export type Billing = {
isBillingEnabled: Scalars['Boolean'];
};
/** The different billing plans available */
export enum BillingPlanKey {
Base = 'BASE',
Enterprise = 'ENTERPRISE',
Pro = 'PRO'
}
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['UUID'];
@ -182,6 +189,30 @@ export type ComputeStepOutputSchemaInput = {
step: Scalars['JSON'];
};
export type CreateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
isActive?: InputMaybe<Scalars['Boolean']>;
isCustom?: InputMaybe<Scalars['Boolean']>;
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
isNullable?: InputMaybe<Scalars['Boolean']>;
isRemoteCreation?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label: Scalars['String'];
name: Scalars['String'];
objectMetadataId: Scalars['String'];
options?: InputMaybe<Scalars['JSON']>;
settings?: InputMaybe<Scalars['JSON']>;
type: FieldMetadataType;
};
export type CreateOneFieldMetadataInput = {
/** The record to create */
field: CreateFieldInput;
};
export type CreateServerlessFunctionInput = {
description?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
@ -205,6 +236,11 @@ export type CursorPaging = {
last?: InputMaybe<Scalars['Int']>;
};
export type DeleteOneFieldInput = {
/** The id of the field to delete. */
id: Scalars['UUID'];
};
export type DeleteOneObjectInput = {
/** The id of the record to delete. */
id: Scalars['UUID'];
@ -447,12 +483,14 @@ export type Mutation = {
computeStepOutputSchema: Scalars['JSON'];
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
createOneField: Field;
createOneObject: Object;
createOneServerlessFunction: ServerlessFunction;
createSAMLIdentityProvider: SetupSsoOutput;
createWorkflowVersionStep: WorkflowAction;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteCurrentWorkspace: Workspace;
deleteOneField: Field;
deleteOneObject: Object;
deleteOneServerlessFunction: ServerlessFunction;
deleteSSOIdentityProvider: DeleteSsoOutput;
@ -478,6 +516,7 @@ export type Mutation = {
switchWorkspace: PublicWorkspaceDataOutput;
track: Analytics;
updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field;
updateOneObject: Object;
updateOneServerlessFunction: ServerlessFunction;
updatePasswordViaResetToken: InvalidatePassword;
@ -528,7 +567,9 @@ export type MutationChallengeArgs = {
export type MutationCheckoutSessionArgs = {
plan?: BillingPlanKey;
recurringInterval: SubscriptionInterval;
requirePaymentMethod?: Scalars['Boolean'];
successUrlPath?: InputMaybe<Scalars['String']>;
};
@ -543,6 +584,11 @@ export type MutationCreateOidcIdentityProviderArgs = {
};
export type MutationCreateOneFieldArgs = {
input: CreateOneFieldMetadataInput;
};
export type MutationCreateOneServerlessFunctionArgs = {
input: CreateServerlessFunctionInput;
};
@ -563,6 +609,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
};
export type MutationDeleteOneFieldArgs = {
input: DeleteOneFieldInput;
};
export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput;
};
@ -665,6 +716,11 @@ export type MutationTrackArgs = {
};
export type MutationUpdateOneFieldArgs = {
input: UpdateOneFieldMetadataInput;
};
export type MutationUpdateOneObjectArgs = {
input: UpdateOneObjectInput;
};
@ -825,6 +881,8 @@ export type Query = {
clientConfig: ClientConfig;
currentUser: User;
currentWorkspace: Workspace;
field: Field;
fields: FieldConnection;
findAvailableWorkspacesByEmail: Array<AvailableWorkspaceOutput>;
findManyServerlessFunctions: Array<ServerlessFunction>;
findOneServerlessFunction: ServerlessFunction;
@ -1241,6 +1299,22 @@ export type UpdateBillingEntity = {
success: Scalars['Boolean'];
};
export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
isActive?: InputMaybe<Scalars['Boolean']>;
isCustom?: InputMaybe<Scalars['Boolean']>;
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
isNullable?: InputMaybe<Scalars['Boolean']>;
isSystem?: InputMaybe<Scalars['Boolean']>;
isUnique?: InputMaybe<Scalars['Boolean']>;
label?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
options?: InputMaybe<Scalars['JSON']>;
settings?: InputMaybe<Scalars['JSON']>;
};
export type UpdateObjectPayload = {
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
@ -1255,6 +1329,13 @@ export type UpdateObjectPayload = {
shortcut?: InputMaybe<Scalars['String']>;
};
export type UpdateOneFieldMetadataInput = {
/** The id of the record to update */
id: Scalars['UUID'];
/** The record to update */
update: UpdateFieldInput;
};
export type UpdateOneObjectInput = {
/** The id of the object to update */
id: Scalars['UUID'];
@ -1944,6 +2025,8 @@ export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSes
export type CheckoutSessionMutationVariables = Exact<{
recurringInterval: SubscriptionInterval;
successUrlPath?: InputMaybe<Scalars['String']>;
plan: BillingPlanKey;
requirePaymentMethod: Scalars['Boolean'];
}>;
@ -3234,10 +3317,12 @@ export type BillingPortalSessionQueryHookResult = ReturnType<typeof useBillingPo
export type BillingPortalSessionLazyQueryHookResult = ReturnType<typeof useBillingPortalSessionLazyQuery>;
export type BillingPortalSessionQueryResult = Apollo.QueryResult<BillingPortalSessionQuery, BillingPortalSessionQueryVariables>;
export const CheckoutSessionDocument = gql`
mutation CheckoutSession($recurringInterval: SubscriptionInterval!, $successUrlPath: String) {
mutation CheckoutSession($recurringInterval: SubscriptionInterval!, $successUrlPath: String, $plan: BillingPlanKey!, $requirePaymentMethod: Boolean!) {
checkoutSession(
recurringInterval: $recurringInterval
successUrlPath: $successUrlPath
plan: $plan
requirePaymentMethod: $requirePaymentMethod
) {
url
}
@ -3260,6 +3345,8 @@ export type CheckoutSessionMutationFn = Apollo.MutationFunction<CheckoutSessionM
* variables: {
* recurringInterval: // value for 'recurringInterval'
* successUrlPath: // value for 'successUrlPath'
* plan: // value for 'plan'
* requirePaymentMethod: // value for 'requirePaymentMethod'
* },
* });
*/

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const copilotQueryState = createState({
key: 'activities/copilot-query',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const emailThreadIdWhenEmailThreadWasClosedState = createState<
string | null

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const canCreateActivityState = createState<boolean>({
key: 'canCreateActivityState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isActivityInCreateModeState = createState<boolean>({
key: 'isActivityInCreateModeState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isUpsertingActivityInDBState = createState<boolean>({
key: 'isUpsertingActivityInDBState',

View File

@ -7,26 +7,29 @@ import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHa
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil';
import { RecoilURLSyncJSON } from 'recoil-sync';
import { IconsProvider } from 'twenty-ui';
export const App = () => {
return (
<RecoilRoot>
<AppErrorBoundary>
<CaptchaProvider>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<AppRouter />
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarProviderScope>
</CaptchaProvider>
</AppErrorBoundary>
<RecoilURLSyncJSON location={{ part: 'queryParams' }}>
<AppErrorBoundary>
<CaptchaProvider>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<ExceptionHandlerProvider>
<HelmetProvider>
<AppRouter />
</HelmetProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarProviderScope>
</CaptchaProvider>
</AppErrorBoundary>
</RecoilURLSyncJSON>
</RecoilRoot>
);
};

View File

@ -43,16 +43,17 @@ import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTi
import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { AppPath } from '@/types/AppPath';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@ -382,6 +383,7 @@ export const useAuth = () => {
params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
},
) => {
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
@ -394,6 +396,12 @@ export const useAuth = () => {
params.workspacePersonalInviteToken,
);
}
if (isDefined(params.billingCheckoutSession)) {
url.searchParams.set(
'billingCheckoutSessionState',
JSON.stringify(params.billingCheckoutSession),
);
}
if (isDefined(workspaceSubdomain)) {
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
@ -408,6 +416,7 @@ export const useAuth = () => {
(params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
}) => {
redirect(buildRedirectUrl('/auth/google', params));
},
@ -418,6 +427,7 @@ export const useAuth = () => {
(params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
billingCheckoutSession?: BillingCheckoutSession;
}) => {
redirect(buildRedirectUrl('/auth/microsoft', params));
},

View File

@ -1,7 +1,9 @@
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { BillingPlanKey, SubscriptionInterval } from '~/generated/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
@ -13,10 +15,24 @@ jest.mock('@/auth/hooks/useAuth', () => ({
}));
describe('useSignInWithGoogle', () => {
const mockBillingCheckoutSession = {
plan: BillingPlanKey.Pro,
interval: SubscriptionInterval.Month,
requirePaymentMethod: true,
skipPlanPage: false,
};
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
it('should call signInWithGoogle with correct params', () => {
const signInWithGoogleMock = jest.fn();
const mockUseParams = { workspaceInviteHash: 'testHash' };
const mockSearchParams = new URLSearchParams('inviteToken=testToken');
const mockSearchParams = new URLSearchParams(
'inviteToken=testToken&billingCheckoutSessionState={"plan":"Pro","interval":"Month","requirePaymentMethod":true,"skipPlanPage":false}',
);
(useParams as jest.Mock).mockReturnValue(mockUseParams);
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams]);
@ -24,12 +40,15 @@ describe('useSignInWithGoogle', () => {
signInWithGoogle: signInWithGoogleMock,
});
const { result } = renderHook(() => useSignInWithGoogle());
const { result } = renderHook(() => useSignInWithGoogle(), {
wrapper: Wrapper,
});
result.current.signInWithGoogle();
expect(signInWithGoogleMock).toHaveBeenCalledWith({
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: 'testToken',
billingCheckoutSession: mockBillingCheckoutSession,
});
});
@ -44,12 +63,15 @@ describe('useSignInWithGoogle', () => {
signInWithGoogle: signInWithGoogleMock,
});
const { result } = renderHook(() => useSignInWithGoogle());
const { result } = renderHook(() => useSignInWithGoogle(), {
wrapper: Wrapper,
});
result.current.signInWithGoogle();
expect(signInWithGoogleMock).toHaveBeenCalledWith({
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: undefined,
billingCheckoutSession: mockBillingCheckoutSession,
});
});
});

View File

@ -1,7 +1,8 @@
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
@ -13,6 +14,17 @@ jest.mock('@/auth/hooks/useAuth', () => ({
}));
describe('useSignInWithMicrosoft', () => {
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
const mockBillingCheckoutSession = {
plan: 'PRO',
interval: 'Month',
requirePaymentMethod: true,
skipPlanPage: false,
};
it('should call signInWithMicrosoft with the correct parameters', () => {
const workspaceInviteHashMock = 'testHash';
const inviteTokenMock = 'testToken';
@ -28,12 +40,15 @@ describe('useSignInWithMicrosoft', () => {
signInWithMicrosoft: signInWithMicrosoftMock,
});
const { result } = renderHook(() => useSignInWithMicrosoft());
const { result } = renderHook(() => useSignInWithMicrosoft(), {
wrapper: Wrapper,
});
result.current.signInWithMicrosoft();
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: inviteTokenMock,
billingCheckoutSession: mockBillingCheckoutSession,
});
});
@ -49,10 +64,13 @@ describe('useSignInWithMicrosoft', () => {
signInWithMicrosoft: signInWithMicrosoftMock,
});
const { result } = renderHook(() => useSignInWithMicrosoft());
const { result } = renderHook(() => useSignInWithMicrosoft(), {
wrapper: Wrapper,
});
result.current.signInWithMicrosoft();
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
billingCheckoutSession: mockBillingCheckoutSession,
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: undefined,
});

View File

@ -1,15 +1,27 @@
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
export const useSignInWithGoogle = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const [searchParams] = useSearchParams();
const workspacePersonalInviteToken =
searchParams.get('inviteToken') ?? undefined;
const billingCheckoutSession = {
plan: 'PRO',
interval: 'Month',
requirePaymentMethod: true,
skipPlanPage: false,
} as BillingCheckoutSession;
const { signInWithGoogle } = useAuth();
return {
signInWithGoogle: () =>
signInWithGoogle({ workspaceInviteHash, workspacePersonalInviteToken }),
signInWithGoogle({
workspaceInviteHash,
workspacePersonalInviteToken,
billingCheckoutSession,
}),
};
};

View File

@ -1,18 +1,23 @@
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { useRecoilValue } from 'recoil';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const [searchParams] = useSearchParams();
const workspacePersonalInviteToken =
searchParams.get('inviteToken') ?? undefined;
const billingCheckoutSession = useRecoilValue(billingCheckoutSessionState);
const { signInWithMicrosoft } = useAuth();
return {
signInWithMicrosoft: () =>
signInWithMicrosoft({
workspaceInviteHash,
workspacePersonalInviteToken,
billingCheckoutSession,
}),
};
};

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { UserExists } from '~/generated/graphql';
export const availableSSOIdentityProvidersForAuthState = createState<

View File

@ -0,0 +1,39 @@
import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { createState } from '@ui/utilities/state/utils/createState';
import { syncEffect } from 'recoil-sync';
import { BillingPlanKey, SubscriptionInterval } from '~/generated/graphql';
export const billingCheckoutSessionState = createState<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: {
plan: BillingPlanKey.Pro,
interval: SubscriptionInterval.Month,
requirePaymentMethod: true,
skipPlanPage: false,
},
effects: [
syncEffect({
refine: (value: unknown) => {
if (
typeof value === 'object' &&
value !== null &&
'plan' in value &&
'interval' in value &&
'requirePaymentMethod' in value &&
'skipPlanPage' in value
) {
return {
type: 'success',
value: value as BillingCheckoutSession,
warnings: [],
} as const;
}
return {
type: 'failure',
message: 'Invalid BillingCheckoutSessionState',
path: [] as any,
} as const;
},
}),
],
});

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { User } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';

View File

@ -1,5 +1,5 @@
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const currentWorkspaceMembersState = createState<
CurrentWorkspaceMember[]

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Workspace } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isCurrentUserLoadedState = createState<boolean>({
key: 'isCurrentUserLoadedState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isVerifyPendingState = createState<boolean>({
key: 'isVerifyPendingState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const previousUrlState = createState<string>({
key: 'previousUrlState',

View File

@ -1,5 +1,5 @@
import { createState } from 'twenty-ui';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { createState } from '@ui/utilities/state/utils/createState';
export const signInUpModeState = createState<SignInUpMode>({
key: 'signInUpModeState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export enum SignInUpStep {
Init = 'init',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { AuthTokenPair } from '~/generated/graphql';
import { cookieStorageEffect } from '~/utils/recoil-effects';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { PublicWorkspaceDataOutput } from '~/generated/graphql';
export const workspacePublicDataState =

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Workspace } from '~/generated/graphql';

View File

@ -0,0 +1,9 @@
import { SubscriptionInterval } from '~/generated-metadata/graphql';
import { BillingPlanKey } from '~/generated/graphql';
export type BillingCheckoutSession = {
plan: BillingPlanKey;
interval: SubscriptionInterval;
requirePaymentMethod: boolean;
skipPlanPage: boolean;
};

View File

@ -4,10 +4,14 @@ export const CHECKOUT_SESSION = gql`
mutation CheckoutSession(
$recurringInterval: SubscriptionInterval!
$successUrlPath: String
$plan: BillingPlanKey!
$requirePaymentMethod: Boolean!
) {
checkoutSession(
recurringInterval: $recurringInterval
successUrlPath: $successUrlPath
plan: $plan
requirePaymentMethod: $requirePaymentMethod
) {
url
}

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const captchaTokenState = createState<string | undefined>({
key: 'captchaTokenState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isCaptchaScriptLoadedState = createState<boolean>({
key: 'isCaptchaScriptLoadedState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isRequestingCaptchaTokenState = createState<boolean>({
key: 'isRequestingCaptchaTokenState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
key: 'isLoadingTokensFromExtensionState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { ApiConfig } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { AuthProviders } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Billing } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Captcha } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const chromeExtensionIdState = createState<string | null | undefined>({
key: 'chromeExtensionIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
type ClientConfigApiStatus = {
isLoaded: boolean;

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isAnalyticsEnabledState = createState<boolean>({
key: 'isAnalyticsEnabled',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isDebugModeState = createState<boolean>({
key: 'isDebugModeState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isDeveloperDefaultSignInPrefilledState = createState<boolean>({
key: 'isDeveloperDefaultSignInPrefilledState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isMultiWorkspaceEnabledState = createState<boolean>({
key: 'isMultiWorkspaceEnabled',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isSSOEnabledState = createState<boolean>({
key: 'isSSOEnabledState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Sentry } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Support } from '~/generated/graphql';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const commandMenuSearchState = createState<string>({
key: 'command-menu/commandMenuSearchState',

View File

@ -1,5 +1,5 @@
import { CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE } from '@/context-store/constants/ContextStoreInstanceIdDefaultValue';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const mainContextStoreComponentInstanceIdState = createState<string>({
key: 'mainContextStoreComponentInstanceIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { ClientConfig } from '~/generated/graphql';
export const domainConfigurationState = createState<

View File

@ -1,5 +1,5 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { cookieStorageEffect } from '~/utils/recoil-effects';
import { createState } from 'twenty-ui';
export const lastAuthenticatedWorkspaceDomainState = createState<
| {

View File

@ -1,7 +1,7 @@
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const dateTimeFormatState = createState<{
timeZone: string;

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const currentMobileNavigationDrawerState = createState<
'main' | 'settings'

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isAppWaitingForFreshObjectMetadataState = createState<boolean>({
key: 'isAppWaitingForFreshObjectMetadataState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';

View File

@ -1,5 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
type AggregateOperation = {
operation: AGGREGATE_OPERATIONS | null;

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const lastShowPageRecordIdState = createState<string | null>({
key: 'lastShowPageRecordIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const recordIndexIsCompactModeActiveState = createState<boolean>({
key: 'recordIndexIsCompactModeActiveState',

View File

@ -1,5 +1,5 @@
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export type KanbanAggregateOperation = {
operation?: AGGREGATE_OPERATIONS | null;

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const recordIndexKanbanFieldMetadataIdState = createState<string | null>(
{

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { Sort } from '@/object-record/object-sort-dropdown/types/Sort';

View File

@ -1,5 +1,5 @@
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const recordIndexViewFilterGroupsState = createState<ViewFilterGroup[]>({
key: 'recordIndexViewFilterGroupsState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { ViewType } from '@/views/types/ViewType';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isNewViewableRecordLoadingState = createState<boolean>({
key: 'activities/is-new-viewable-record-loading',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const viewableRecordIdState = createState<string | null>({
key: 'activities/viewable-record-id',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const viewableRecordNameSingularState = createState<string | null>({
key: 'activities/viewable-record-name-singular',

View File

@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react';
import { createState } from '@ui/utilities/state/utils/createState';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { createState } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';

View File

@ -1,7 +1,7 @@
import { renderHook } from '@testing-library/react';
import { createState } from '@ui/utilities/state/utils/createState';
import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { createState } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isRemoveSortingModalOpenState = createState<boolean>({
key: 'isRemoveSortingModalOpenState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isSoftFocusUsingMouseState = createState<boolean>({
key: 'isSoftFocusUsingMouseState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const settingsPreviewRecordIdState = createState<string | null>({
key: 'settingsPreviewRecordIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const apiKeyTokenState = createState<string | null>({
key: 'apiKeyTokenState',

View File

@ -1,7 +1,7 @@
/* @license Enterprise */
import { SSOIdentityProvider } from '@/settings/security/types/SSOIdentityProvider';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const SSOIdentitiesProvidersState = createState<
Omit<SSOIdentityProvider, '__typename'>[]

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { SpreadsheetImportDialogOptions } from '../types';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const activeDropdownFocusIdState = createState<string | null>({
key: 'activeDropdownFocusIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const previousDropdownFocusIdState = createState<string | null>({
key: 'previousDropdownFocusIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerAnimationCompletedState = createState<boolean>({
key: 'isRightDrawerAnimationCompletedState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerMinimizedState = createState<boolean>({
key: 'ui/layout/is-right-drawer-minimized',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerOpenState = createState<boolean>({
key: 'ui/layout/is-right-drawer-open',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { MessageThread } from '@/activities/emails/types/MessageThread';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const rightDrawerCloseEventState = createState<Event | null>({
key: 'rightDrawerCloseEventState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { RightDrawerTopBarDropdownButtons } from '@/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { RightDrawerPages } from '../types/RightDrawerPages';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const isDefaultLayoutAuthModalVisibleState = createState<boolean>({
key: 'isDefaultLayoutAuthModalVisibleState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const navigationMemorizedUrlState = createState<string>({
key: 'navigationMemorizedUrlState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export type StepsState = {
activeStep: number;

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { INITIAL_HOTKEYS_SCOPE } from '../../constants/InitialHotkeysScope';
import { HotkeyScope } from '../../types/HotkeyScope';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const internalHotkeysEnabledScopesState = createState<string[]>({
key: 'internalHotkeysEnabledScopesState',

View File

@ -1,5 +1,5 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { Keys } from 'react-hotkeys-hook/dist/types';
import { createState } from 'twenty-ui';
export const pendingHotkeyState = createState<Keys | null>({
key: 'pendingHotkeyState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
import { HotkeyScope } from '../../types/HotkeyScope';

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const currentPageLocationState = createState<string>({
key: 'currentPageLocationState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const openOverrideWorkflowDraftConfirmationModalState =
createState<boolean>({

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const workflowCreateStepFromParentStepIdState = createState<
string | undefined

View File

@ -1,5 +1,5 @@
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const workflowDiagramState = createState<WorkflowDiagram | undefined>({
key: 'workflowDiagramState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const workflowDiagramTriggerNodeSelectionState = createState<
string | undefined

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const workflowIdState = createState<string | undefined>({
key: 'workflowIdState',

View File

@ -1,4 +1,4 @@
import { createState } from 'twenty-ui';
import { createState } from '@ui/utilities/state/utils/createState';
export const workflowLastCreatedStepIdState = createState<string | undefined>({
key: 'workflowLastCreatedStepIdState',

Some files were not shown because too many files have changed in this diff Show More