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: Cut over to Stripe PaymentElement #3665

Merged
merged 5 commits into from
Jan 20, 2025
Merged
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
10 changes: 10 additions & 0 deletions src/assets/billing/bank.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion src/pages/PlanPage/PlanPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import config from 'config'

import { SentryRoute } from 'sentry'

import { Theme, useThemeContext } from 'shared/ThemeContext'
import LoadingLogo from 'ui/LoadingLogo'

import { PlanProvider } from './context'
import PlanBreadcrumb from './PlanBreadcrumb'
import { PlanPageDataQueryOpts } from './queries/PlanPageDataQueryOpts'
import Tabs from './Tabs'

import { StripeAppearance } from '../../stripe'

const CancelPlanPage = lazy(() => import('./subRoutes/CancelPlanPage'))
const CurrentOrgPlan = lazy(() => import('./subRoutes/CurrentOrgPlan'))
const InvoicesPage = lazy(() => import('./subRoutes/InvoicesPage'))
Expand All @@ -37,6 +40,8 @@ function PlanPage() {
const { data: ownerData } = useSuspenseQueryV5(
PlanPageDataQueryOpts({ owner, provider })
)
const { theme } = useThemeContext()
const isDarkMode = theme !== Theme.LIGHT

if (config.IS_SELF_HOSTED || !ownerData?.isCurrentUserPartOfOrg) {
return <Redirect to={`/${provider}/${owner}`} />
Expand All @@ -45,7 +50,15 @@ function PlanPage() {
return (
<div className="flex flex-col gap-4">
<Tabs />
<Elements stripe={stripePromise}>
<Elements
stripe={stripePromise}
options={{
...StripeAppearance(isDarkMode),
// mode and currency are required for the PaymentElement
mode: 'setup',
currency: 'usd',
}}
>
<PlanProvider>
<PlanBreadcrumb />
<Suspense fallback={<Loader />}>
Expand Down
28 changes: 16 additions & 12 deletions src/pages/PlanPage/PlanPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ describe('AddressCard', () => {
{ wrapper }
)

expect(screen.getByText('Cardholder name')).toBeInTheDocument()
expect(screen.getByText('Full name')).toBeInTheDocument()
expect(screen.getByText('N/A')).toBeInTheDocument()
expect(screen.getByText('Billing address')).toBeInTheDocument()
expect(screen.queryByText(/null/)).not.toBeInTheDocument()
Expand All @@ -241,7 +241,7 @@ describe('AddressCard', () => {
{ wrapper }
)

expect(screen.getByText(/Cardholder name/)).toBeInTheDocument()
expect(screen.getByText(/Full name/)).toBeInTheDocument()
expect(screen.getByText(/Bob Smith/)).toBeInTheDocument()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function AddressCard({
{!isFormOpen && (
<>
<div className="flex justify-between">
<h4 className="font-semibold">Cardholder name</h4>
<h4 className="font-semibold">Full name</h4>
<A
variant="semibold"
onClick={() => setIsFormOpen(true)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { z } from 'zod'

import bankLogo from 'assets/billing/bank.svg'
import { USBankAccountSchema } from 'services/account'

interface BankInformationProps {
usBankAccount: z.infer<typeof USBankAccountSchema>
nextBillingDisplayDate: string | null
}

function BankInformation({
usBankAccount,
nextBillingDisplayDate,
}: BankInformationProps) {
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<img src={bankLogo} alt="bank logo" />
<div className="ml-1 flex flex-col self-center">
<b>
{usBankAccount?.bankName}
&nbsp;••••&nbsp;
{usBankAccount?.last4}
</b>
</div>
</div>
{nextBillingDisplayDate && (
<p className="text-sm text-ds-gray-quinary">
Your next billing date is{' '}
<span className="text-ds-gray-octonary">
{nextBillingDisplayDate}
</span>
.
</p>
)}
</div>
)
}

export default BankInformation
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ CardInformation.propTypes = {
expMonth: PropTypes.number.isRequired,
expYear: PropTypes.number.isRequired,
}).isRequired,
openForm: PropTypes.func.isRequired,
}

export default CardInformation
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,59 @@ import PropTypes from 'prop-types'
import { useState } from 'react'

import { subscriptionDetailType } from 'services/account'
import { formatTimestampToCalendarDate } from 'shared/utils/billing'
import A from 'ui/A'
import Button from 'ui/Button'
import Icon from 'ui/Icon'

import BankInformation from './BankInformation'
import CardInformation from './CardInformation'
import CreditCardForm from './CreditCardForm'
import PaymentMethodForm from './PaymentMethodForm'
function PaymentCard({ subscriptionDetail, provider, owner }) {
const [isFormOpen, setIsFormOpen] = useState(false)
const card = subscriptionDetail?.defaultPaymentMethod?.card
const usBankAccount = subscriptionDetail?.defaultPaymentMethod?.usBankAccount

let nextBillingDisplayDate = null
if (!subscriptionDetail?.cancelAtPeriodEnd) {
nextBillingDisplayDate = formatTimestampToCalendarDate(
subscriptionDetail?.currentPeriodEnd
)
}

return (
<div className="flex flex-col gap-2 border-t p-4">
<div className="flex flex-col gap-3 border-t p-4">
<div className="flex justify-between">
<h4 className="font-semibold">Payment method</h4>
{!isFormOpen && (
<A
variant="semibold"
onClick={() => setIsFormOpen(true)}
hook="edit-card"
hook="edit-payment-method"
>
Edit <Icon name="chevronRight" size="sm" variant="solid" />
</A>
)}
</div>
{isFormOpen ? (
<CreditCardForm
<PaymentMethodForm
provider={provider}
owner={owner}
closeForm={() => setIsFormOpen(false)}
subscriptionDetail={subscriptionDetail}
/>
) : card ? (
<CardInformation card={card} subscriptionDetail={subscriptionDetail} />
) : usBankAccount ? (
<BankInformation
usBankAccount={usBankAccount}
nextBillingDisplayDate={nextBillingDisplayDate}
/>
) : (
<div className="flex flex-col gap-4 text-ds-gray-quinary">
<p className="mt-4">
No credit card set. Please contact support if you think it’s an
error or set it yourself.
No payment method set. Please contact support if you think it&apos;s
an error or set it yourself.
</p>
<div className="flex self-start">
<Button
Expand All @@ -56,7 +72,7 @@ function PaymentCard({ subscriptionDetail, provider, owner }) {
}

PaymentCard.propTypes = {
subscriptionDetail: PropTypes.oneOf([subscriptionDetailType, null]),
subscriptionDetail: subscriptionDetailType,
provider: PropTypes.string.isRequired,
owner: PropTypes.string.isRequired,
}
Expand Down
Loading
Loading