+
diff --git a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.jsx b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.jsx
index 4900260d4d..136da130e1 100644
--- a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.jsx
+++ b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.jsx
@@ -4,10 +4,9 @@ import { useParams } from 'react-router-dom'
import config from 'config'
import { useOwnerPageData } from 'pages/OwnerPage/hooks'
-import { useAccountDetails, usePlanData } from 'services/account'
+import { usePlanData } from 'services/account'
import ExceededUploadsAlert from './ExceededUploadsAlert'
-import GithubConfigBanner from './GithubConfigBanner'
import ReachingUploadLimitAlert from './ReachingUploadLimitAlert'
const useUploadsInfo = () => {
@@ -31,14 +30,9 @@ const useUploadsInfo = () => {
return { isUploadLimitExceeded, isApproachingUploadLimit }
}
-const AlertBanners = ({
- isUploadLimitExceeded,
- isApproachingUploadLimit,
- hasGhApp,
-}) => {
+const AlertBanners = ({ isUploadLimitExceeded, isApproachingUploadLimit }) => {
return (
<>
- {!hasGhApp && }
{isUploadLimitExceeded ? (
) : isApproachingUploadLimit ? (
@@ -55,17 +49,8 @@ AlertBanners.propTypes = {
}
export default function HeaderBanners() {
- const { owner, provider } = useParams()
- // TODO: refactor this to add a gql field for the integration id used to determine if the org has a GH app
- const { data: accountDetails } = useAccountDetails({
- provider,
- owner,
- })
-
const { isUploadLimitExceeded, isApproachingUploadLimit } = useUploadsInfo()
- const hasGhApp = !!accountDetails?.integrationId
-
if (config.IS_SELF_HOSTED) {
return null
}
@@ -75,7 +60,6 @@ export default function HeaderBanners() {
>
)
diff --git a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx
index bfa97996c2..fc070874ca 100644
--- a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx
+++ b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx
@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { render, screen, waitFor } from '@testing-library/react'
+import { render, screen } from '@testing-library/react'
import { graphql, http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { MemoryRouter, Route } from 'react-router-dom'
@@ -225,29 +225,6 @@ describe('HeaderBanners', () => {
})
})
- describe('user does not have gh app installed', () => {
- beforeEach(() => {
- setup({
- integrationId: null,
- })
- })
-
- it('displays github app config banner', async () => {
- render(
- ,
- { wrapper }
- )
-
- await waitFor(() => {
- const banner = screen.getByText("Codecov's GitHub app")
- return expect(banner).toBeInTheDocument()
- })
- })
- })
-
describe('user is running in self hosted mode', () => {
beforeEach(() => {
setup({
diff --git a/src/pages/OwnerPage/OnboardingContainerContext/context.test.tsx b/src/pages/OwnerPage/OnboardingContainerContext/context.test.tsx
new file mode 100644
index 0000000000..0b50a7b69b
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingContainerContext/context.test.tsx
@@ -0,0 +1,101 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { MemoryRouter, Route, Switch } from 'react-router-dom'
+
+import { LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER } from 'pages/OwnerPage/OnboardingOrg/constants'
+import { ONBOARDING_SOURCE } from 'pages/TermsOfService/constants'
+
+import { OnboardingContainerProvider, useOnboardingContainer } from './context'
+
+const wrapper: React.FC = ({ children }) => (
+
+
+
+ {children}
+
+
+
+)
+
+const noQueryParamWrapper: React.FC = ({
+ children,
+}) => (
+
+
+
+ {children}
+
+
+
+)
+
+const TestComponent = () => {
+ const { showOnboardingContainer, setShowOnboardingContainer } =
+ useOnboardingContainer()
+
+ return (
+
+
Show container: {showOnboardingContainer.toString()}
+
+
+ )
+}
+
+describe('OnboardingContainer context', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ })
+
+ describe('when called outside of provider', () => {
+ it('throws error', () => {
+ console.error = () => {}
+ expect(() => render()).toThrow(
+ 'useOnboardingContainer has to be used within ``'
+ )
+ })
+ })
+
+ describe('when called inside provider', () => {
+ it('initializes with false when no localStorage value exists', () => {
+ render(, { wrapper: noQueryParamWrapper })
+
+ expect(screen.getByText('Show container: false')).toBeInTheDocument()
+ })
+
+ it('initializes with true when source param is onboarding', () => {
+ render(, { wrapper })
+
+ expect(screen.getByText('Show container: true')).toBeInTheDocument()
+ expect(
+ localStorage.getItem(LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER)
+ ).toBe('true')
+ })
+
+ it('initializes with stored localStorage value', () => {
+ localStorage.setItem(LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER, 'true')
+
+ render(, { wrapper: noQueryParamWrapper })
+
+ expect(screen.getByText('Show container: true')).toBeInTheDocument()
+ })
+
+ it('can toggle the container visibility', async () => {
+ const user = userEvent.setup()
+
+ render(, { wrapper })
+
+ expect(screen.getByText('Show container: true')).toBeInTheDocument()
+
+ const button = screen.getByRole('button', { name: 'toggle container' })
+ await user.click(button)
+
+ expect(screen.getByText('Show container: false')).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/pages/OwnerPage/OnboardingContainerContext/context.tsx b/src/pages/OwnerPage/OnboardingContainerContext/context.tsx
new file mode 100644
index 0000000000..2983011291
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingContainerContext/context.tsx
@@ -0,0 +1,82 @@
+import { createContext, useContext, useState } from 'react'
+import { useParams } from 'react-router-dom'
+
+import { LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER } from 'pages/OwnerPage/OnboardingOrg/constants'
+import { ONBOARDING_SOURCE } from 'pages/TermsOfService/constants'
+import { useLocationParams } from 'services/navigation'
+import { Provider } from 'shared/api/helpers'
+import { providerToName } from 'shared/utils/provider'
+
+type OnboardingContainerContextValue = {
+ showOnboardingContainer: boolean
+ setShowOnboardingContainer: (showOnboardingContainer: boolean) => void
+}
+
+export const OnboardingContainerContext =
+ createContext(null)
+
+interface URLParams {
+ provider: Provider
+}
+
+export const OnboardingContainerProvider: React.FC = ({
+ children,
+}) => {
+ const {
+ params,
+ }: {
+ params: { source?: string }
+ } = useLocationParams()
+ const { provider } = useParams()
+ const isGh = providerToName(provider) === 'GitHub'
+ if (
+ // this should only show for newly onboarded GH users
+ isGh &&
+ params['source'] === ONBOARDING_SOURCE &&
+ localStorage.getItem(LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER) === null
+ ) {
+ localStorage.setItem(LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER, 'true')
+ }
+ const localStorageValue = localStorage.getItem(
+ LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER
+ )
+
+ const [showOnboardingContainer, setShowFunction] = useState(
+ localStorageValue === 'true' ? true : false
+ )
+
+ const setShowOnboardingContainer = (showOnboardingContainer: boolean) => {
+ if (isGh) {
+ setShowFunction(showOnboardingContainer)
+ localStorage.setItem(
+ LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER,
+ showOnboardingContainer ? 'true' : 'false'
+ )
+ }
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+OnboardingContainerContext.displayName = 'OnboardingContainerContext'
+
+export function useOnboardingContainer() {
+ const rawContext = useContext(OnboardingContainerContext)
+
+ if (rawContext === null) {
+ throw new Error(
+ 'useOnboardingContainer has to be used within ``'
+ )
+ }
+
+ return rawContext
+}
diff --git a/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.test.tsx b/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.test.tsx
new file mode 100644
index 0000000000..77ba2f80f9
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.test.tsx
@@ -0,0 +1,82 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { MemoryRouter, Route } from 'react-router-dom'
+import { type Mock, vi } from 'vitest'
+
+import { useLocationParams } from 'services/navigation'
+
+import OnboardingOrg from './OnboardingOrg'
+
+import { OnboardingContainerProvider } from '../OnboardingContainerContext/context'
+
+vi.mock('services/navigation', async () => {
+ const servicesNavigation = await vi.importActual('services/navigation')
+
+ return {
+ ...servicesNavigation,
+ useLocationParams: vi.fn(),
+ }
+})
+
+const mockedUseLocationParams = useLocationParams as Mock
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, suspense: false },
+ },
+})
+
+const wrapper: React.FC = ({ children }) => (
+
+
+
+ {children}
+
+
+
+)
+
+describe('OnboardingOrg', () => {
+ beforeEach(() => {
+ localStorage.clear()
+ mockedUseLocationParams.mockReturnValue({ params: {} })
+ })
+
+ it('renders the component correctly', () => {
+ render(, { wrapper })
+
+ expect(
+ screen.getByText('How to integrate another organization to Codecov')
+ ).toBeInTheDocument()
+ expect(
+ screen.getByText('Add your GitHub Organization to Codecov')
+ ).toBeInTheDocument()
+ expect(screen.getByText('Install Codecov')).toBeInTheDocument()
+ expect(screen.getByText('Dismiss')).toBeInTheDocument()
+ expect(
+ screen.getByAltText('GitHub Organization Install List Example')
+ ).toBeInTheDocument()
+ })
+
+ it('opens and closes the AppInstallModal', async () => {
+ const user = userEvent.setup()
+ render(, { wrapper })
+
+ // Modal should be closed initially
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+
+ // Click install button to open the modal
+ const installButton = screen.getByText('Install Codecov')
+ await user.click(installButton)
+
+ // Modal should be open
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
+
+ const cancelButton = screen.getByRole('button', { name: /Cancel/i })
+ await user.click(cancelButton)
+
+ // Modal should be closed
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.tsx b/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.tsx
new file mode 100644
index 0000000000..8d929815fa
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingOrg/OnboardingOrg.tsx
@@ -0,0 +1,77 @@
+import { useState } from 'react'
+
+import orgListInstallApp from 'assets/onboarding/org_list_install_app.png'
+import { eventTracker } from 'services/events/events'
+import AppInstallModal from 'shared/AppInstallModal'
+import Button from 'ui/Button'
+
+import { useOnboardingContainer } from '../OnboardingContainerContext/context'
+
+function OnboardingOrg() {
+ const { showOnboardingContainer, setShowOnboardingContainer } =
+ useOnboardingContainer()
+
+ const dismiss = () => {
+ setShowOnboardingContainer(!showOnboardingContainer)
+ }
+
+ const [showModal, setShowModal] = useState(false)
+
+ return (
+ <>
+
+
+
+ How to integrate another organization to Codecov
+
+
+
+
+
+

+
+
+
+ Add your GitHub Organization to Codecov
+
+
+ To get full access, you need to install the Codecov app on your
+ GitHub organization. Admin required.
+
+
+
+
+
+
+
+ setShowModal(false)}
+ onComplete={() => setShowModal(false)}
+ />
+ >
+ )
+}
+
+export default OnboardingOrg
diff --git a/src/pages/OwnerPage/OnboardingOrg/constants.ts b/src/pages/OwnerPage/OnboardingOrg/constants.ts
new file mode 100644
index 0000000000..99bfb5a0ba
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingOrg/constants.ts
@@ -0,0 +1,2 @@
+export const LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER =
+ 'show-onboarding-container'
diff --git a/src/pages/OwnerPage/OnboardingOrg/index.ts b/src/pages/OwnerPage/OnboardingOrg/index.ts
new file mode 100644
index 0000000000..b4629aec61
--- /dev/null
+++ b/src/pages/OwnerPage/OnboardingOrg/index.ts
@@ -0,0 +1,2 @@
+export { default } from './OnboardingOrg'
+export { LOCAL_STORAGE_SHOW_ONBOARDING_CONTAINER } from './constants'
diff --git a/src/pages/OwnerPage/OwnerPage.jsx b/src/pages/OwnerPage/OwnerPage.jsx
index 957429c3b4..06af7655ba 100644
--- a/src/pages/OwnerPage/OwnerPage.jsx
+++ b/src/pages/OwnerPage/OwnerPage.jsx
@@ -4,13 +4,15 @@ import { useHistory, useParams } from 'react-router-dom'
import SilentNetworkErrorWrapper from 'layouts/shared/SilentNetworkErrorWrapper'
import NotFound from 'pages/NotFound'
import { useOwnerPageData } from 'pages/OwnerPage/hooks'
-import { useSentryToken } from 'services/account'
+import { useAccountDetails, useSentryToken } from 'services/account'
import { useLocationParams } from 'services/navigation'
import { renderToast } from 'services/toast'
import { ActiveContext } from 'shared/context'
import ListRepo from 'shared/ListRepo'
import HeaderBanners from './HeaderBanners'
+import { useOnboardingContainer } from './OnboardingContainerContext/context'
+import OnboardingOrg from './OnboardingOrg'
import Tabs from './Tabs'
export const LOCAL_STORAGE_USER_STARTED_TRIAL_KEY = 'user-started-trial'
@@ -36,7 +38,7 @@ const useSentryTokenRedirect = ({ ownerData }) => {
}
function OwnerPage() {
- const { provider } = useParams()
+ const { owner, provider } = useParams()
const { data: ownerData } = useOwnerPageData()
const { params } = useLocationParams({
repoDisplay: 'All',
@@ -47,6 +49,8 @@ function OwnerPage() {
LOCAL_STORAGE_USER_STARTED_TRIAL_KEY
)
+ const { showOnboardingContainer } = useOnboardingContainer()
+
useEffect(() => {
if (userStartedTrial) {
renderToast({
@@ -62,6 +66,14 @@ function OwnerPage() {
}
}, [userStartedTrial])
+ // TODO: refactor this to add a gql field for the integration id used to determine if the org has a GH app
+ const { data: accountDetails } = useAccountDetails({
+ provider,
+ owner,
+ })
+
+ const hasGhApp = !!accountDetails?.integrationId
+
if (!ownerData) {
return
}
@@ -74,11 +86,15 @@ function OwnerPage() {
+ {showOnboardingContainer ?
: null}
{ownerData?.isCurrentUserPartOfOrg && (
)}
-
+
diff --git a/src/pages/OwnerPage/OwnerPage.test.jsx b/src/pages/OwnerPage/OwnerPage.test.jsx
index d9868db345..4159b85c18 100644
--- a/src/pages/OwnerPage/OwnerPage.test.jsx
+++ b/src/pages/OwnerPage/OwnerPage.test.jsx
@@ -1,9 +1,10 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
-import { graphql, HttpResponse } from 'msw'
+import { graphql, http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { MemoryRouter, Route } from 'react-router-dom'
+import { OnboardingContainerProvider } from './OnboardingContainerContext/context'
import OwnerPage from './OwnerPage'
const mocks = vi.hoisted(() => ({
@@ -31,14 +32,16 @@ let testLocation
const wrapper = ({ children }) => (
- {children}
- {
- testLocation = location
- return null
- }}
- />
+
+ {children}
+ {
+ testLocation = location
+ return null
+ }}
+ />
+
)
@@ -56,9 +59,10 @@ afterAll(() => {
describe('OwnerPage', () => {
function setup(
- { owner, successfulMutation = true } = {
+ { owner, successfulMutation = true, integrationId = 9 } = {
owner: null,
successfulMutation: true,
+ integrationId: 9,
}
) {
server.use(
@@ -82,6 +86,9 @@ describe('OwnerPage', () => {
return HttpResponse.json({
data: { saveSentryState: null },
})
+ }),
+ http.get('/internal/gh/codecov/account-details/', () => {
+ return HttpResponse.json({ integrationId })
})
)
}
diff --git a/src/pages/RepoPage/RepoPage.test.tsx b/src/pages/RepoPage/RepoPage.test.tsx
index f2d373c070..79fa226323 100644
--- a/src/pages/RepoPage/RepoPage.test.tsx
+++ b/src/pages/RepoPage/RepoPage.test.tsx
@@ -105,7 +105,6 @@ const mockUser = {
student: false,
studentCreatedAt: null,
studentUpdatedAt: null,
- customerIntent: 'PERSONAL',
},
trackingMetadata: {
service: 'github',
diff --git a/src/pages/TermsOfService/TermsOfService.test.tsx b/src/pages/TermsOfService/TermsOfService.test.tsx
index 048ab16f6e..fd68f26516 100644
--- a/src/pages/TermsOfService/TermsOfService.test.tsx
+++ b/src/pages/TermsOfService/TermsOfService.test.tsx
@@ -248,9 +248,6 @@ describe('TermsOfService', () => {
/I agree to the TOS and privacy policy/i
)
- const customerIntent = screen.getByRole('radio', { name: /Personal use/ })
- await user.click(customerIntent)
-
await user.click(selectedTos)
const submit = await screen.findByRole('button', { name: /Continue/ })
@@ -263,7 +260,7 @@ describe('TermsOfService', () => {
businessEmail: 'personal@cr.com',
termsAgreement: true,
marketingConsent: false,
- customerIntent: 'PERSONAL',
+ name: 'Chetney',
},
})
)
@@ -298,7 +295,7 @@ describe('TermsOfService', () => {
'case #1',
{
validationDescription:
- 'user has email, signs TOS, submit is now enabled',
+ 'user has email and name, signs TOS, submit is now enabled',
internalUserData: {
email: 'personal@cr.com',
termsAgreement: false,
@@ -309,15 +306,15 @@ describe('TermsOfService', () => {
},
[expectPageIsReady],
[expectSubmitIsDisabled],
+ [expectPrepopulatedFields, { email: 'personal@cr.com', name: 'Chetney' }],
[expectUserSignsTOS],
- [expectUserToChooseCustomerIntent],
[expectSubmitIsEnabled],
],
[
'case #2',
{
validationDescription:
- 'user wants to receive emails, signs TOS, submit is now enabled',
+ 'user has email and name, user wants to receive emails, signs TOS, submit is now enabled',
internalUserData: {
email: 'chetney@cr.com',
termsAgreement: false,
@@ -327,38 +324,18 @@ describe('TermsOfService', () => {
},
},
[expectPageIsReady],
- [expectUserSelectsMarketingWithFoundEmail, { email: 'chetney@cr.com' }],
+ [expectPrepopulatedFields, { email: 'chetney@cr.com', name: 'Chetney' }],
[expectSubmitIsDisabled],
- [expectUserToChooseCustomerIntent],
- [expectUserSignsTOS],
- [expectSubmitIsEnabled],
- ],
- [
- 'case #3',
- {
- validationDescription:
- 'user has email, user wants to receive emails, signs TOS, submit is now enabled',
- internalUserData: {
- email: 'chetney@cr.com',
- termsAgreement: false,
- name: 'Chetney',
- externalId: '1234',
- owners: null,
- },
- },
- [expectPageIsReady],
- [expectSubmitIsDisabled],
- [expectUserSelectsMarketingWithFoundEmail, { email: 'chetney@cr.com' }],
+ [expectUserSelectsMarketing],
[expectSubmitIsDisabled],
[expectUserSignsTOS],
- [expectUserToChooseCustomerIntent],
[expectSubmitIsEnabled],
],
[
- 'case #4',
+ 'case #3',
{
validationDescription:
- 'signs TOS, decides not to, is warned they must sign and cannot submit',
+ 'has prefilled email and name, signs TOS, decides not to, is warned they must sign and cannot submit',
internalUserData: {
email: 'chetney@cr.com',
termsAgreement: false,
@@ -369,7 +346,7 @@ describe('TermsOfService', () => {
},
[expectPageIsReady],
[expectSubmitIsDisabled],
- [expectUserToChooseCustomerIntent],
+ [expectPrepopulatedFields, { email: 'chetney@cr.com', name: 'Chetney' }],
[expectUserSignsTOS],
[expectSubmitIsEnabled],
[expectUserSignsTOS],
@@ -377,10 +354,10 @@ describe('TermsOfService', () => {
[expectUserIsWarnedTOS],
],
[
- 'case #5',
+ 'case #4',
{
validationDescription:
- 'user checks marketing consent and is required to provide an email, sign TOS (check email validation messages)',
+ 'user checks marketing consent and is required to provide an email, provide a name, sign TOS (check email validation messages)',
internalUserData: {
termsAgreement: false,
name: 'Chetney',
@@ -391,21 +368,20 @@ describe('TermsOfService', () => {
},
[expectPageIsReady],
[expectSubmitIsDisabled],
- [expectEmailRequired],
[expectUserTextEntryEmailField, { email: 'chetney' }],
[expectUserIsWarnedForValidEmail],
[expectSubmitIsDisabled],
- [expectUserTextEntryEmailField, { email: '@cr.com' }],
+ [expectUserTextEntryEmailField, { email: '@hello.com' }],
[expectUserIsNotWarnedForValidEmail],
[expectSubmitIsDisabled],
+ [expectUserTextEntryNameField],
[expectUserSelectsMarketing],
[expectSubmitIsDisabled],
- [expectUserToChooseCustomerIntent],
[expectUserSignsTOS],
[expectSubmitIsEnabled],
],
[
- 'case #6',
+ 'case #5',
{
validationDescription:
'user checks marketing consent and does not provide an email, sign TOS (check email validation messages)',
@@ -418,29 +394,28 @@ describe('TermsOfService', () => {
},
},
[expectPageIsReady],
- [expectEmailRequired],
[expectSubmitIsDisabled],
[expectUserSignsTOS],
[expectSubmitIsDisabled],
- [expectUserToChooseCustomerIntent],
],
[
- 'case #7',
+ 'case #6',
{
validationDescription: 'server unknown error notification',
isUnknownError: true,
internalUserData: {
termsAgreement: false,
- email: 'personal@cr.com',
- name: 'Chetney',
+ email: '',
+ name: '',
externalId: '1234',
owners: null,
},
},
[expectPageIsReady],
+ [expectUserTextEntryEmailField, { email: 'personal@cr.com' }],
+ [expectUserTextEntryNameField],
[expectUserSignsTOS],
[expectClickSubmit],
- [expectUserToChooseCustomerIntent],
[
expectRendersServerFailureResult,
{
@@ -456,22 +431,23 @@ describe('TermsOfService', () => {
],
],
[
- 'case #8',
+ 'case #7',
{
validationDescription: 'server failure error notification',
isUnAuthError: true,
internalUserData: {
termsAgreement: false,
- email: 'personal@cr.com',
- name: 'Chetney',
+ email: '',
+ name: '',
externalId: '1234',
owners: null,
},
},
[expectPageIsReady],
+ [expectUserTextEntryEmailField, { email: 'personal@cr.com' }],
+ [expectUserTextEntryNameField],
[expectUserSignsTOS],
[expectClickSubmit],
- [expectUserToChooseCustomerIntent],
[
expectRendersServerFailureResult,
{
@@ -481,27 +457,28 @@ describe('TermsOfService', () => {
],
],
[
- 'case #9',
+ 'case #8',
{
validationDescription:
'server validation error notification (saveTerms)',
isValidationError: true,
internalUserData: {
termsAgreement: false,
- email: 'personal@cr.com',
- name: 'Chetney',
+ email: '',
+ name: '',
externalId: '1234',
owners: null,
},
},
[expectPageIsReady],
+ [expectUserTextEntryEmailField, { email: 'personal@cr.com' }],
+ [expectUserTextEntryNameField],
[expectUserSignsTOS],
[expectClickSubmit],
- [expectUserToChooseCustomerIntent],
[expectRendersServerFailureResult, 'validation error'],
],
[
- 'case #10',
+ 'case #9',
{
validationDescription:
'redirects to main root if user has already synced a provider',
@@ -526,33 +503,6 @@ describe('TermsOfService', () => {
},
[expectRedirectTo, '/gh/codecov/cool-repo'],
],
- [
- 'case #11',
- {
- validationDescription: 'provide no customer intent, does not submit',
- internalUserData: {
- termsAgreement: true,
- name: 'Chetney',
- externalId: '1234',
- email: '',
- owners: [
- {
- avatarUrl: 'http://roland.com/avatar-url',
- integrationId: null,
- name: null,
- ownerid: 2,
- stats: null,
- service: 'github',
- username: 'roland',
- },
- ],
- },
- },
- [expectPageIsReady],
- [expectSubmitIsDisabled],
- [expectUserSignsTOS],
- [expectSubmitIsDisabled],
- ],
])(
'form validation, %s',
(
@@ -693,33 +643,43 @@ async function expectPageIsReady() {
expect(welcome).toBeInTheDocument()
}
-async function expectUserToChooseCustomerIntent(user: UserEvent) {
- const customerIntent = screen.getByRole('radio', { name: /Personal use/ })
-
- await user.click(customerIntent)
+async function expectPrepopulatedFields(
+ user: UserEvent,
+ args: { email: string; name: string }
+) {
+ await waitFor(() => {
+ const emailInput = screen.getByLabelText(
+ /Enter your email/i
+ ) as HTMLInputElement
+ expect(emailInput).toHaveValue(args.email)
+ })
+ await waitFor(() => {
+ const nameInput = screen.getByLabelText(
+ /Enter your name/i
+ ) as HTMLInputElement
+ expect(nameInput).toHaveValue(args.name)
+ })
}
-async function expectUserSignsTOS(user: UserEvent) {
- const selectedTos = screen.getByLabelText(
- /I agree to the TOS and privacy policy/i
- )
-
- await user.click(selectedTos)
+async function expectUserTextEntryNameField(user: UserEvent) {
+ const nameInput = screen.getByLabelText(/Enter your name/i)
+ await user.type(nameInput, 'My name')
}
-async function expectUserSelectsMarketingWithFoundEmail(
+async function expectUserTextEntryEmailField(
user: UserEvent,
args: { email: string }
) {
- const selectedMarketing = screen.getByLabelText(
- /I would like to receive updates via email/i
- )
- const emailIsInTheLabelOfSelectedMarketing = screen.getByText(
- new RegExp(args.email, 'i')
+ const emailInput = screen.getByLabelText(/Enter your email/i)
+ await user.type(emailInput, args.email)
+}
+
+async function expectUserSignsTOS(user: UserEvent) {
+ const selectedTos = screen.getByLabelText(
+ /I agree to the TOS and privacy policy/i
)
- expect(emailIsInTheLabelOfSelectedMarketing).toBeInTheDocument()
- await user.click(selectedMarketing)
+ await user.click(selectedTos)
}
async function expectUserSelectsMarketing(user: UserEvent) {
@@ -730,15 +690,6 @@ async function expectUserSelectsMarketing(user: UserEvent) {
await user.click(selectedMarketing)
}
-async function expectUserTextEntryEmailField(
- user: UserEvent,
- args: { email: string }
-) {
- const emailInput = screen.getByLabelText(/Contact email/i)
-
- await user.type(emailInput, args.email)
-}
-
async function expectSubmitIsDisabled() {
const submit = screen.getByRole('button', { name: /Continue/ })
expect(submit).toBeDisabled()
@@ -770,17 +721,6 @@ async function expectClickSubmit(user: UserEvent) {
await user.click(submit)
}
-async function expectEmailRequired(user: UserEvent) {
- const selectedMarketing = screen.getByLabelText(
- /I would like to receive updates via email/i
- )
-
- await user.click(selectedMarketing)
-
- const emailRequired = screen.getByText(/Contact email/i)
- expect(emailRequired).toBeInTheDocument()
-}
-
async function expectRendersServerFailureResult(
user: UserEvent,
expectedError = {}
diff --git a/src/pages/TermsOfService/TermsOfService.tsx b/src/pages/TermsOfService/TermsOfService.tsx
index 1e1e1a290b..51ffa56c95 100644
--- a/src/pages/TermsOfService/TermsOfService.tsx
+++ b/src/pages/TermsOfService/TermsOfService.tsx
@@ -8,21 +8,30 @@ import config from 'config'
import { SentryBugReporter } from 'sentry'
import umbrellaSvg from 'assets/svg/umbrella.svg'
-import { CustomerIntent, useInternalUser } from 'services/user'
+import { useInternalUser } from 'services/user'
import A from 'ui/A'
import Button from 'ui/Button'
-import RadioInput from 'ui/RadioInput/RadioInput'
import TextInput from 'ui/TextInput'
+import { ONBOARDING_SOURCE } from './constants'
import { useSaveTermsAgreement } from './hooks/useTermsOfService'
const FormSchema = z.object({
- marketingEmail: z.string().email().nullish(),
+ marketingName: z.string().min(1, 'Name is required'),
+ marketingEmail: z.string().email('Invalid email'),
marketingConsent: z.boolean().nullish(),
tos: z.literal(true),
- customerIntent: z.string(),
+ apiError: z.string().nullish(),
})
+type FormData = {
+ marketingName?: string
+ marketingEmail?: string
+ marketingConsent?: boolean
+ tos?: boolean
+ apiError?: string
+}
+
interface IsDisabled {
isValid: boolean
isDirty: boolean
@@ -33,33 +42,50 @@ function isDisabled({ isValid, isDirty, isMutationLoading }: IsDisabled) {
return (!isValid && isDirty) || !isDirty || isMutationLoading
}
+interface NameInputProps {
+ register: ReturnType
['register']
+ marketingNameMessage?: string
+}
+
+function NameInput({ register, marketingNameMessage }: NameInputProps) {
+ return (
+
+
+
+
+ {marketingNameMessage && (
+
{marketingNameMessage}
+ )}
+
+
+ )
+}
+
interface EmailInputProps {
register: ReturnType['register']
marketingEmailMessage?: string
- showEmailRequired: boolean
}
-function EmailInput({
- register,
- marketingEmailMessage,
- showEmailRequired,
-}: EmailInputProps) {
- if (!showEmailRequired) return null
-
+function EmailInput({ register, marketingEmailMessage }: EmailInputProps) {
return (
{marketingEmailMessage && (
@@ -71,17 +97,40 @@ function EmailInput({
}
export default function TermsOfService() {
+ const { data: currentUser, isLoading: userIsLoading } = useInternalUser({})
const {
register,
+ reset,
handleSubmit,
formState: { isDirty, isValid, errors: formErrors },
setError,
watch,
unregister,
- } = useForm({
+ } = useForm
({
resolver: zodResolver(FormSchema),
mode: 'onChange',
+ defaultValues: {
+ marketingName: currentUser?.name || '',
+ marketingEmail: currentUser?.email || '',
+ marketingConsent: undefined,
+ tos: false,
+ // this field just used for custom form error
+ apiError: undefined,
+ },
})
+
+ useEffect(() => {
+ if (currentUser && !isDirty) {
+ reset({
+ marketingName: currentUser.name || '',
+ marketingEmail: currentUser.email || '',
+ marketingConsent: undefined,
+ tos: false,
+ apiError: undefined,
+ })
+ }
+ }, [currentUser, isDirty, reset])
+
const { mutate, isLoading: isMutationLoading } = useSaveTermsAgreement({
onSuccess: ({ data }) => {
if (data?.saveTermsAgreement?.error) {
@@ -91,7 +140,6 @@ export default function TermsOfService() {
},
onError: (error) => setError('apiError', error),
})
- const { data: currentUser, isLoading: userIsLoading } = useInternalUser({})
useLayoutEffect(() => {
if (!config.SENTRY_DSN) {
@@ -101,19 +149,19 @@ export default function TermsOfService() {
return widget.removeFromDom
}, [])
- interface FormValues {
- marketingEmail?: string
- marketingConsent?: boolean
- customerIntent?: string
- }
+ const onSubmit: SubmitHandler = (data: FormData) => {
+ if (!data.marketingName || !data.marketingEmail) return
- const onSubmit: SubmitHandler = (data: FormValues) => {
mutate({
- businessEmail: data?.marketingEmail || currentUser?.email,
- termsAgreement: true,
+ businessEmail: data.marketingEmail,
marketingConsent: data?.marketingConsent,
- customerIntent: data?.customerIntent || CustomerIntent.PERSONAL,
+ name: data.marketingName,
+ termsAgreement: true,
})
+
+ const url = new URL(window.location.href)
+ url.searchParams.set('source', ONBOARDING_SOURCE)
+ window.location.href = url.toString()
}
useEffect(() => {
@@ -139,41 +187,14 @@ export default function TermsOfService() {