Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ACH payment method #3616

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
fix tests
  • Loading branch information
suejung-sentry committed Jan 14, 2025
commit eaaf86d005bdb6464cd9eb7a065ee7fd0e633b1c
28 changes: 16 additions & 12 deletions src/pages/PlanPage/PlanPage.test.jsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import { MemoryRouter, Route } from 'react-router-dom'

import config from 'config'

import { ThemeContextProvider } from 'shared/ThemeContext'

import PlanPage from './PlanPage'

vi.mock('config')
@@ -44,18 +46,20 @@ const wrapper =
({ children }) => (
<QueryClientProviderV5 client={queryClientV5}>
<QueryClientProvider client={queryClient}>
<Suspense fallback={null}>
<MemoryRouter initialEntries={[initialEntries]}>
<Route path="/plan/:provider/:owner">{children}</Route>
<Route
path="*"
render={({ location }) => {
testLocation = location
return null
}}
/>
</MemoryRouter>
</Suspense>
<ThemeContextProvider>
<Suspense fallback={null}>
<MemoryRouter initialEntries={[initialEntries]}>
<Route path="/plan/:provider/:owner">{children}</Route>
<Route
path="*"
render={({ location }) => {
testLocation = location
return null
}}
/>
</MemoryRouter>
</Suspense>
</ThemeContextProvider>
</QueryClientProvider>
</QueryClientProviderV5>
)
Original file line number Diff line number Diff line change
@@ -8,11 +8,15 @@ import { Plans } from 'shared/utils/billing'

import BillingDetails from './BillingDetails'

vi.mock('./PaymentCard/PaymentCard', () => ({ default: () => 'Payment Card' }))
vi.mock('./ViewPaymentMethod/PaymentMethod/PaymentMethod', () => ({
default: () => 'Payment Method',
}))
vi.mock('./EmailAddress/EmailAddress', () => ({
default: () => 'Email Address',
}))
vi.mock('./Address/AddressCard', () => ({ default: () => 'Address Card' }))
vi.mock('./ViewPaymentMethod/Address/Address', () => ({
default: () => 'Address Card',
}))

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -84,12 +88,12 @@ describe('BillingDetails', () => {
}

describe('when there is a subscription', () => {
it('renders the payment card', async () => {
it('renders the payment method card', async () => {
setup({ hasSubscription: true })
render(<BillingDetails />, { wrapper })

const paymentCard = await screen.findByText(/Payment Card/)
expect(paymentCard).toBeInTheDocument()
const paymentCards = await screen.findAllByText(/Payment Method/)
expect(paymentCards.length).toBeGreaterThan(0)
})

it('renders the email address component', async () => {
@@ -132,7 +136,7 @@ describe('BillingDetails', () => {
it('renders the payment card', async () => {
render(<BillingDetails />, { wrapper })

const paymentCard = screen.queryByText(/Payment Card/)
const paymentCard = screen.queryByText(/Payment Method/)
expect(paymentCard).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ function BillingDetails() {
setEditMode={setEditMode}
provider={provider}
owner={owner}
existingSubscriptionDetail={subscriptionDetail}
subscriptionDetail={subscriptionDetail}
/>
) : (
<>
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { Elements } from '@stripe/react-stripe-js'
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 { vi } from 'vitest'
import { z } from 'zod'

import { SubscriptionDetailSchema } from 'services/account/useAccountDetails'

import AddressForm from './AddressForm'

const queryClient = new QueryClient()

const mockGetElement = vi.fn()
const mockGetValue = vi.fn()

vi.mock('@stripe/react-stripe-js', async () => {
const actual = await vi.importActual('@stripe/react-stripe-js')
return {
...actual,
useElements: () => ({
getElement: mockGetElement.mockReturnValue({
getValue: mockGetValue.mockResolvedValue({
complete: true,
value: {
name: 'John Doe',
address: {
line1: '123 Main St',
line2: null,
city: 'San Francisco',
state: 'CA',
postal_code: '94105',
country: 'US',
},
},
}),
}),
}),
}
})

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => (
<QueryClientProvider client={queryClient}>
<Elements stripe={null}>
<MemoryRouter initialEntries={['/plan/gh/codecov']}>
<Route path="/plan/:provider/:owner">{children}</Route>
</MemoryRouter>
</Elements>
</QueryClientProvider>
)

const mockSubscriptionDetail: z.infer<typeof SubscriptionDetailSchema> = {
defaultPaymentMethod: {
billingDetails: {
address: {
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postalCode: '94105',
country: 'US',
line2: null,
},
phone: '1234567890',
name: 'John Doe',
email: 'test@example.com',
},
card: {
brand: 'visa',
expMonth: 12,
expYear: 2025,
last4: '4242',
},
},
currentPeriodEnd: 1706851492,
cancelAtPeriodEnd: false,
customer: {
id: 'cust_123',
email: 'test@example.com',
},
latestInvoice: null,
taxIds: [],
trialEnd: null,
}

const mocks = {
useUpdateBillingAddress: vi.fn(),
}

vi.mock('services/account/useUpdateBillingAddress', () => ({
useUpdateBillingAddress: () => mocks.useUpdateBillingAddress(),
}))

afterEach(() => {
vi.clearAllMocks()
})

describe('AddressForm', () => {
const setup = () => {
return { user: userEvent.setup() }
}

it('renders the form', () => {
mocks.useUpdateBillingAddress.mockReturnValue({
mutate: vi.fn(),
isLoading: false,
})

render(
<AddressForm
address={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.address
}
name={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.name ||
undefined
}
provider="gh"
owner="codecov"
closeForm={() => {}}
/>,
{ wrapper }
)

expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
})

describe('when submitting', () => {
it('calls the service to update the address', async () => {
const user = userEvent.setup()
const updateAddress = vi.fn()
mocks.useUpdateBillingAddress.mockReturnValue({
mutate: updateAddress,
isLoading: false,
})

render(
<AddressForm
address={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.address
}
name={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.name ||
undefined
}
provider="gh"
owner="codecov"
closeForm={() => {}}
/>,
{ wrapper }
)

await user.click(screen.getByTestId('submit-address-update'))
expect(updateAddress).toHaveBeenCalledWith(
{
name: 'John Doe',
address: {
line1: '123 Main St',
line2: null,
city: 'San Francisco',
state: 'CA',
postal_code: '94105',
country: 'US',
},
},
expect.any(Object)
)
})
})

describe('when the user clicks on cancel', () => {
it('calls the closeForm prop', async () => {
const { user } = setup()
const closeForm = vi.fn()
mocks.useUpdateBillingAddress.mockReturnValue({
mutate: vi.fn(),
isLoading: false,
})

render(
<AddressForm
address={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.address
}
name={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.name ||
undefined
}
provider="gh"
owner="codecov"
closeForm={closeForm}
/>,
{ wrapper }
)

await user.click(screen.getByRole('button', { name: /cancel/i }))

expect(closeForm).toHaveBeenCalled()
})
})

describe('when the form is loading', () => {
it('has the save and cancel buttons disabled', () => {
mocks.useUpdateBillingAddress.mockReturnValue({
mutate: vi.fn(),
isLoading: true,
error: null,
reset: vi.fn(),
})

render(
<AddressForm
address={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.address
}
name={
mockSubscriptionDetail.defaultPaymentMethod?.billingDetails?.name ||
undefined
}
provider="gh"
owner="codecov"
closeForm={() => {}}
/>,
{ wrapper }
)

expect(screen.getByRole('button', { name: /save/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled()
})
})
})
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import Button from 'ui/Button'

interface AddressFormProps {
address?: z.infer<typeof AddressSchema>
name?: string | null | undefined
name?: string
closeForm: () => void
provider: string
owner: string
@@ -23,12 +23,7 @@ function AddressForm({
}: AddressFormProps) {
const elements = useElements()

const {
mutate: updateAddress,
isLoading,
error,
reset,
} = useUpdateBillingAddress({
const { mutate: updateAddress, isLoading } = useUpdateBillingAddress({
provider,
owner,
})
@@ -47,8 +42,6 @@ function AddressForm({
}
}

const showError = error && !reset

return (
<form onSubmit={submit} aria-label="form">
<div className={cs('flex flex-col gap-3')}>
@@ -72,15 +65,13 @@ function AddressForm({
},
}}
/>
<p className="mt-1 text-ds-primary-red">{showError && error}</p>
</div>
<div className="flex gap-1">
<Button
hook="submit-address-update"
type="submit"
variant="primary"
disabled={isLoading}
to={undefined}
>
Save
</Button>
@@ -90,7 +81,6 @@ function AddressForm({
variant="plain"
disabled={isLoading}
onClick={closeForm}
to={undefined}
>
Cancel
</Button>
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.