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

@ -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;
};