Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
feat(plg): Add seats (#63227)
Browse files Browse the repository at this point in the history
  • Loading branch information
vdavid authored Jun 19, 2024
1 parent d378d73 commit 611dcfa
Show file tree
Hide file tree
Showing 13 changed files with 624 additions and 259 deletions.
3 changes: 2 additions & 1 deletion client/web/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,14 @@ ts_project(
"src/cody/management/api/teamMembers.ts",
"src/cody/management/api/teamSubscriptions.ts",
"src/cody/management/api/types.ts",
"src/cody/management/subscription/BillingAddressPreview.tsx",
"src/cody/management/subscription/PaymentMethodPreview.tsx",
"src/cody/management/subscription/StripeAddressElement.tsx",
"src/cody/management/subscription/StripeCardDetails.tsx",
"src/cody/management/subscription/manage/BillingAddress.tsx",
"src/cody/management/subscription/manage/CodySubscriptionManagePage.tsx",
"src/cody/management/subscription/manage/InvoiceHistory.tsx",
"src/cody/management/subscription/manage/LoadingIconButton.tsx",
"src/cody/management/subscription/manage/NonEditableBillingAddress.tsx",
"src/cody/management/subscription/manage/PaymentDetails.tsx",
"src/cody/management/subscription/manage/SubscriptionDetails.tsx",
"src/cody/management/subscription/manage/utils.ts",
Expand Down
21 changes: 17 additions & 4 deletions client/web/src/cody/management/api/react-query/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {

import { Client } from '../client'
import type {
UpdateSubscriptionRequest,
Subscription,
SubscriptionSummary,
CreateTeamRequest,
PreviewResult,
PreviewCreateTeamRequest,
PreviewResult,
PreviewUpdateSubscriptionRequest,
Subscription,
SubscriptionSummary,
UpdateSubscriptionRequest,
GetSubscriptionInvoicesResponse,
} from '../teamSubscriptions'

Expand Down Expand Up @@ -85,6 +86,18 @@ export const usePreviewCreateTeam = (): UseMutationResult<PreviewResult | undefi
useMutation({
mutationFn: async requestBody => {
const response = await callCodyProApi(Client.previewCreateTeam(requestBody))
return response.json()
},
})

export const usePreviewUpdateCurrentSubscription = (): UseMutationResult<
PreviewResult | undefined,
Error,
PreviewUpdateSubscriptionRequest
> =>
useMutation({
mutationFn: async requestBody => {
const response = await callCodyProApi(Client.previewUpdateCurrentSubscription(requestBody))
return (await response.json()) as PreviewResult
},
})
4 changes: 2 additions & 2 deletions client/web/src/cody/management/api/teamSubscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ export interface PaymentMethod {
export interface PreviewResult {
dueNow: UsdCents
newPrice: UsdCents
dueDate: Date
dueDate: string
}

export interface DiscountInfo {
description: string
expiresAt?: Date
expiresAt?: string
}

export interface Invoice {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import React from 'react'

import { Text } from '@sourcegraph/wildcard'
import { mdiPencilOutline } from '@mdi/js'

import type { Subscription } from '../../api/teamSubscriptions'
import { Text, H3, Button, Icon } from '@sourcegraph/wildcard'

import type { Subscription } from '../api/teamSubscriptions'

import styles from './manage/PaymentDetails.module.scss'

export const BillingAddressPreview: React.FC<{
subscription: Subscription
isEditable: boolean
onButtonClick?: () => void
className?: string
}> = ({ subscription: { name, address }, isEditable, onButtonClick = () => undefined, className }) => (
<div className={className}>
<div className="d-flex align-items-center justify-content-between">
<H3>Billing address</H3>
{isEditable && (
<Button variant="link" className={styles.titleButton} onClick={onButtonClick}>
<Icon aria-hidden={true} svgPath={mdiPencilOutline} className="mr-1" /> Edit
</Button>
)}
</div>

export const NonEditableBillingAddress: React.FC<{ subscription: Subscription }> = ({
subscription: { name, address },
}) => (
<div>
<div className="mt-3">
<Text size="small" className="mb-1 text-muted font-weight-medium">
Full name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react'

import { mdiPencilOutline, mdiCreditCardOutline, mdiPlus } from '@mdi/js'
import classNames from 'classnames'

import { H3, Button, Icon, Text } from '@sourcegraph/wildcard'

import type { Subscription } from '../api/teamSubscriptions'

import styles from './manage/PaymentDetails.module.scss'

export const PaymentMethodPreview: React.FC<
Pick<Subscription, 'paymentMethod'> & { isEditable: boolean; onButtonClick?: () => void; className?: string }
> = ({ paymentMethod, isEditable, onButtonClick = () => undefined, className }) =>
paymentMethod ? (
<div className={className}>
<div className="d-flex align-items-center justify-content-between">
<H3>Active credit card</H3>
{isEditable && (
<Button variant="link" className={styles.titleButton} onClick={onButtonClick}>
<Icon aria-hidden={true} svgPath={mdiPencilOutline} className="mr-1" /> Edit
</Button>
)}
</div>
<div className="mt-3 d-flex justify-content-between">
<Text as="span" className={classNames('text-muted', styles.paymentMethodNumber)}>
<Icon aria-hidden={true} svgPath={mdiCreditCardOutline} /> ···· ···· ···· {paymentMethod.last4}
</Text>
<Text as="span" className="text-muted">
Expires {paymentMethod.expMonth}/{paymentMethod.expYear}
</Text>
</div>
</div>
) : (
<div className={classNames('d-flex align-items-center justify-content-between', className)}>
<H3>No payment method is available</H3>
{isEditable && (
<Button variant="link" className={styles.titleButton} onClick={onButtonClick}>
<Icon aria-hidden={true} svgPath={mdiPlus} className="mr-1" /> Add
</Button>
)}
</div>
)
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React, { useMemo, useState, useEffect } from 'react'

import { mdiPencilOutline, mdiCheck } from '@mdi/js'
import { mdiCheck } from '@mdi/js'
import { useStripe, useElements, AddressElement, Elements } from '@stripe/react-stripe-js'
import type { Stripe, StripeElementsOptions } from '@stripe/stripe-js'
import classNames from 'classnames'

import { useTheme, Theme } from '@sourcegraph/shared/src/theme'
import { H3, Button, Icon, Text, Form } from '@sourcegraph/wildcard'
import { H3, Button, Text, Form } from '@sourcegraph/wildcard'

import { useUpdateCurrentSubscription } from '../../api/react-query/subscriptions'
import type { Subscription } from '../../api/teamSubscriptions'
import { BillingAddressPreview } from '../BillingAddressPreview'
import { StripeAddressElement } from '../StripeAddressElement'

import { LoadingIconButton } from './LoadingIconButton'
import { NonEditableBillingAddress } from './NonEditableBillingAddress'

import styles from './PaymentDetails.module.scss'

Expand Down Expand Up @@ -63,26 +63,15 @@ export const useBillingAddressStripeElementsOptions = (): StripeElementsOptions
interface BillingAddressProps {
stripe: Stripe | null
subscription: Subscription
title?: string
editable: boolean
}

export const BillingAddress: React.FC<BillingAddressProps> = ({ stripe, subscription, title, editable }) => {
export const BillingAddress: React.FC<BillingAddressProps> = ({ stripe, subscription }) => {
const [isEditMode, setIsEditMode] = useState(false)

const options = useBillingAddressStripeElementsOptions()

return (
<div>
<div className="d-flex align-items-center justify-content-between">
{title ?? <H3>{title}</H3>}
{editable && (
<Button variant="link" className={styles.titleButton} onClick={() => setIsEditMode(true)}>
<Icon aria-hidden={true} svgPath={mdiPencilOutline} className="mr-1" /> Edit
</Button>
)}
</div>

{isEditMode ? (
<Elements stripe={stripe} options={options}>
<BillingAddressForm
Expand All @@ -92,7 +81,13 @@ export const BillingAddress: React.FC<BillingAddressProps> = ({ stripe, subscrip
/>
</Elements>
) : (
<NonEditableBillingAddress subscription={subscription} />
<>
<BillingAddressPreview
subscription={subscription}
isEditable={true}
onButtonClick={() => setIsEditMode(true)}
/>
</>
)}
</div>
)
Expand Down Expand Up @@ -169,26 +164,29 @@ const BillingAddressForm: React.FC<BillingAddressFormProps> = ({ subscription, o
}

return (
<Form onSubmit={handleSubmit} onReset={onReset} className={styles.billingAddressForm}>
<StripeAddressElement subscription={subscription} onFocus={() => setIsErrorVisible(false)} />

{isErrorVisible && errorMessage ? <Text className="mt-3 text-danger">{errorMessage}</Text> : null}

<div className={classNames('d-flex justify-content-end', styles.billingAddressFormButtonContainer)}>
<Button type="reset" variant="secondary" outline={true}>
Cancel
</Button>
<LoadingIconButton
type="submit"
variant="primary"
className="ml-2"
disabled={isLoading}
isLoading={isLoading}
iconSvgPath={mdiCheck}
>
Save
</LoadingIconButton>
</div>
</Form>
<>
<H3>Billing address</H3>
<Form onSubmit={handleSubmit} onReset={onReset} className={styles.billingAddressForm}>
<StripeAddressElement subscription={subscription} onFocus={() => setIsErrorVisible(false)} />

{isErrorVisible && errorMessage ? <Text className="mt-3 text-danger">{errorMessage}</Text> : null}

<div className={classNames('d-flex justify-content-end', styles.billingAddressFormButtonContainer)}>
<Button type="reset" variant="secondary" outline={true}>
Cancel
</Button>
<LoadingIconButton
type="submit"
variant="primary"
className="ml-2"
disabled={isLoading}
isLoading={isLoading}
iconSvgPath={mdiCheck}
>
Save
</LoadingIconButton>
</div>
</Form>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const PageContent: React.FC = () => {
<PageHeader className="mt-4">
<PageHeader.Heading as="h2" styleAs="h1" className="mb-4 d-flex align-items-center">
<PageHeaderIcon name="cody-logo" className="mr-2" />
<Text as="span">Manage Subscription</Text>
<Text as="span">Manage subscription</Text>
</PageHeader.Heading>
</PageHeader>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React, { useEffect, useState } from 'react'

import { mdiPencilOutline, mdiCreditCardOutline, mdiPlus, mdiCheck } from '@mdi/js'
import { mdiCheck } from '@mdi/js'
import { CardNumberElement, Elements, useElements, useStripe } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import classNames from 'classnames'

import { logger } from '@sourcegraph/common'
import { Button, Form, Grid, H3, Icon, Text } from '@sourcegraph/wildcard'
import { Button, Form, Grid, H3, Text } from '@sourcegraph/wildcard'

import { useUpdateCurrentSubscription } from '../../api/react-query/subscriptions'
// Suppressing false positive caused by an ESLint bug. See https://github.com/typescript-eslint/typescript-eslint/issues/4608
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { PaymentMethod, Subscription } from '../../api/types'
import { PaymentMethodPreview } from '../PaymentMethodPreview'
import { StripeCardDetails } from '../StripeCardDetails'

import { BillingAddress } from './BillingAddress'
Expand All @@ -37,59 +37,31 @@ export const PaymentDetails: React.FC<{ subscription: Subscription }> = ({ subsc
<PaymentMethod paymentMethod={subscription.paymentMethod} />
</div>
<div className={styles.gridItem}>
<BillingAddress stripe={stripe} subscription={subscription} title="Billing address" editable={true} />
<BillingAddress stripe={stripe} subscription={subscription} />
</div>
</Grid>
)

const PaymentMethod: React.FC<{ paymentMethod: PaymentMethod | undefined }> = ({ paymentMethod }) => {
const [isEditMode, setIsEditMode] = useState(false)

if (!paymentMethod) {
return <PaymentMethodMissing onAddButtonClick={() => setIsEditMode(true)} />
}

if (isEditMode) {
if (isEditMode && paymentMethod) {
return (
<Elements stripe={stripe}>
<PaymentMethodForm onReset={() => setIsEditMode(false)} onSubmit={() => setIsEditMode(false)} />
</Elements>
)
}

return <ActivePaymentMethod paymentMethod={paymentMethod} onEditButtonClick={() => setIsEditMode(true)} />
return (
<PaymentMethodPreview
paymentMethod={paymentMethod}
isEditable={true}
onButtonClick={() => setIsEditMode(true)}
/>
)
}

const PaymentMethodMissing: React.FC<{ onAddButtonClick: () => void }> = ({ onAddButtonClick }) => (
<div className="d-flex align-items-center justify-content-between">
<H3>No payment method is available</H3>
<Button variant="link" className={styles.titleButton} onClick={onAddButtonClick}>
<Icon aria-hidden={true} svgPath={mdiPlus} className="mr-1" /> Add
</Button>
</div>
)

const ActivePaymentMethod: React.FC<
Required<Pick<Subscription, 'paymentMethod'>> & { onEditButtonClick: () => void }
> = props => (
<>
<div className="d-flex align-items-center justify-content-between">
<H3>Active credit card</H3>
<Button variant="link" className={styles.titleButton} onClick={props.onEditButtonClick}>
<Icon aria-hidden={true} svgPath={mdiPencilOutline} className="mr-1" /> Edit
</Button>
</div>
<div className="mt-3 d-flex justify-content-between">
<Text as="span" className={classNames('text-muted', styles.paymentMethodNumber)}>
<Icon aria-hidden={true} svgPath={mdiCreditCardOutline} /> ···· ···· ···· {props.paymentMethod.last4}
</Text>
<Text as="span" className="text-muted">
Expires {props.paymentMethod.expMonth}/{props.paymentMethod.expYear}
</Text>
</div>
</>
)

interface PaymentMethodFormProps {
onReset: () => void
onSubmit: () => void
Expand Down
Loading

0 comments on commit 611dcfa

Please sign in to comment.