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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 additions & 1 deletion src/pages/PlanPage/PlanPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { SentryRoute } from 'sentry'

import { useStripeSetupIntent } from 'services/account/useStripeSetupIntent'

Check failure on line 11 in src/pages/PlanPage/PlanPage.jsx

View workflow job for this annotation

GitHub Actions / Run Lint

'useStripeSetupIntent' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 11 in src/pages/PlanPage/PlanPage.jsx

View workflow job for this annotation

GitHub Actions / Run Lint

'useStripeSetupIntent' is defined but never used. Allowed unused vars must match /^_/u
import LoadingLogo from 'ui/LoadingLogo'

import { PlanProvider } from './context'
Expand Down Expand Up @@ -37,6 +38,12 @@
const { data: ownerData } = useSuspenseQueryV5(
PlanPageDataQueryOpts({ owner, provider })
)
// const { data: setupIntent } = useStripeSetupIntent({ owner, provider })

const setupIntent = {
clientSecret:
'seti_1QfCiSGlVGuVgOrkPhA3FjTZ_secret_RYJLn86FhD6Co4PXYqdSkDYCgMcgZN0',
}

if (config.IS_SELF_HOSTED || !ownerData?.isCurrentUserPartOfOrg) {
return <Redirect to={`/${provider}/${owner}`} />
Expand All @@ -45,7 +52,10 @@
return (
<div className="flex flex-col gap-4">
<Tabs />
<Elements stripe={stripePromise}>
<Elements
stripe={stripePromise}
options={{ clientSecret: setupIntent?.clientSecret }}
>
<PlanProvider>
<PlanBreadcrumb />
<Suspense fallback={<Loader />}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react'

Check failure on line 1 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx

View workflow job for this annotation

GitHub Actions / Run Lint

'useState' is defined but never used. Allowed unused vars must match /^_/u
import { z } from 'zod'

import {
Expand All @@ -6,84 +6,97 @@
SubscriptionDetailSchema,
} from 'services/account'
import A from 'ui/A'
import Button from 'ui/Button'

Check failure on line 9 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx

View workflow job for this annotation

GitHub Actions / Run Lint

'A' is defined but never used. Allowed unused vars must match /^_/u
import Icon from 'ui/Icon'

Check failure on line 11 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx

View workflow job for this annotation

GitHub Actions / Run Lint

'Icon' is defined but never used. Allowed unused vars must match /^_/u
import AddressForm from './AddressForm'
import { cn } from 'shared/utils/cn'

interface AddressCardProps {
isEditMode: boolean
setEditMode: (isEditMode: boolean) => void
subscriptionDetail: z.infer<typeof SubscriptionDetailSchema>
provider: string
owner: string
className?: string
}

function AddressCard({
isEditMode,
setEditMode,
subscriptionDetail,
provider,
owner,
className,
}: AddressCardProps) {
const [isFormOpen, setIsFormOpen] = useState(false)
const billingDetails =
subscriptionDetail?.defaultPaymentMethod?.billingDetails

const isAddressSameAsPrimary = false // TODO

return (
<div className="flex flex-col gap-2 border-t p-4">
{isFormOpen && (
<div className={cn('flex gap-2', className)}>
{isEditMode && (
<AddressForm
name={billingDetails?.name || ''}
address={billingDetails?.address}
provider={provider}
owner={owner}
closeForm={() => setIsFormOpen(false)}
closeForm={() => setEditMode(false)}
/>
)}
{!isFormOpen && (
<>
<div className="flex justify-between">
<h4 className="font-semibold">Cardholder name</h4>
<A
variant="semibold"
onClick={() => setIsFormOpen(true)}
hook="edit-address"
isExternal={false}
to={undefined}
>
Edit <Icon name="chevronRight" size="sm" variant="solid" />
</A>
</div>
<BillingInner
billingDetails={billingDetails}
setIsFormOpen={setIsFormOpen}
/>
</>
{!isEditMode && (
<BillingInner
billingDetails={billingDetails}
setEditMode={setEditMode}
isAddressSameAsPrimary={isAddressSameAsPrimary}
/>
)}
</div>
)
}

interface BillingInnerProps {
billingDetails?: z.infer<typeof BillingDetailsSchema>
setIsFormOpen: (val: boolean) => void
setEditMode: (val: boolean) => void
isAddressSameAsPrimary: boolean
}

function BillingInner({ billingDetails, setIsFormOpen }: BillingInnerProps) {
function BillingInner({
billingDetails,
setEditMode,
isAddressSameAsPrimary,
}: BillingInnerProps) {
const isEmptyAddress =
!billingDetails?.address?.line1 &&
!billingDetails?.address?.line2 &&
!billingDetails?.address?.city &&
!billingDetails?.address?.state &&
!billingDetails?.address?.postalCode

if (billingDetails) {
return (
<div>
<p>{`${billingDetails.name ?? 'N/A'}`}</p>
<br />
<h4 className="mb-2 font-semibold">Billing address</h4>
<p>{`${billingDetails.address?.line1 ?? ''} ${
billingDetails.address?.line2 ?? ''
}`}</p>
<p>
{billingDetails.address?.city
? `${billingDetails.address?.city}, `
: ''}
{`${billingDetails.address?.state ?? ''} ${
billingDetails.address?.postalCode ?? ''
}`}
</p>
{isAddressSameAsPrimary ? (
<p>Same as primary address</p>
) : isEmptyAddress ? (
<p>-</p>
) : (
<>
<p>{`${billingDetails.address?.line1 ?? ''} ${
billingDetails.address?.line2 ?? ''
}`}</p>
<p>
{billingDetails.address?.city
? `${billingDetails.address?.city}, `
: ''}
{`${billingDetails.address?.state ?? ''} ${
billingDetails.address?.postalCode ?? ''
}`}
</p>
</>
)}
</div>
)
}
Expand All @@ -98,8 +111,8 @@
<Button
hook="open-modal"
variant="primary"
onClick={() => setIsFormOpen(true)}
onClick={() => setEditMode(true)}
to={undefined}

Check failure on line 115 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx

View workflow job for this annotation

GitHub Actions / Test Runner #2 - Vitest

Unhandled error

TypeError: setEditMode is not a function ❯ onClick src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx:115:15 ❯ HTMLUnknownElement.callCallback node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 ❯ innerInvokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25 ❯ invokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3 ❯ HTMLUnknownElementImpl._dispatch node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9 ❯ HTMLUnknownElementImpl.dispatchEvent node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17 ❯ HTMLUnknownElement.dispatchEvent node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34 ❯ Object.invokeGuardedCallbackDev node_modules/react-dom/cjs/react-dom.development.js:4213:16 ❯ invokeGuardedCallback node_modules/react-dom/cjs/react-dom.development.js:4277:31 This error originated in "src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "doesn't render address info stuff anymore". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.

Check failure on line 115 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx

View workflow job for this annotation

GitHub Actions / Test Runner #2 - Vitest

Unhandled error

TypeError: setEditMode is not a function ❯ onClick src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.tsx:115:15 ❯ HTMLUnknownElement.callCallback node_modules/react-dom/cjs/react-dom.development.js:4164:14 ❯ HTMLUnknownElement.callTheUserObjectsOperation node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30 ❯ innerInvokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25 ❯ invokeEventListeners node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3 ❯ HTMLUnknownElementImpl._dispatch node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9 ❯ HTMLUnknownElementImpl.dispatchEvent node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17 ❯ HTMLUnknownElement.dispatchEvent node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34 ❯ Object.invokeGuardedCallbackDev node_modules/react-dom/cjs/react-dom.development.js:4213:16 ❯ invokeGuardedCallback node_modules/react-dom/cjs/react-dom.development.js:4277:31 This error originated in "src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/Address/AddressCard.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "renders the address form component". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
disabled={false}
>
Set address
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function AddressForm({
disabled={isLoading}
to={undefined}
>
Update
Save
</Button>
<Button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useParams } from 'react-router-dom'

import { useAccountDetails } from 'services/account'

import AddressCard from './Address/AddressCard'
import EmailAddress from './EmailAddress'
import PaymentCard from './PaymentCard'
import PaymentMethod from './PaymentMethod'
import Button from 'ui/Button'
import { useState } from 'react'
import A from 'ui/A'
import EditPaymentMethod from './EditPaymentMethod'

interface URLParams {
provider: string
Expand All @@ -18,34 +21,88 @@ function BillingDetails() {
owner,
})
const subscriptionDetail = accountDetails?.subscriptionDetail
const [isEditMode, setEditMode] = useState(false)

const isAdmin = true // TODO

if (!subscriptionDetail) {
return null
}

return (
<div className="flex flex-col border">
<h3 className="p-4 font-semibold">Billing details</h3>
<EmailAddress />
<PaymentCard
// @ts-expect-error - TODO fix this once we update PaymentCard to TS
subscriptionDetail={subscriptionDetail}
provider={provider}
owner={owner}
/>
<AddressCard
subscriptionDetail={subscriptionDetail}
provider={provider}
owner={owner}
/>
{subscriptionDetail.taxIds.length > 0 ? (
<div className="flex flex-col gap-2 p-4">
<h4 className="font-semibold">Tax ID</h4>
{subscriptionDetail.taxIds.map((val, index) => (
<p key={index}>{val?.value}</p>
))}
<div className="flex flex-col divide-y border">
{/* Billing Details Section */}
<div className="flex items-center justify-between gap-4 p-4">
<div>
<h3 className="font-semibold">Billing details</h3>
<p className="pt-1 text-xs text-ds-gray-octonary">
You can modify your billing details. To update your tax IDs, please{' '}
{/* @ts-expect-error ignore until we can convert A component to ts */}
<A to={{ pageName: 'support' }} variant="link">
contact support
</A>
</p>
</div>
) : null}
{!isEditMode ? (
<Button
hook="button"
onClick={() => setEditMode(true)}
variant="default"
disabled={!isAdmin}
className="flex-none"
>
Edit payment
</Button>
) : (
<Button
hook="button"
onClick={() => setEditMode(false)}
variant="default"
disabled={!isAdmin}
className="flex-none"
>
Back
</Button>
)}
</div>
{isEditMode ? (
<EditPaymentMethod
isEditMode={isEditMode}
setEditMode={setEditMode}
provider={provider}
owner={owner}
/>
) : (
<>
<EmailAddress />
<PaymentMethod
heading="Primary Payment Method"
isPrimary={true}
isEditMode={isEditMode}
setEditMode={setEditMode}
subscriptionDetail={subscriptionDetail}
provider={provider}
owner={owner}
/>
<PaymentMethod
heading="Secondary Payment Method"
isPrimary={false}
isEditMode={isEditMode}
setEditMode={setEditMode}
subscriptionDetail={subscriptionDetail}
provider={provider}
owner={owner}
/>
{subscriptionDetail?.taxIds?.length ? (
<div className="flex flex-col gap-2 p-4">
<h4 className="font-semibold">Tax ID</h4>
{subscriptionDetail?.taxIds?.map((val, index) => (
<p key={index}>{val?.value}</p>
))}
</div>
) : null}
</>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useState } from 'react'

import AddressForm from '../Address/AddressForm'

import EditPaymentMethodForm from './EditPaymentMethodForm'

interface EditPaymentMethodProps {
isEditMode: boolean
setEditMode: (isEditMode: boolean) => void
provider: string
owner: string
}

const EditPaymentMethod = ({
isEditMode,
setEditMode,
provider,
owner,
}: EditPaymentMethodProps) => {
const [activeTab, setActiveTab] = useState<'primary' | 'secondary'>('primary')

return (
<div className="flex flex-col gap-4 p-4">
<h3 className="font-semibold">Edit payment method</h3>
<div>
{/* Tabs for Primary and Secondary Payment Methods */}
<div className="ml-2 flex border-b border-ds-gray-tertiary">
{['primary', 'secondary'].map((tab) => (
<button
key={tab}
className={`py-2 ${tab === 'primary' ? 'mr-4' : ''} ${
activeTab === tab
? 'border-b-2 border-ds-gray-octonary font-semibold text-ds-gray-octonary'
: 'text-ds-gray-quinary hover:border-b-2 hover:border-ds-gray-quinary'
}`}
onClick={() => setActiveTab(tab as 'primary' | 'secondary')}
>
{tab === 'primary' ? 'Primary' : 'Secondary'} Payment Method
</button>
))}
</div>

{/* Payment Details for the selected tab */}
<div className="m-4">
{activeTab === 'primary' && (
<div>
<EditPaymentMethodForm
isEditMode={isEditMode}

Check failure on line 48 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethod/EditPaymentMethod.tsx

View workflow job for this annotation

GitHub Actions / Run Type Checker

Type '{ isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; provider: string; owner: string; }' is not assignable to type 'IntrinsicAttributes & PaymentMethodFormProps'.

Check failure on line 48 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethod/EditPaymentMethod.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Staging

Type '{ isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; provider: string; owner: string; }' is not assignable to type 'IntrinsicAttributes & PaymentMethodFormProps'.

Check failure on line 48 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethod/EditPaymentMethod.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Production

Type '{ isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; provider: string; owner: string; }' is not assignable to type 'IntrinsicAttributes & PaymentMethodFormProps'.
setEditMode={setEditMode}
provider={provider}
owner={owner}
/>
<AddressForm closeForm={() => {}} provider={''} owner={''} />
</div>
)}
{activeTab === 'secondary' && (
<div>
<EditPaymentMethodForm
isEditMode={isEditMode}

Check failure on line 59 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethod/EditPaymentMethod.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Staging

Type '{ isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; provider: string; owner: string; }' is not assignable to type 'IntrinsicAttributes & PaymentMethodFormProps'.

Check failure on line 59 in src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/EditPaymentMethod/EditPaymentMethod.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Production

Type '{ isEditMode: boolean; setEditMode: (isEditMode: boolean) => void; provider: string; owner: string; }' is not assignable to type 'IntrinsicAttributes & PaymentMethodFormProps'.
setEditMode={setEditMode}
provider={provider}
owner={owner}
/>
<AddressForm closeForm={() => {}} provider={''} owner={''} />
</div>
)}
</div>
</div>
</div>
)
}

export default EditPaymentMethod
Loading
Loading