Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/Apps/Order/Components/CreditCardPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface CreditCardPickerProps {
commitMutation: CommitMutation
innerRef: React.RefObject<CreditCardPicker>
tracking: TrackingProp
onCreditCardChange?: () => void
}

interface CreditCardPickerState {
Expand Down Expand Up @@ -270,6 +271,8 @@ export class CreditCardPicker extends React.Component<
<RadioGroup
data-test="credit-cards"
onSelect={val => {
this.props.onCreditCardChange?.()

if (val === "new") {
this.setState({ creditCardSelection: { type: "new" } })
} else {
Expand Down Expand Up @@ -322,6 +325,7 @@ export class CreditCardPicker extends React.Component<
<CreditCardInput
error={stripeError?.message}
onChange={response => {
this.props.onCreditCardChange?.()
this.setState({ stripeError: response.error as StripeError })
}}
required
Expand Down
23 changes: 21 additions & 2 deletions src/Apps/Order/Components/__mocks__/CreditCardPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export const useThrownError = () => {
)
}

export const useAnotherCreditCard = (creditCardId: string) => {
CreditCardPickerMock.getCreditCardId.mockResolvedValue({
type: "success",
creditCardId,
})
}

const CreditCardPickerMock = {
getCreditCardId: jest.fn(
() => Promise.resolve(goodResult) as Promise<CreditCardIdResult>,
Expand All @@ -56,7 +63,19 @@ beforeEach(() => {
useGoodResult()
})

export const CreditCardPickerFragmentContainer = ({ innerRef }) => {
export const CreditCardPickerFragmentContainer = ({
innerRef,
onCreditCardChange,
}) => {
innerRef.current = CreditCardPickerMock
return createElement("div")

// Stands in for selecting a different card in the real picker
return createElement(
"div",
{
"data-testid": "select-another-credit-card",
onClick: onCreditCardChange,
},
null,
)
}
13 changes: 11 additions & 2 deletions src/Apps/Order/Routes/NewPayment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { NewPaymentRouteSetOrderPaymentMutation } from "__generated__/NewPa
import type { NewPayment_me$data } from "__generated__/NewPayment_me.graphql"
import type { NewPayment_order$data } from "__generated__/NewPayment_order.graphql"
import type { Router } from "found"
import { type FC, createRef, useState } from "react"
import { type FC, useRef, useState } from "react"
import { createFragmentContainer, graphql } from "react-relay"
import { useTracking } from "react-tracking"

Expand Down Expand Up @@ -77,7 +77,7 @@ export const NewPaymentRoute: FC<
stripe,
} = props
const isLoading = isCommittingMutation || isGettingCreditCardId
const CreditCardPicker = createRef<CreditCardPicker>()
const CreditCardPicker = useRef<CreditCardPicker>(null)

const trackErrorMessageEvent = (
title: string,
Expand Down Expand Up @@ -129,6 +129,14 @@ export const NewPaymentRoute: FC<
return null
}

// The result of the picker is cached so that retrying after a recoverable
// failure (a declined charge, or an SCA challenge) doesn't create a duplicate
// credit card. That cache is only valid for as long as the user sticks with
// the same payment method, so drop it as soon as they pick a different one.
const handleCreditCardChange = () => {
setCreditCardPickerResult(null)
}

const onContinue = async () => {
try {
const creditCardId =
Expand Down Expand Up @@ -326,6 +334,7 @@ export const NewPaymentRoute: FC<
commitMutation={commitMutation}
innerRef={CreditCardPicker}
tracking={tracking}
onCreditCardChange={handleCreditCardChange}
/>
<Media greaterThan="xs">
<ContinueButton onClick={onContinue} loading={isLoading} />
Expand Down
34 changes: 33 additions & 1 deletion src/Apps/Order/Routes/__tests__/NewPayment.jest.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { screen } from "@testing-library/react"
import { NewPaymentFragmentContainer } from "Apps/Order/Routes/NewPayment"
import {
fixFailedPaymentFailure,
Expand All @@ -13,7 +14,6 @@ import { MockBoot } from "DevTools/MockBoot"
import { mockLocation } from "DevTools/mockLocation"
import { mockStripe } from "DevTools/mockStripe"
import { setupTestWrapperTL } from "DevTools/setupTestWrapperTL"
import { screen } from "@testing-library/react"
import type { NewPaymentTestQuery$rawResponse } from "__generated__/NewPaymentTestQuery.graphql"
import { DateTime } from "luxon"
import { graphql } from "react-relay"
Expand Down Expand Up @@ -255,6 +255,38 @@ describe("Payment", () => {
})
})

it("uses the newly selected credit card when retrying after a failed charge", async () => {
mockCommitMutation.mockResolvedValueOnce(fixFailedPaymentFailure)
const { user } = renderWithRelay({ CommerceOrder: () => testOrder })
const page = new OrderAppTestPageRTL(screen, user)
await page.clickSubmit()

expect(mockCommitMutation).toHaveBeenLastCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
input: expect.objectContaining({ creditCardId: "credit-card-id" }),
}),
}),
)

CreditCardPickerMock.useAnotherCreditCard("another-credit-card-id")
await user.click(screen.getByTestId("select-another-credit-card"))

mockCommitMutation.mockResolvedValueOnce(fixFailedPaymentSuccess)
await page.clickSubmit()

expect(mockCommitMutation).toHaveBeenLastCalledWith(
expect.objectContaining({
variables: expect.objectContaining({
input: expect.objectContaining({
creditCardId: "another-credit-card-id",
}),
}),
}),
)
expect(pushMock).toHaveBeenCalledWith("/orders/1234/details")
})

it("shows an error modal when there is a network error", async () => {
mockCommitMutation.mockRejectedValue({})
const { user } = renderWithRelay({ CommerceOrder: () => testOrder })
Expand Down
Loading