-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat(plg): Add new Checkout flow that uses Stripe's createToken
API
#63213
Changes from 6 commits
897ee09
034d38a
316dccd
d4f4064
cc47ad8
734fcef
99380b3
792dcd5
3eda37a
88110af
a09ff2f
cccb751
9eb57d1
846db99
2b2d8d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,8 +47,15 @@ export const callCodyProApi = async <Data>(call: Call<Data>): Promise<Data | und | |
|
||
// Throw errors for unsuccessful HTTP calls so that `callCodyProApi` callers don't need to check whether the response is OK. | ||
// Motivation taken from here: https://tanstack.com/query/latest/docs/framework/react/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default | ||
throw new Error(`Request to Cody Pro API failed with status ${response.status}`) | ||
throw new Error(`Request to Cody Pro API failed with status ${response.status}: ${response.statusText}`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be helpful to see the status text as well? I can revert if not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I faced a similar issue on the PR I'm working on.
This way we can use error message and status separately and add more fields/methods to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in https://github.com/sourcegraph/sourcegraph/pull/63213/commits/3eda37a7448a94684dfdf223a1dffba98d8d91b2, although we don't currently process this error anywhere. |
||
} | ||
|
||
return (await response.json()) as Data | ||
const responseText = await response.text() | ||
|
||
// Handle both JSON and text responses. | ||
try { | ||
return JSON.parse(responseText) as Data | ||
} catch { | ||
return responseText as Data | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: the caller needs to know whether they want plain text or JSON. So far, I had no great idea how to make this better and frankly, we have 0 callers that use a text output, so I only needed this to avoid getting an error here upon an empty response. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an alternative, we can make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,13 @@ import { | |
} from '@tanstack/react-query' | ||
|
||
import { Client } from '../client' | ||
import type { UpdateSubscriptionRequest, Subscription } from '../teamSubscriptions' | ||
import type { | ||
UpdateSubscriptionRequest, | ||
Subscription, | ||
CreateTeamRequest, | ||
PreviewResult, | ||
PreviewCreateTeamRequest, | ||
} from '../types' | ||
|
||
import { callCodyProApi } from './callCodyProApi' | ||
|
||
|
@@ -45,3 +51,16 @@ export const useUpdateCurrentSubscription = (): UseMutationResult< | |
}, | ||
}) | ||
} | ||
|
||
export const useCreateTeam = (): UseMutationResult<string | undefined, Error, CreateTeamRequest> => { | ||
const queryClient = useQueryClient() | ||
return useMutation({ | ||
mutationFn: async requestBody => callCodyProApi(Client.createTeam(requestBody)), | ||
onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKeys.all }), | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @taras-yemets, I appreciate your 👀 here because you've spent more time with the use-query docs than I have. |
||
} | ||
|
||
export const usePreviewCreateTeam = (): UseMutationResult<PreviewResult | undefined, Error, PreviewCreateTeamRequest> => | ||
useMutation({ | ||
mutationFn: async requestBody => callCodyProApi(Client.previewCreateTeam(requestBody)), | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,7 +112,29 @@ export interface UpdateSubscriptionRequest { | |
subscriptionUpdate?: SubscriptionUpdateOptions | ||
} | ||
|
||
export interface PreviewUpdateSubscriptionRequest { | ||
newSeatCount?: number | ||
newBillingInterval?: BillingInterval | ||
newCancelAtPeriodEnd?: boolean | ||
} | ||
|
||
export interface GetSubscriptionInvoicesResponse { | ||
invoices: Invoice[] | ||
continuationToken?: string | ||
} | ||
|
||
export interface CreateTeamRequest { | ||
name: string | ||
slug: string | ||
seats: number | ||
address: Address | ||
billingInterval: BillingInterval | ||
couponCode?: string | ||
creditCardToken: string | ||
} | ||
|
||
export interface PreviewCreateTeamRequest { | ||
seats: number | ||
billingInterval: BillingInterval | ||
couponCode?: string | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be super great if these were generated, btw. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React, { useMemo } from 'react' | ||
|
||
import { AddressElement } from '@stripe/react-stripe-js' | ||
import type { StripeAddressElementOptions } from '@stripe/stripe-js' | ||
|
||
import { Subscription } from '../api/teamSubscriptions' | ||
|
||
interface StripeAddressElementProps { | ||
subscription?: Subscription | ||
onFocus?: () => void | ||
} | ||
|
||
export const StripeAddressElement: React.FC<StripeAddressElementProps> = ({ subscription, onFocus }) => { | ||
const options = useMemo( | ||
(): StripeAddressElementOptions => ({ | ||
mode: 'billing', | ||
display: { name: 'full' }, | ||
...(subscription | ||
? { | ||
defaultValues: { | ||
name: subscription.name, | ||
address: { | ||
...subscription.address, | ||
postal_code: subscription.address.postalCode, | ||
}, | ||
}, | ||
} | ||
: {}), | ||
autocomplete: { mode: 'automatic' }, | ||
}), | ||
[subscription] | ||
) | ||
|
||
return <AddressElement options={options} onFocus={onFocus} /> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, { useCallback } from 'react' | ||
|
||
import { CardNumberElement, CardExpiryElement, CardCvcElement } from '@stripe/react-stripe-js' | ||
import type { StripeCardElementOptions } from '@stripe/stripe-js' | ||
|
||
import { useTheme, Theme } from '@sourcegraph/shared/src/theme' | ||
import { Label, Text, Grid } from '@sourcegraph/wildcard' | ||
|
||
const useCardElementOptions = (): ((type: 'number' | 'expiry' | 'cvc') => StripeCardElementOptions) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @taras-yemets, I made this a function because we were getting warnings from Stripe in the JS console because we were passing |
||
const { theme } = useTheme() | ||
|
||
return useCallback( | ||
(type: 'number' | 'expiry' | 'cvc') => ({ | ||
...(type === 'number' ? { disableLink: true } : {}), | ||
|
||
classes: { | ||
base: 'form-control py-2', | ||
focus: 'focus-visible', | ||
invalid: 'is-invalid', | ||
}, | ||
|
||
style: { | ||
base: { | ||
color: theme === Theme.Light ? '#262b38' : '#dbe2f0', | ||
}, | ||
}, | ||
}), | ||
[theme] | ||
) | ||
} | ||
|
||
interface StripeCardDetailsProps { | ||
onFocus?: () => void | ||
className?: string | ||
} | ||
|
||
export const StripeCardDetails: React.FC<StripeCardDetailsProps> = ({ onFocus, className }) => { | ||
const getOptions = useCardElementOptions() | ||
|
||
return ( | ||
<div className={className}> | ||
<div> | ||
<Label className="d-block"> | ||
<Text className="mb-2">Card number</Text> | ||
<CardNumberElement options={getOptions('number')} onFocus={onFocus} /> | ||
</Label> | ||
</div> | ||
|
||
<Grid columnCount={2} className="mt-3 mb-0 pb-3"> | ||
<Label className="d-block"> | ||
<Text className="mb-2">Expiry date</Text> | ||
<CardExpiryElement options={getOptions('expiry')} onFocus={onFocus} /> | ||
</Label> | ||
|
||
<Label className="d-block"> | ||
<Text className="mb-2">CVC</Text> | ||
<CardCvcElement options={getOptions('cvc')} onFocus={onFocus} /> | ||
</Label> | ||
</Grid> | ||
</div> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't use this yet (nor the other
preview*
function above), but it felt wrong to remove them once I added them incl. the related types. 🤷