Skip to content

Commit

Permalink
fix: Move stripe idempotency key in serverside
Browse files Browse the repository at this point in the history
  • Loading branch information
sashko9807 committed Dec 9, 2024
1 parent 813875c commit cfec147
Show file tree
Hide file tree
Showing 8 changed files with 14 additions and 41 deletions.
6 changes: 1 addition & 5 deletions src/components/client/donation-flow/DonationFlowForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,14 @@ export const validationSchema: yup.SchemaOf<DonationFormData> = yup
export function DonationFlowForm() {
const formikRef = useRef<FormikProps<DonationFormData> | null>(null)
const { t } = useTranslation('donation-flow')
const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow()
const { campaign, setupIntent, paymentError, setPaymentError } = useDonationFlow()
const stripe = useStripe()
const elements = useElements()
const router = useRouter()
const updateSetupIntentMutation = useUpdateSetupIntent()
const cancelSetupIntentMutation = useCancelSetupIntent()
const paymentMethodSectionRef = React.useRef<HTMLDivElement>(null)
const authenticationSectionRef = React.useRef<HTMLDivElement>(null)
const stripeChargeRef = React.useRef<string>(idempotencyKey)
const [showCancelDialog, setShowCancelDialog] = React.useState(false)
const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false)
const { data: { user: person } = { user: null } } = useCurrentPerson()
Expand Down Expand Up @@ -176,7 +175,6 @@ export function DonationFlowForm() {
try {
const updatedIntent = await updateSetupIntentMutation.mutateAsync({
id: setupIntent.id,
idempotencyKey,
payload: {
metadata: {
type: person?.company ? DonationType.corporate : DonationType.donation,
Expand All @@ -199,7 +197,6 @@ export function DonationFlowForm() {
campaign,
values,
session,
stripeChargeRef.current,
)
router.push(
`${window.location.origin}${routes.campaigns.donationStatus(campaign.slug)}?p_status=${
Expand All @@ -212,7 +209,6 @@ export function DonationFlowForm() {
type: 'invalid_request_error',
message: (error as StripeError).message ?? t('step.summary.alerts.error'),
})
stripeChargeRef.current = crypto.randomUUID()
return
}

Expand Down
7 changes: 1 addition & 6 deletions src/components/client/donation-flow/DonationFlowPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,16 @@ import { CampaignResponse } from 'gql/campaigns'
export default function DonationFlowPage({
slug,
setupIntent,
idempotencyKey,
}: {
slug: string
setupIntent: Stripe.SetupIntent
idempotencyKey: string
}) {
const { data } = useViewCampaign(slug)
//This query needs to be prefetched in the pages folder
//otherwise on the first render the data will be undefined
const campaign = data?.campaign as CampaignResponse
return (
<DonationFlowProvider
campaign={campaign}
setupIntent={setupIntent}
idempotencyKey={idempotencyKey}>
<DonationFlowProvider campaign={campaign} setupIntent={setupIntent}>
<StripeElementsProvider>
<DonationFlowLayout campaign={campaign}>
<DonationFlowForm />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@ type DonationContext = {
setPaymentError: React.Dispatch<React.SetStateAction<StripeError | null>>
campaign: CampaignResponse
stripe: Promise<StripeType | null>
idempotencyKey: string
}

const DonationFlowContext = React.createContext({} as DonationContext)

export const DonationFlowProvider = ({
campaign,
setupIntent,
idempotencyKey,
children,
}: PropsWithChildren<{
campaign: CampaignResponse
setupIntent: Stripe.SetupIntent
idempotencyKey: string
}>) => {
const [paymentError, setPaymentError] = React.useState<StripeError | null>(null)

const value = {
idempotencyKey,
setupIntent,
paymentError,
setPaymentError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export async function confirmStripePayment(
campaign: CampaignResponse,
values: DonationFormData,
session: Session | null,
idempotencyKey: string,
): Promise<StripeJS.PaymentIntent> {
if (setupIntent.status !== DonationFormPaymentStatus.SUCCEEDED) {
const { error: intentError } = await stripe.confirmSetup({
Expand All @@ -30,12 +29,7 @@ export async function confirmStripePayment(
throw intentError
}
}
const payment = await createIntentFromSetup(
setupIntent.id,
idempotencyKey,
values.mode as PaymentMode,
session,
)
const payment = await createIntentFromSetup(setupIntent.id, values.mode as PaymentMode, session)

if (payment.data.status === DonationFormPaymentStatus.REQUIRES_ACTION) {
const { error: confirmPaymentError } = await stripe.confirmCardPayment(
Expand Down
1 change: 0 additions & 1 deletion src/gql/donations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export type CancelSetupIntentInput = {

export type UpdateSetupIntentInput = {
id: string
idempotencyKey: string
payload: Stripe.SetupIntentUpdateParams
}

Expand Down
4 changes: 1 addition & 3 deletions src/pages/campaigns/donation/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,17 @@ export const getServerSideProps: GetServerSideProps = async (ctx: GetServerSideP
)

//Generate idempotencyKey to prevent duplicate creation of resources in stripe
const idempotencyKey = crypto.randomUUID()

//create and prefetch the payment intent
const { data: setupIntent } = await apiClient.post<
Stripe.SetupIntentCreateParams,
AxiosResponse<Stripe.SetupIntentCreateParams>
>(endpoints.donation.createSetupIntent.url, idempotencyKey)
>(endpoints.donation.createSetupIntent.url)

return {
props: {
slug,
setupIntent,
idempotencyKey,
dehydratedState: dehydrate(client),
...(await serverSideTranslations(ctx.locale ?? 'bg', [
'common',
Expand Down
12 changes: 6 additions & 6 deletions src/service/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,19 @@ export const endpoints = {
createSubscriptionPayment: <Endpoint>{ url: '/stripe/create-subscription', method: 'POST' },
createPaymentIntent: <Endpoint>{ url: '/stripe/payment-intent', method: 'POST' },
createSetupIntent: <Endpoint>{ url: '/stripe/setup-intent', method: 'POST' },
createPaymentIntentFromSetup: (id: string, idempotencyKey: string) =>
createPaymentIntentFromSetup: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}/payment-intent?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}/payment-intent`,
method: 'POST',
},
createSubscriptionFromSetup: (id: string, idempotencyKey: string) =>
createSubscriptionFromSetup: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}/subscription?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}/subscription`,
method: 'POST',
},
updateSetupIntent: (id: string, idempotencyKey: string) =>
updateSetupIntent: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}`,
method: 'POST',
},
cancelSetupIntent: (id: string) =>
Expand Down
13 changes: 4 additions & 9 deletions src/service/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,11 @@ export function useUpdateSetupIntent() {
//Create payment intent useing the react-query mutation
const { data: session } = useSession()
return useMutation({
mutationFn: async ({ id, idempotencyKey, payload }: UpdateSetupIntentInput) => {
mutationFn: async ({ id, payload }: UpdateSetupIntentInput) => {
return await apiClient.post<
Stripe.SetupIntentUpdateParams,
AxiosResponse<Stripe.SetupIntent>
>(
endpoints.donation.updateSetupIntent(id, idempotencyKey).url,
payload,
authConfig(session?.accessToken),
)
>(endpoints.donation.updateSetupIntent(id).url, payload, authConfig(session?.accessToken))
},
})
}
Expand Down Expand Up @@ -84,14 +80,13 @@ export function useCreateSubscriptionPayment() {

export async function createIntentFromSetup(
setupIntentId: string,
idempotencyKey: string,
mode: PaymentMode,
session: Session | null,
): Promise<AxiosResponse<Stripe.PaymentIntent>> {
return await apiClient.post<PaymentMode, AxiosResponse<Stripe.PaymentIntent>, AxiosError<Error>>(
mode === 'one-time'
? endpoints.donation.createPaymentIntentFromSetup(setupIntentId, idempotencyKey).url
: endpoints.donation.createSubscriptionFromSetup(setupIntentId, idempotencyKey).url,
? endpoints.donation.createPaymentIntentFromSetup(setupIntentId).url
: endpoints.donation.createSubscriptionFromSetup(setupIntentId).url,
undefined,
authConfig(session?.accessToken),
)
Expand Down

0 comments on commit cfec147

Please sign in to comment.