Skip to content

Commit

Permalink
feat: Add ACH microdeposits handling
Browse files Browse the repository at this point in the history
  • Loading branch information
suejung-sentry committed Jan 30, 2025
1 parent 0647710 commit 36dbab4
Show file tree
Hide file tree
Showing 10 changed files with 581 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import config from 'config'

import { SentryRoute } from 'sentry'

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'
import PlanBreadcrumb from './PlanBreadcrumb'
import { PlanPageDataQueryOpts } from './queries/PlanPageDataQueryOpts'
import Tabs from './Tabs'

import { useUnverifiedPaymentMethods } from 'services/account/useUnverifiedPaymentMethods'
import { StripeAppearance } from '../../stripe'

const CancelPlanPage = lazy(() => import('./subRoutes/CancelPlanPage'))
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?.length ? (
<UnverifiedPaymentMethodAlert
url={unverifiedPaymentMethods?.[0]?.hostedVerificationUrl}
/>
) : null}
<Suspense fallback={<Loader />}>
<Switch>
<SentryRoute path={path} exact>
Expand Down Expand Up @@ -90,4 +109,27 @@ function PlanPage() {
)
}

const UnverifiedPaymentMethodAlert = ({ url }: { url?: string | null }) => {
return (
<>
<Alert variant="warning">
<Alert.Title>New Payment Method Awaiting Verification</Alert.Title>
<Alert.Description>
Your new payment method requires verification.{' '}
<A
href={url}
isExternal
hook="stripe-payment-method-verification"
to={undefined}
>
Click here
</A>{' '}
to complete the verification process.
</Alert.Description>
</Alert>
<br />
</>
)
}

export default PlanPage
27 changes: 20 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: pageData } = useCurrentOrgPlanPageData({
provider,
owner,
})
Expand All @@ -40,16 +40,25 @@ function CurrentOrgPlan() {
})
)

// 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 &&
pageData?.billing?.unverifiedPaymentMethods?.length

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 +71,13 @@ function CurrentOrgPlan() {
subscriptionDetail={accountDetails?.subscriptionDetail}
/>
) : null}
<InfoMessageStripeCallback />
<InfoMessageStripeCallback
hasUnverifiedPaymentMethods={
!!pageData?.billing?.unverifiedPaymentMethods?.length
}
/>
{isDelinquent ? <DelinquentAlert /> : null}
{planData?.plan ? (
{pageData?.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 @@ -5,12 +5,16 @@ import Message from 'old_ui/Message'

// Stripe redirects to this page with ?success or ?cancel in the URL
// this component takes care of rendering a message if it is successful
function InfoMessageStripeCallback() {
function InfoMessageStripeCallback({
hasUnverifiedPaymentMethods,
}: {
hasUnverifiedPaymentMethods: boolean
}) {
const urlParams = qs.parse(useLocation().search, {
ignoreQueryPrefix: true,
})

if ('success' in urlParams)
if ('success' in urlParams && !hasUnverifiedPaymentMethods)
return (
<div className="col-start-1 col-end-13 mb-4">
<Message variant="success">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { useParams } from 'react-router-dom'

Expand All @@ -23,6 +23,7 @@ import { useUpgradeControls } from './hooks'
import PlanTypeOptions from './PlanTypeOptions'
import UpdateBlurb from './UpdateBlurb/UpdateBlurb'
import UpdateButton from './UpdateButton'
import UpgradeFormModal from './UpgradeFormModal'

type URLParams = {
provider: Provider
Expand All @@ -45,6 +46,9 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) {
const { data: plans } = useAvailablePlans({ provider, owner })
const { data: planData } = usePlanData({ owner, provider })
const { upgradePlan } = useUpgradeControls()
const [showModal, setShowModal] = useState(false)
const [formData, setFormData] = useState<UpgradeFormFields>()
const [isUpgrading, setIsUpgrading] = useState(false)
const isSentryUpgrade = canApplySentryUpgrade({
isEnterprisePlan: planData?.plan?.isEnterprisePlan,
plans,
Expand Down Expand Up @@ -90,10 +94,20 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) {
trigger('seats')
}, [newPlan, trigger])

const onSubmit = handleSubmit((data) => {
if (accountDetails?.unverifiedPaymentMethods?.length) {

Check failure on line 98 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Run Type Checker

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.

Check failure on line 98 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Production

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.

Check failure on line 98 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Staging

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.
setFormData(data)
setShowModal(true)
} else {
setIsUpgrading(true)
upgradePlan(data)
}
})

return (
<form
className="flex flex-col gap-6 border p-4 text-ds-gray-default md:w-2/3"
onSubmit={handleSubmit(upgradePlan)}
onSubmit={onSubmit}
>
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Organization</h3>
Expand All @@ -119,6 +133,21 @@ function UpgradeForm({ selectedPlan, setSelectedPlan }: UpgradeFormProps) {
nextBillingDate={getNextBillingDate(accountDetails)!}
/>
<UpdateButton isValid={isValid} newPlan={newPlan} seats={seats} />
{showModal && formData && (
<UpgradeFormModal
isOpen={showModal}
onClose={() => setShowModal(false)}
onConfirm={() => {
setIsUpgrading(true)
upgradePlan(formData)
}}
url={
accountDetails?.unverifiedPaymentMethods?.[0]

Check failure on line 145 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Run Type Checker

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.

Check failure on line 145 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Production

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.

Check failure on line 145 in src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.tsx

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Staging

Property 'unverifiedPaymentMethods' does not exist on type '{ name: string | null; email: string | null; activatedUserCount: number; planAutoActivate: boolean | null; subscriptionDetail: { latestInvoice: { number: string | null; id: string | null; ... 17 more ...; customerShipping?: any; } | null; ... 6 more ...; collectionMethod?: string | ... 1 more ... | undefined; } | nu...'.
?.hostedVerificationLink || ''
}
isUpgrading={isUpgrading}
/>
)}
</form>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import A from 'ui/A'
import Button from 'ui/Button'
import Icon from 'ui/Icon'
import Modal from 'ui/Modal'

interface UpgradeModalProps {
isOpen: boolean
onClose: () => void
onConfirm: () => void
url: string
isUpgrading?: boolean
}

const UpgradeFormModal = ({
isOpen,
onClose,
onConfirm,
url,
isUpgrading = false,
}: UpgradeModalProps) => (
<Modal
isOpen={isOpen}
onClose={onClose}
title={
<p className="flex items-center gap-2 text-base">
<Icon
name="exclamationTriangle"
size="sm"
className="fill-ds-primary-yellow"
/>
Incomplete Plan Upgrade
</p>
}
body={
<div className="flex flex-col gap-4">
<div>
You have an incomplete plan upgrade that is awaiting payment
verification{' '}
<A
href={url}
isExternal
hook={'verify-payment-method'}
to={undefined}
>
here
</A>
.
</div>
<p>
Are you sure you wish to abandon the pending upgrade and start a new
one?
</p>
</div>
}
footer={
<div className="flex gap-2">
<Button hook="cancel-upgrade" onClick={onClose} disabled={isUpgrading}>
Cancel
</Button>
<Button
hook="confirm-upgrade"
variant="primary"
onClick={onConfirm}
disabled={isUpgrading}
>
{isUpgrading ? 'Processing...' : 'Yes, Start New Upgrade'}
</Button>
</div>
}
/>
)

export default UpgradeFormModal
2 changes: 2 additions & 0 deletions src/services/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export * from './useAutoActivate'
export * from './useAvailablePlans'
export * from './useCancelPlan'
export * from './useCreateStripeSetupIntent'
export * from './useCurrentOrgPlanPageData'

Check failure on line 7 in src/services/account/index.ts

View workflow job for this annotation

GitHub Actions / Run Lint

Multiple exports of name 'query'
export * from './useEraseAccount'
export * from './useInvoice'
export * from './useInvoices'
export * from './usePlanData'

Check failure on line 11 in src/services/account/index.ts

View workflow job for this annotation

GitHub Actions / Run Lint

Multiple exports of name 'query'

Check failure on line 11 in src/services/account/index.ts

View workflow job for this annotation

GitHub Actions / Run Type Checker

Module './useCurrentOrgPlanPageData' has already exported a member named 'query'. Consider explicitly re-exporting to resolve the ambiguity.

Check failure on line 11 in src/services/account/index.ts

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Production

Module './useCurrentOrgPlanPageData' has already exported a member named 'query'. Consider explicitly re-exporting to resolve the ambiguity.

Check failure on line 11 in src/services/account/index.ts

View workflow job for this annotation

GitHub Actions / Upload Bundle Stats - Staging

Module './useCurrentOrgPlanPageData' has already exported a member named 'query'. Consider explicitly re-exporting to resolve the ambiguity.
export * from './useSentryToken'
export * from './useUnverifiedPaymentMethods'
export * from './useUpdateBillingEmail'
export * from './useUpdateCard'
export * from './useUpdatePaymentMethod'
Expand Down
Loading

0 comments on commit 36dbab4

Please sign in to comment.