Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into sshin/stripe-4
Browse files Browse the repository at this point in the history
  • Loading branch information
suejung-sentry committed Feb 3, 2025
2 parents 1f39176 + 2a51004 commit 5c9138b
Show file tree
Hide file tree
Showing 23 changed files with 973 additions and 73 deletions.
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultConfig = {
GH_APP: DEFAULT_GH_APP,
GH_APP_AI: 'codecov-ai',
SUNBURST_ENABLED: true,
DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: true,
}

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

if ('DISPLAY_SELF_HOSTED_EXPIRATION_BANNER' in keys) {
keys['DISPLAY_SELF_HOSTED_EXPIRATION_BANNER'] =
keys['DISPLAY_SELF_HOSTED_EXPIRATION_BANNER'].toLowerCase() === 'true'
}

if ('SENTRY_TRACING_SAMPLE_RATE' in keys) {
keys['SENTRY_TRACING_SAMPLE_RATE'] = parseFloat(
keys['SENTRY_TRACING_SAMPLE_RATE']
Expand Down
10 changes: 10 additions & 0 deletions src/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ describe('config', () => {
})
})

describe('sets DISPLAY_SELF_HOSTED_EXPIRATION_BANNER to boolean', () => {
it('sets to true', () => {
const obj = { DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: 'true' }

expect(removeReactAppPrefix(obj)).toEqual({
DISPLAY_SELF_HOSTED_EXPIRATION_BANNER: true,
})
})
})

describe('sets IS_DEDICATED_NAMESPACE to boolean', () => {
it('sets to true', () => {
const obj = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import config from 'config'

import { ThemeContextProvider } from 'shared/ThemeContext'

import { Location } from 'history'
import { UnverifiedPaymentMethodSchema } from 'services/account'
import { z } from 'zod'
import PlanPage from './PlanPage'

vi.mock('config')
Expand Down Expand Up @@ -40,9 +43,9 @@ const queryClientV5 = new QueryClientV5({
defaultOptions: { queries: { retry: false } },
})

let testLocation
let testLocation: Location<unknown>
const wrapper =
(initialEntries = '') =>
(initialEntries = ''): React.FC<React.PropsWithChildren> =>
({ children }) => (
<QueryClientProviderV5 client={queryClientV5}>
<QueryClientProvider client={queryClient}>
Expand Down Expand Up @@ -79,7 +82,13 @@ afterAll(() => {

describe('PlanPage', () => {
function setup(
{ owner, isSelfHosted = false } = {
{
owner,
isSelfHosted = false,
unverifiedPaymentMethods = [] as z.infer<
typeof UnverifiedPaymentMethodSchema
>[],
} = {
owner: {
username: 'codecov',
isCurrentUserPartOfOrg: true,
Expand All @@ -92,6 +101,17 @@ describe('PlanPage', () => {
server.use(
graphql.query('PlanPageData', () => {
return HttpResponse.json({ data: { owner } })
}),
graphql.query('UnverifiedPaymentMethods', () => {
return HttpResponse.json({
data: {
owner: {
billing: {
unverifiedPaymentMethods,
},
},
},
})
})
)
}
Expand All @@ -102,7 +122,7 @@ describe('PlanPage', () => {
owner: {
username: 'codecov',
isCurrentUserPartOfOrg: false,
numberOfUploads: null,
numberOfUploads: 0,
},
})
})
Expand All @@ -120,7 +140,7 @@ describe('PlanPage', () => {
owner: {
username: 'codecov',
isCurrentUserPartOfOrg: false,
numberOfUploads: null,
numberOfUploads: 0,
},
})
})
Expand Down Expand Up @@ -149,6 +169,34 @@ describe('PlanPage', () => {
const tabs = await screen.findByText(/Tabs/)
expect(tabs).toBeInTheDocument()
})

describe('when there are unverified payment methods', () => {
beforeEach(() => {
setup({
owner: {
username: 'codecov',
isCurrentUserPartOfOrg: true,
numberOfUploads: 30,
},
unverifiedPaymentMethods: [
{
paymentMethodId: 'pm_123',
hostedVerificationUrl: 'https://verify.stripe.com',
},
],
})
})

it('renders unverified payment method alert', async () => {
render(<PlanPage />, { wrapper: wrapper('/plan/gh/codecov') })

const alert = await screen.findByText(/Verify Your New Payment Method/)
expect(alert).toBeInTheDocument()

const link = screen.getByText('Click here')
expect(link).toHaveAttribute('href', 'https://verify.stripe.com')
})
})
})

describe('testing routes', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import config from 'config'

import { SentryRoute } from 'sentry'

import { useUnverifiedPaymentMethods } from 'services/account/useUnverifiedPaymentMethods'
import { Provider } from 'shared/api/helpers'
import { Theme, useThemeContext } from 'shared/ThemeContext'
import A from 'ui/A'
import { Alert } from 'ui/Alert'
import LoadingLogo from 'ui/LoadingLogo'

import { PlanProvider } from './context'
Expand All @@ -35,11 +39,21 @@ const Loader = () => (
</div>
)

interface URLParams {
owner: string
provider: Provider
}

function PlanPage() {
const { owner, provider } = useParams()
const { owner, provider } = useParams<URLParams>()
const { data: ownerData } = useSuspenseQueryV5(
PlanPageDataQueryOpts({ owner, provider })
)
const { data: unverifiedPaymentMethods } = useUnverifiedPaymentMethods({
provider,
owner,
})

const { theme } = useThemeContext()
const isDarkMode = theme !== Theme.LIGHT

Expand All @@ -61,6 +75,11 @@ function PlanPage() {
>
<PlanProvider>
<PlanBreadcrumb />
{unverifiedPaymentMethods && unverifiedPaymentMethods.length > 0 ? (
<UnverifiedPaymentMethodAlert
url={unverifiedPaymentMethods[0]?.hostedVerificationUrl}
/>
) : null}
<Suspense fallback={<Loader />}>
<Switch>
<SentryRoute path={path} exact>
Expand Down Expand Up @@ -90,4 +109,28 @@ function PlanPage() {
)
}

const UnverifiedPaymentMethodAlert = ({ url }: { url?: string | null }) => {
return (
<>
<Alert variant="warning">
<Alert.Title>Verify Your New Payment Method</Alert.Title>
<Alert.Description>
Your new payment method needs to be verified.{' '}
<A
href={url}
isExternal
hook="stripe-payment-method-verification"
to={undefined}
>
Click here
</A>{' '}
to complete the process. The verification code may take around 2 days
to appear on your bank statement.
</Alert.Description>
</Alert>
<br />
</>
)
}

export default PlanPage
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { MemoryRouter, Route } from 'react-router-dom'
import { z } from 'zod'

import { PlanUpdatedPlanNotificationContext } from 'pages/PlanPage/context'
import { AccountDetailsSchema, TrialStatuses } from 'services/account'
import { BillingRate, Plans } from 'shared/utils/billing'
import { AccountDetailsSchema } from 'services/account'
import { Plans } from 'shared/utils/billing'
import { AlertOptions, type AlertOptionsType } from 'ui/Alert'

import CurrentOrgPlan from './CurrentOrgPlan'
Expand All @@ -36,28 +36,6 @@ const mockNoEnterpriseAccount = {
},
}

const mockPlanDataResponse = {
baseUnitPrice: 10,
benefits: [],
billingRate: BillingRate.MONTHLY,
marketingName: 'some-name',
monthlyUploadLimit: 123,
value: Plans.USERS_PR_INAPPM,
trialStatus: TrialStatuses.NOT_STARTED,
trialStartDate: '',
trialEndDate: '',
trialTotalDays: 0,
pretrialUsersCount: 0,
planUserCount: 1,
hasSeatsLeft: true,
isEnterprisePlan: false,
isFreePlan: false,
isProPlan: false,
isSentryPlan: false,
isTeamPlan: false,
isTrialPlan: false,
}

const mockEnterpriseAccountDetailsNinetyPercent = {
owner: {
account: {
Expand Down Expand Up @@ -170,10 +148,15 @@ describe('CurrentOrgPlan', () => {
graphql.query('EnterpriseAccountDetails', () => {
return HttpResponse.json({ data: enterpriseAccountDetails })
}),
graphql.query('GetPlanData', () => {
graphql.query('CurrentOrgPlanPageData', () => {
return HttpResponse.json({
data: {
owner: { hasPrivateRepos: true, plan: { ...mockPlanDataResponse } },
owner: {
plan: { value: Plans.USERS_PR_INAPPM },
billing: {
unverifiedPaymentMethods: [],
},
},
},
})
}),
Expand Down
28 changes: 21 additions & 7 deletions src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useSuspenseQuery as useSuspenseQueryV5 } from '@tanstack/react-queryV5'
import { useParams } from 'react-router-dom'

import { usePlanUpdatedNotification } from 'pages/PlanPage/context'
import { useAccountDetails, usePlanData } from 'services/account'
import { useAccountDetails, useCurrentOrgPlanPageData } from 'services/account'
import { Provider } from 'shared/api/helpers'
import { getScheduleStart } from 'shared/plan/ScheduledPlanDetails/ScheduledPlanDetails'
import A from 'ui/A'
Expand All @@ -28,7 +28,7 @@ function CurrentOrgPlan() {
owner,
})

const { data: planData } = usePlanData({
const { data } = useCurrentOrgPlanPageData({
provider,
owner,
})
Expand All @@ -40,16 +40,28 @@ function CurrentOrgPlan() {
})
)

const hasUnverifiedPaymentMethods =
!!data?.billing?.unverifiedPaymentMethods?.length

// awaitingInitialPaymentMethodVerification is true if the
// customer needs to verify a delayed notification payment method
// like ACH for their first subscription
const awaitingInitialPaymentMethodVerification =
!accountDetails?.subscriptionDetail?.defaultPaymentMethod &&
hasUnverifiedPaymentMethods

const scheduledPhase = accountDetails?.scheduleDetail?.scheduledPhase
const isDelinquent = accountDetails?.delinquent
const isDelinquent =
accountDetails?.delinquent && !awaitingInitialPaymentMethodVerification
const scheduleStart = scheduledPhase
? getScheduleStart(scheduledPhase)
: undefined

const shouldRenderBillingDetails =
(accountDetails?.planProvider !== 'github' &&
!awaitingInitialPaymentMethodVerification &&
((accountDetails?.planProvider !== 'github' &&
!accountDetails?.rootOrganization) ||
accountDetails?.usesInvoice
accountDetails?.usesInvoice)

const planUpdatedNotification = usePlanUpdatedNotification()

Expand All @@ -62,9 +74,11 @@ function CurrentOrgPlan() {
subscriptionDetail={accountDetails?.subscriptionDetail}
/>
) : null}
<InfoMessageStripeCallback />
<InfoMessageStripeCallback
hasUnverifiedPaymentMethods={hasUnverifiedPaymentMethods}
/>
{isDelinquent ? <DelinquentAlert /> : null}
{planData?.plan ? (
{data?.plan ? (
<div className="flex flex-col gap-4 sm:mr-4 sm:flex-initial md:w-2/3 lg:w-3/4">
{planUpdatedNotification.alertOption &&
!planUpdatedNotification.isCancellation ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ const wrapper =

describe('InfoMessageStripeCallback', () => {
describe('when rendering without success or cancel in the url', () => {
const { container } = render(<InfoMessageStripeCallback />, {
wrapper: wrapper('/account/gh/codecov'),
})
const { container } = render(
<InfoMessageStripeCallback hasUnverifiedPaymentMethods={false} />,
{
wrapper: wrapper('/account/gh/codecov'),
}
)

it('doesnt render anything', () => {
expect(container).toBeEmptyDOMElement()
Expand All @@ -23,13 +26,29 @@ describe('InfoMessageStripeCallback', () => {

describe('when rendering with success in the url', () => {
it('renders a success message', async () => {
render(<InfoMessageStripeCallback />, {
wrapper: wrapper('/account/gh/codecov?success'),
})
render(
<InfoMessageStripeCallback hasUnverifiedPaymentMethods={false} />,
{
wrapper: wrapper('/account/gh/codecov?success'),
}
)

await expect(
screen.getByText(/Subscription Update Successful/)
).toBeInTheDocument()
})
})

describe('when hasUnverifiedPaymentMethods is true', () => {
it('does not enders a success message even at ?success', async () => {
const { container } = render(
<InfoMessageStripeCallback hasUnverifiedPaymentMethods={true} />,
{
wrapper: wrapper('/account/gh/codecov?success'),
}
)

expect(container).toBeEmptyDOMElement()
})
})
})
Loading

0 comments on commit 5c9138b

Please sign in to comment.