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 flows #3673

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
Prev Previous commit
Next Next commit
wip
  • Loading branch information
suejung-sentry committed Jan 23, 2025
commit 9c51adbf9d3c02dd1850d13ae3689e4a8bcf2e5a
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ function PaymentCard({ subscriptionDetail, provider, owner }) {
const [isFormOpen, setIsFormOpen] = useState(false)
const card = subscriptionDetail?.defaultPaymentMethod?.card
const usBankAccount = subscriptionDetail?.defaultPaymentMethod?.usBankAccount
// const isAwaitingDelayedPaymentMethodVerification = true

let nextBillingDisplayDate = null
if (!subscriptionDetail?.cancelAtPeriodEnd) {
Original file line number Diff line number Diff line change
@@ -7,8 +7,14 @@ import {
BillingDetailsSchema,
SubscriptionDetailSchema,
} from 'services/account'
import { useUpdatePaymentMethod } from 'services/account/useUpdatePaymentMethod'
import {
MissingAddressError,
MissingEmailError,
MissingNameError,
useUpdatePaymentMethod,
} from 'services/account/useUpdatePaymentMethod'
import { Provider } from 'shared/api/helpers'
import A from 'ui/A'
import Button from 'ui/Button'

interface PaymentMethodFormProps {
@@ -33,15 +39,13 @@ const PaymentMethodForm = ({
mutate: updatePaymentMethod,
isLoading,
error,
reset,
} = useUpdatePaymentMethod({
provider,
owner,
name: billingDetails?.name || undefined,
email: billingDetails?.email || undefined,
address: stripeAddress(billingDetails) || undefined,
})

async function submit(e: React.FormEvent) {
e.preventDefault()

@@ -62,8 +66,6 @@ const PaymentMethodForm = ({
})
}

const showError = error && !reset

return (
<form onSubmit={submit} aria-label="form">
<div className={cs('flex flex-col gap-3')}>
@@ -80,7 +82,7 @@ const PaymentMethodForm = ({
}}
/>
<p className="mt-1 text-ds-primary-red">
{showError && error?.message}
{error ? getErrorMessage(error) : null}
</p>
<div className="mb-8 mt-4 flex gap-1">
<Button
@@ -124,4 +126,27 @@ export const stripeAddress = (
}
}

export const getErrorMessage = (error: Error): JSX.Element => {
switch (error.message) {
case MissingNameError:
return <span>Missing name, please edit Full Name</span>
case MissingEmailError:
return <span>Missing email, please edit Email</span>
case MissingAddressError:
return <span>Missing address, please edit Address</span>
default:
return (
<span>
There&apos;s been an error. Please try refreshing your browser, if
this error persists please{' '}
{/* @ts-expect-error ignore until we can convert A component to ts */}
<A to={{ pageName: 'support' }} variant="link">
contact support
</A>
.
</span>
)
}
}

export default PaymentMethodForm
Original file line number Diff line number Diff line change
@@ -373,7 +373,7 @@ describe('CurrentOrgPlan', () => {
)
expect(paymentFailed).toBeInTheDocument()
const contactSupport = await screen.findByText(
'Please try a different card or contact support at support@codecov.io.'
'Please try a different payment method or contact support at support@codecov.io.'
)
expect(contactSupport).toBeInTheDocument()
})
56 changes: 51 additions & 5 deletions src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx
Original file line number Diff line number Diff line change
@@ -40,29 +40,51 @@ function CurrentOrgPlan() {
})
)

const isAwaitingFirstPaymentMethodVerification = false
const isAwaitingVerification =
accountDetails?.unverifiedPaymentMethods?.length

const scheduledPhase = accountDetails?.scheduleDetail?.scheduledPhase
const isDelinquent = accountDetails?.delinquent
// customer is delinquent until their first payment method is verified
const isDelinquent =
accountDetails?.delinquent && !isAwaitingFirstPaymentMethodVerification
const scheduleStart = scheduledPhase
? getScheduleStart(scheduledPhase)
: undefined

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

const planUpdatedNotification = usePlanUpdatedNotification()

const account = enterpriseDetails?.owner?.account

const hasSuccessfulDefaultPaymentMethod =
accountDetails?.subscriptionDetail?.defaultPaymentMethod

const hideSuccessBanner = isAwaitingVerification
? hasSuccessfulDefaultPaymentMethod
: true

return (
<div className="w-full lg:w-4/5">
{isAwaitingVerification ? (
<UnverifiedPaymentMethodAlert
url={
accountDetails?.unverifiedPaymentMethods?.[0]
?.hostedVerificationLink
}
/>
) : null}
{planUpdatedNotification.isCancellation ? (
<InfoAlertCancellation
subscriptionDetail={accountDetails?.subscriptionDetail}
/>
) : null}
<InfoMessageStripeCallback />
{hideSuccessBanner ? <InfoMessageStripeCallback /> : null}
{isDelinquent ? <DelinquentAlert /> : null}
{planData?.plan ? (
<div className="flex flex-col gap-4 sm:mr-4 sm:flex-initial md:w-2/3 lg:w-3/4">
@@ -147,7 +169,31 @@ const DelinquentAlert = () => {
<Alert variant={'error'}>
<Alert.Title>Your most recent payment failed</Alert.Title>
<Alert.Description>
Please try a different card or contact support at support@codecov.io.
Please try a different payment method or contact support at
support@codecov.io.
</Alert.Description>
</Alert>
<br />
</>
)
}

const UnverifiedPaymentMethodAlert = ({ url }: { url: string | undefined }) => {
return (
<>
<Alert variant={'warning'}>
<Alert.Title>New Payment Method Awaiting Verification</Alert.Title>
<Alert.Description>
Your new payment method requires verification. Click{' '}
<A
href={url}
isExternal={true}
hook="stripe-payment-method-verification"
to={undefined}
>
here
</A>{' '}
to complete the verification process.
</Alert.Description>
</Alert>
<br />
8 changes: 8 additions & 0 deletions src/services/account/useAccountDetails.ts
Original file line number Diff line number Diff line change
@@ -150,6 +150,14 @@ export const AccountDetailsSchema = z.object({
studentCount: z.number(),
subscriptionDetail: SubscriptionDetailSchema,
usesInvoice: z.boolean(),
unverifiedPaymentMethods: z
.array(
z.object({
paymentMethodId: z.string(),
hostedVerificationLink: z.string(),
})
)
.nullable(),
})

export interface UseAccountDetailsArgs {
5 changes: 5 additions & 0 deletions src/services/account/useUpdatePaymentMethod.ts
Original file line number Diff line number Diff line change
@@ -94,3 +94,8 @@ export function useUpdatePaymentMethod({
},
})
}

// Errors from Stripe api confirmSetup() - unfortunately seems to just be by text, no error codes
export const MissingNameError = `You specified "never" for fields.billing_details.name when creating the payment Element, but did not pass confirmParams.payment_method_data.billing_details.name when calling stripe.confirmSetup(). If you opt out of collecting data via the payment Element using the fields option, the data must be passed in when calling stripe.confirmSetup().`
export const MissingEmailError = `You specified "never" for fields.billing_details.email when creating the payment Element, but did not pass confirmParams.payment_method_data.billing_details.email when calling stripe.confirmSetup(). If you opt out of collecting data via the payment Element using the fields option, the data must be passed in when calling stripe.confirmSetup().`
export const MissingAddressError = `You specified "never" for fields.billing_details.address when creating the payment Element, but did not pass confirmParams.payment_method_data.billing_details.address when calling stripe.confirmSetup(). If you opt out of collecting data via the payment Element using the fields option, the data must be passed in when calling stripe.confirmSetup().`
Loading
Oops, something went wrong.