Skip to content

Commit c5c4d95

Browse files
Merge remote-tracking branch 'origin/main' into sshin/stripe-5
2 parents 0df4e54 + e9ca127 commit c5c4d95

File tree

7 files changed

+110
-6
lines changed

7 files changed

+110
-6
lines changed

src/config.js

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const defaultConfig = {
1313
GH_APP: DEFAULT_GH_APP,
1414
GH_APP_AI: 'codecov-ai',
1515
SUNBURST_ENABLED: true,
16+
DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: true,
1617
}
1718

1819
export function removeReactAppPrefix(obj) {
@@ -38,6 +39,11 @@ export function removeReactAppPrefix(obj) {
3839
keys['SUNBURST_ENABLED'] = keys['SUNBURST_ENABLED'].toLowerCase() === 'true'
3940
}
4041

42+
if ('DISPLAY_SELF_HOSTED_EXPIRATION_BANNER' in keys) {
43+
keys['DISPLAY_SELF_HOSTED_EXPIRATION_BANNER'] =
44+
keys['DISPLAY_SELF_HOSTED_EXPIRATION_BANNER'].toLowerCase() === 'true'
45+
}
46+
4147
if ('SENTRY_TRACING_SAMPLE_RATE' in keys) {
4248
keys['SENTRY_TRACING_SAMPLE_RATE'] = parseFloat(
4349
keys['SENTRY_TRACING_SAMPLE_RATE']

src/config.test.js

+10
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ describe('config', () => {
7070
})
7171
})
7272

73+
describe('sets DISPLAY_SELF_HOSTED_EXPIRATION_BANNER to boolean', () => {
74+
it('sets to true', () => {
75+
const obj = { DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: 'true' }
76+
77+
expect(removeReactAppPrefix(obj)).toEqual({
78+
DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: true,
79+
})
80+
})
81+
})
82+
7383
describe('sets IS_DEDICATED_NAMESPACE to boolean', () => {
7484
it('sets to true', () => {
7585
const obj = {

src/shared/GlobalBanners/SelfHostedLicenseExpiration/SelfHostedLicenseExpiration.test.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,21 @@ describe('SelfHostedLicenseExpiration', () => {
165165
expect(resolveIssueButton).not.toBeInTheDocument()
166166
})
167167

168+
it('does not render the banner when disabled', async () => {
169+
config.IS_SELF_HOSTED = true
170+
config.IS_DEDICATED_NAMESPACE = true
171+
config.DISPLAY_SELF_HOSTED_EXPIRATION_BANNER = false
172+
setup({
173+
seatsUsed: 5,
174+
seatsLimit: 10,
175+
expirationDate: null,
176+
})
177+
render(<SelfHostedLicenseExpiration />, { wrapper: wrapper(['']) })
178+
179+
const resolveIssueButton = screen.queryByText(/Resolve issue/)
180+
expect(resolveIssueButton).not.toBeInTheDocument()
181+
})
182+
168183
it('does not render the banner when there are no seats used', async () => {
169184
config.IS_SELF_HOSTED = true
170185
config.IS_DEDICATED_NAMESPACE = true
@@ -200,6 +215,7 @@ describe('SelfHostedLicenseExpiration', () => {
200215
vi.setSystemTime(new Date('2023-08-01'))
201216
config.IS_SELF_HOSTED = true
202217
config.IS_DEDICATED_NAMESPACE = true
218+
config.DISPLAY_SELF_HOSTED_EXPIRATION_BANNER = true
203219
})
204220

205221
afterEach(() => {

src/shared/GlobalBanners/SelfHostedLicenseExpiration/SelfHostedLicenseExpiration.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ const SelfHostedLicenseExpiration = () => {
8181
const isLicenseExpiringWithin30Days = dateDiff < 31 && dateDiff >= 0
8282

8383
const shouldDisplayBanner =
84-
isSeatsLimitReached || isLicenseExpired || isLicenseExpiringWithin30Days
84+
(isSeatsLimitReached ||
85+
isLicenseExpired ||
86+
isLicenseExpiringWithin30Days) &&
87+
config.DISPLAY_SELF_HOSTED_EXPIRATION_BANNER
8588

8689
if (!shouldDisplayBanner) {
8790
return null

src/shared/GlobalTopBanners/TokenlessBanner/TokenlessBanner.test.tsx

+63-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { setupServer } from 'msw/node'
55
import { Suspense } from 'react'
66
import { MemoryRouter, Route } from 'react-router-dom'
77

8+
import { Plans } from 'shared/utils/billing'
9+
810
import TokenlessBanner from './TokenlessBanner'
911

1012
const mocks = vi.hoisted(() => ({
@@ -15,6 +17,8 @@ vi.mock('shared/featureFlags', () => ({
1517
useFlags: mocks.useFlags,
1618
}))
1719

20+
vi.mock('services/users')
21+
1822
vi.mock('./TokenRequiredBanner', () => ({
1923
default: () => 'TokenRequiredBanner',
2024
}))
@@ -41,6 +45,50 @@ afterAll(() => {
4145
server.close()
4246
})
4347

48+
const mockSignedInUser = {
49+
me: {
50+
owner: {
51+
defaultOrgUsername: 'codecov',
52+
},
53+
54+
privateAccess: true,
55+
onboardingCompleted: true,
56+
businessEmail: '[email protected]',
57+
termsAgreement: true,
58+
user: {
59+
name: 'Jane Doe',
60+
username: 'janedoe',
61+
avatarUrl: 'http://127.0.0.1/avatar-url',
62+
avatar: 'http://127.0.0.1/avatar-url',
63+
student: false,
64+
studentCreatedAt: null,
65+
studentUpdatedAt: null,
66+
customerIntent: 'PERSONAL',
67+
},
68+
trackingMetadata: {
69+
service: 'github',
70+
ownerid: 123,
71+
serviceId: '123',
72+
plan: Plans.USERS_BASIC,
73+
staff: false,
74+
hasYaml: false,
75+
bot: null,
76+
delinquent: null,
77+
didTrial: null,
78+
planProvider: null,
79+
planUserCount: 1,
80+
createdAt: 'timestamp',
81+
updatedAt: 'timestamp',
82+
profile: {
83+
createdAt: 'timestamp',
84+
otherGoal: null,
85+
typeProjects: [],
86+
goals: [],
87+
},
88+
},
89+
},
90+
}
91+
4492
const wrapper =
4593
(initialEntries = ['/gh/codecov']): React.FC<React.PropsWithChildren> =>
4694
({ children }) => (
@@ -57,9 +105,11 @@ describe('TokenlessBanner', () => {
57105
function setup({
58106
tokenlessSection = true,
59107
uploadTokenRequired = false,
108+
currentUser,
60109
}: {
61110
tokenlessSection?: boolean
62111
uploadTokenRequired?: boolean
112+
currentUser?: any
63113
} = {}) {
64114
mocks.useFlags.mockReturnValue({ tokenlessSection })
65115

@@ -74,12 +124,15 @@ describe('TokenlessBanner', () => {
74124
},
75125
},
76126
})
127+
}),
128+
graphql.query('CurrentUser', () => {
129+
return HttpResponse.json({ data: currentUser })
77130
})
78131
)
79132
}
80133

81134
it('renders nothing when tokenlessSection flag is false', () => {
82-
setup({ tokenlessSection: false })
135+
setup({ tokenlessSection: false, currentUser: mockSignedInUser })
83136
const { container } = render(<TokenlessBanner />, {
84137
wrapper: wrapper(['/gh/codecov']),
85138
})
@@ -95,7 +148,7 @@ describe('TokenlessBanner', () => {
95148
})
96149

97150
it('renders TokenRequiredBanner when uploadTokenRequired is true', async () => {
98-
setup({ uploadTokenRequired: true })
151+
setup({ uploadTokenRequired: true, currentUser: mockSignedInUser })
99152
render(<TokenlessBanner />, { wrapper: wrapper(['/gh/codecov']) })
100153

101154
await waitFor(() => {
@@ -105,7 +158,7 @@ describe('TokenlessBanner', () => {
105158
})
106159

107160
it('renders TokenNotRequiredBanner when uploadTokenRequired is false', async () => {
108-
setup({ uploadTokenRequired: false })
161+
setup({ uploadTokenRequired: false, currentUser: mockSignedInUser })
109162
render(<TokenlessBanner />, { wrapper: wrapper(['/gh/codecov']) })
110163

111164
await waitFor(() => {
@@ -115,7 +168,7 @@ describe('TokenlessBanner', () => {
115168
})
116169

117170
it('renders nothing if coming from onboarding', async () => {
118-
setup({ uploadTokenRequired: true })
171+
setup({ uploadTokenRequired: true, currentUser: mockSignedInUser })
119172
render(<TokenlessBanner />, {
120173
wrapper: wrapper(['/gh/codecov?source=onboarding']),
121174
})
@@ -128,4 +181,10 @@ describe('TokenlessBanner', () => {
128181
).not.toBeInTheDocument()
129182
})
130183
})
184+
185+
it('renders nothing when currentUser is not provided', () => {
186+
setup({ uploadTokenRequired: false, currentUser: mockSignedInUser })
187+
const { container } = render(<TokenlessBanner />, { wrapper: wrapper() })
188+
expect(container).toBeEmptyDOMElement()
189+
})
131190
})

src/shared/GlobalTopBanners/TokenlessBanner/TokenlessBanner.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom'
44
import { ONBOARDING_SOURCE } from 'pages/TermsOfService/constants'
55
import { useLocationParams } from 'services/navigation'
66
import { useUploadTokenRequired } from 'services/uploadTokenRequired'
7+
import { useUser } from 'services/user'
78
import { useFlags } from 'shared/featureFlags'
89

910
const TokenRequiredBanner = lazy(() => import('./TokenRequiredBanner'))
@@ -20,11 +21,19 @@ const TokenlessBanner: React.FC = () => {
2021
})
2122
const { provider, owner } = useParams<UseParams>()
2223
const { data } = useUploadTokenRequired({ provider, owner, enabled: !!owner })
24+
const { data: currentUser } = useUser()
2325
const { params } = useLocationParams()
2426
// @ts-expect-error useLocationParams needs to be typed
2527
const cameFromOnboarding = params['source'] === ONBOARDING_SOURCE
2628

27-
if (!tokenlessSection || !owner || !data || cameFromOnboarding) return null
29+
if (
30+
!tokenlessSection ||
31+
!owner ||
32+
!data ||
33+
!currentUser?.user ||
34+
cameFromOnboarding
35+
)
36+
return null
2837

2938
return data?.uploadTokenRequired ? (
3039
<TokenRequiredBanner />

src/shared/utils/billing.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const Plans = {
2020
USERS_TEAMY: 'users-teamy',
2121
USERS_ENTERPRISEM: 'users-enterprisem',
2222
USERS_ENTERPRISEY: 'users-enterprisey',
23+
USERS_DEVELOPER: 'users-developer',
2324
} as const
2425

2526
export type PlanName = (typeof Plans)[keyof typeof Plans]

0 commit comments

Comments
 (0)