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 related hooks #3662

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Add ACH related hooks
  • Loading branch information
suejung-sentry committed Jan 16, 2025
commit 90214a2aecae5f3effbe66e0834ce3e6e199f678
2 changes: 1 addition & 1 deletion src/services/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export * from './useInvoice'
export * from './usePlanData'
export * from './useAvailablePlans'
export * from './useSentryToken'
export * from './useUpdateCard'
export * from './useUpdatePaymentMethod'
export * from './useUpgradePlan'
export * from './useUpdateBillingEmail'
49 changes: 44 additions & 5 deletions src/services/account/useAccountDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'
import { z } from 'zod'

import { accountDetailsObject, accountDetailsParsedObj } from './mocks'
import { useAccountDetails } from './useAccountDetails'
import { accountDetailsParsedObj } from './mocks'
import { AccountDetailsSchema, useAccountDetails } from './useAccountDetails'

vi.mock('js-cookie')

Expand Down Expand Up @@ -45,17 +46,17 @@ afterAll(() => {
})

describe('useAccountDetails', () => {
function setup() {
function setup(accountDetails: z.infer<typeof AccountDetailsSchema>) {
server.use(
http.get(`/internal/${provider}/${owner}/account-details/`, () => {
return HttpResponse.json(accountDetailsObject)
return HttpResponse.json(accountDetails)
})
)
}

describe('when called', () => {
it('returns the data', async () => {
setup()
setup(accountDetailsParsedObj)
const { result } = renderHook(
() => useAccountDetails({ provider, owner }),
{ wrapper: wrapper() }
Expand All @@ -65,5 +66,43 @@ describe('useAccountDetails', () => {
expect(result.current.data).toEqual(accountDetailsParsedObj)
)
})

it('returns data with usBankAccount when enabled', async () => {
const withUSBankAccount = {
...accountDetailsParsedObj,
subscriptionDetail: {
...accountDetailsParsedObj.subscriptionDetail,
defaultPaymentMethod: {
billingDetails: null,
usBankAccount: {
bankName: 'Bank of America',
last4: '1234',
},
},
},
}
setup(withUSBankAccount)

const { result } = renderHook(
() => useAccountDetails({ provider, owner }),
{ wrapper: wrapper() }
)

await waitFor(() =>
expect(result.current.data).toEqual({
...accountDetailsParsedObj,
subscriptionDetail: {
...accountDetailsParsedObj.subscriptionDetail,
defaultPaymentMethod: {
billingDetails: null,
usBankAccount: {
bankName: 'Bank of America',
last4: '1234',
},
},
},
})
)
})
})
})
6 changes: 6 additions & 0 deletions src/services/account/useAccountDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export const PaymentMethodSchema = z
last4: z.string(),
})
.nullish(),
usBankAccount: z
.object({
bankName: z.string(),
last4: z.string(),
})
.nullish(),
billingDetails: BillingDetailsSchema.nullable(),
})
.nullable()
Expand Down
90 changes: 90 additions & 0 deletions src/services/account/useCreateStripeSetupIntent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook, waitFor } from '@testing-library/react'
import { graphql, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import React from 'react'
import { MemoryRouter, Route } from 'react-router-dom'

import { useCreateStripeSetupIntent } from './useCreateStripeSetupIntent'

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})
const wrapper =
(initialEntries = '/gh'): React.FC<React.PropsWithChildren> =>
({ children }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialEntries]}>
<Route path="/:provider">{children}</Route>
</MemoryRouter>
</QueryClientProvider>
)

const provider = 'gh'
const owner = 'codecov'

const server = setupServer()
beforeAll(() => {
server.listen()
})

afterEach(() => {
queryClient.clear()
server.resetHandlers()
})

afterAll(() => {
server.close()
})

describe('useCreateStripeSetupIntent', () => {
function setup(hasError = false) {
server.use(
graphql.mutation('CreateStripeSetupIntent', () => {
if (hasError) {
return HttpResponse.json({ data: {} })
}

return HttpResponse.json({
data: { createStripeSetupIntent: { clientSecret: 'test_secret' } },
})
})
)
}

describe('when called', () => {
describe('on success', () => {
it('returns the data', async () => {
setup()
const { result } = renderHook(
() => useCreateStripeSetupIntent({ provider, owner }),
{ wrapper: wrapper() }
)

await waitFor(() =>
expect(result.current.data).toEqual({ clientSecret: 'test_secret' })
)
})
})

describe('on fail', () => {
beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterAll(() => {
vi.restoreAllMocks()
})

it('fails to parse if bad data', async () => {
setup(true)
const { result } = renderHook(
() => useCreateStripeSetupIntent({ provider, owner }),
{ wrapper: wrapper() }
)

await waitFor(() => expect(result.current.error).toBeTruthy())
})
})
})
})
81 changes: 81 additions & 0 deletions src/services/account/useCreateStripeSetupIntent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'

import Api from 'shared/api'
import { NetworkErrorObject } from 'shared/api/helpers'

export const CreateStripeSetupIntentSchema = z.object({
createStripeSetupIntent: z.object({
clientSecret: z.string().nullish(),
error: z
.discriminatedUnion('__typename', [
z.object({
__typename: z.literal('ValidationError'),
}),
z.object({
__typename: z.literal('UnauthenticatedError'),
}),
])
.nullish(),
}),
})

export interface UseCreateStripeSetupIntentArgs {
provider: string
owner: string
opts?: {
enabled?: boolean
}
}

function createStripeSetupIntent({
provider,
owner,
signal,
}: {
provider: string
owner: string
signal?: AbortSignal
}) {
return Api.graphql({
provider,
signal,
query: `
mutation CreateStripeSetupIntent($owner: String!) {
createStripeSetupIntent(input: { owner: $owner }) {
clientSecret
error {
__typename
}
}
}
`,
variables: {
owner,
},
})
}

export function useCreateStripeSetupIntent({
provider,
owner,
opts = {},
}: UseCreateStripeSetupIntentArgs) {
return useQuery({
queryKey: ['setupIntent', provider, owner],
queryFn: ({ signal }) =>
createStripeSetupIntent({ provider, owner, signal }).then((res) => {
const parsedRes = CreateStripeSetupIntentSchema.safeParse(res.data)
if (!parsedRes.success) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to return different stuff if one of the "error" types comes back?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm true, I'll have those errors reject as well (seems that is the pattern in other hooks too). I'll do a pass at the error pathways too to make sure these get reacted to correctly as well

return Promise.reject({
status: 404,
data: {},
dev: 'useStripeSetupIntent - 404 failed to parse',
} satisfies NetworkErrorObject)
}

return parsedRes.data.createStripeSetupIntent
}),
...opts,
})
}
1 change: 1 addition & 0 deletions src/services/account/useUpdateBillingEmail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe('useUpdateBillingEmail', () => {
await waitFor(() =>
expect(mockBody).toHaveBeenCalledWith({
new_email: '[email protected]',
should_propagate_to_payment_methods: true,
})
)
})
Expand Down
1 change: 1 addition & 0 deletions src/services/account/useUpdateBillingEmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function useUpdateBillingEmail({ provider, owner }: UsePlanDataArgs) {
const body = {
/* eslint-disable camelcase */
new_email: formData?.newEmail,
should_propagate_to_payment_methods: true,
}
return Api.patch({ path, provider, body })
},
Expand Down
Loading
Loading