From 576f852a2d9668374189b5e107e404218320cbf5 Mon Sep 17 00:00:00 2001 From: Sultan Date: Mon, 27 Jan 2025 18:27:06 +0100 Subject: [PATCH 01/26] fix(EMI-2224): Refactor Register to Bid flow --- src/app/Components/Bidding/BidFlow.tests.tsx | 10 +- .../Components/Bidding/Screens/BidResult.tsx | 2 +- .../Bidding/Screens/ConfirmBid/index.tsx | 2 +- .../Bidding/Screens/SelectMaxBid.tests.tsx | 10 +- .../Bidding/Screens/SelectMaxBid.tsx | 149 ++++++++++++++++-- src/app/Components/Containers/BidFlow.tsx | 18 ++- ..._legacy_do_not_use__navigator-ios-shim.tsx | 3 + 7 files changed, 156 insertions(+), 38 deletions(-) diff --git a/src/app/Components/Bidding/BidFlow.tests.tsx b/src/app/Components/Bidding/BidFlow.tests.tsx index 9d85ec55e14..a7c0a68609c 100644 --- a/src/app/Components/Bidding/BidFlow.tests.tsx +++ b/src/app/Components/Bidding/BidFlow.tests.tsx @@ -28,22 +28,17 @@ const commitMutationMock = (fn?: typeof relay.commitMutation) => const bidderPositionQueryMock = bidderPositionQuery as jest.Mock let fakeNavigator: FakeNavigator -let fakeRelay: any beforeEach(() => { fakeNavigator = new FakeNavigator() - fakeRelay = { - refetch: jest.fn(), - } }) it("allows bidders with a qualified credit card to bid", async () => { let screen = renderWithWrappersLEGACY( ) @@ -75,9 +70,8 @@ it("allows bidders without a qualified credit card to register a card and bid", let screen = renderWithWrappersLEGACY( ) diff --git a/src/app/Components/Bidding/Screens/BidResult.tsx b/src/app/Components/Bidding/Screens/BidResult.tsx index 590048f5d72..b744f46e6ed 100644 --- a/src/app/Components/Bidding/Screens/BidResult.tsx +++ b/src/app/Components/Bidding/Screens/BidResult.tsx @@ -6,8 +6,8 @@ import { Timer } from "app/Components/Bidding/Components/Timer" import { Title } from "app/Components/Bidding/Components/Title" import { Flex } from "app/Components/Bidding/Elements/Flex" import { BidderPositionResult } from "app/Components/Bidding/types" -import { NavigationHeader } from "app/Components/NavigationHeader" import { Markdown } from "app/Components/Markdown" +import { NavigationHeader } from "app/Components/NavigationHeader" import { unsafe__getEnvironment } from "app/store/GlobalStore" import { dismissModal, navigate } from "app/system/navigation/navigate" import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index f9bdf3f120f..b1b896c44e9 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -15,8 +15,8 @@ import { BidResultScreen } from "app/Components/Bidding/Screens/BidResult" import { bidderPositionQuery } from "app/Components/Bidding/Screens/ConfirmBid/BidderPositionQuery" import { PriceSummary } from "app/Components/Bidding/Screens/ConfirmBid/PriceSummary" import { Address, Bid, PaymentCardTextFieldParams } from "app/Components/Bidding/types" -import { NavigationHeader } from "app/Components/NavigationHeader" import { Modal } from "app/Components/Modal" +import { NavigationHeader } from "app/Components/NavigationHeader" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import { partnerName } from "app/Scenes/Artwork/Components/ArtworkExtraLinks/partnerName" import { navigate } from "app/system/navigation/navigate" diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx index 106d5ed3acd..eeea44cc90d 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx @@ -2,17 +2,17 @@ import { screen } from "@testing-library/react-native" import { SelectMaxBidTestsQuery } from "__generated__/SelectMaxBidTestsQuery.graphql" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { graphql } from "react-relay" -import { SelectMaxBid, SelectMaxBidContainer } from "./SelectMaxBid" +import { SelectMaxBid } from "./SelectMaxBid" describe("SelectMaxBid", () => { const { renderWithRelay } = setupTestWrapper({ - Component: ({ me, sale_artwork }) => ( - + Component: ({ me, saleArtwork }) => ( + ), query: graphql` query SelectMaxBidTestsQuery @relay_test_operation { - sale_artwork: saleArtwork(id: "wow") { - ...SelectMaxBid_sale_artwork + saleArtwork(id: "wow") { + ...SelectMaxBid_saleArtwork } me { ...SelectMaxBid_me diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx index 5d7712acf45..11f2dd82574 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx @@ -1,33 +1,147 @@ -import { Flex, Button } from "@artsy/palette-mobile" +import { Button, Flex, useScreenDimensions } from "@artsy/palette-mobile" import { SelectMaxBidQuery } from "__generated__/SelectMaxBidQuery.graphql" -import { SelectMaxBid_me$data } from "__generated__/SelectMaxBid_me.graphql" -import { SelectMaxBid_sale_artwork$data } from "__generated__/SelectMaxBid_sale_artwork.graphql" +import { SelectMaxBid_me$key } from "__generated__/SelectMaxBid_me.graphql" +import { SelectMaxBid_saleArtwork$key } from "__generated__/SelectMaxBid_saleArtwork.graphql" import { NavigationHeader } from "app/Components/NavigationHeader" import { Select } from "app/Components/Select" import { dismissModal } from "app/system/navigation/navigate" -import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" -import { ScreenDimensionsContext } from "app/utils/hooks" -import renderWithLoadProgress from "app/utils/renderWithLoadProgress" -import { Schema, screenTrack } from "app/utils/track" +import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" import { compact } from "lodash" -import React, { memo } from "react" -import { ActivityIndicator, View, ViewProps } from "react-native" -import { createRefetchContainer, graphql, QueryRenderer, RelayRefetchProp } from "react-relay" +import React, { useMemo, useState } from "react" +import { graphql, useFragment, useLazyLoadQuery, useRefetchableFragment } from "react-relay" import { ConfirmBidScreen } from "./ConfirmBid" -interface SelectMaxBidProps extends ViewProps { - sale_artwork: SelectMaxBid_sale_artwork$data - me: SelectMaxBid_me$data +interface SelectMaxBidProps { + saleArtwork: SelectMaxBid_saleArtwork$key + me: SelectMaxBid_me$key navigator: NavigatorIOS - relay: RelayRefetchProp } -interface SelectMaxBidState { - selectedBidIndex: number - isRefreshingSaleArtwork: boolean +export const SelectMaxBid: React.FC = ({ navigator, me, saleArtwork }) => { + const { height } = useScreenDimensions() + const [selectedBidIndex, setSelectedBidIndex] = useState(0) + + const [saleArtworkData, refetch] = useRefetchableFragment( + selectMaxBidSaleArtworkFragment, + saleArtwork + ) + const meData = useFragment(selectMaxBidMeFragment, me) + + const handleRefresh = () => { + refetch({}, { fetchPolicy: "network-only" }) + } + + const handleNext = () => { + navigator.push({ + component: ConfirmBidScreen, + passProps: { + me: meData, + sale_artwork: saleArtworkData, + increments: saleArtworkData.increments, + selectedBidIndex, + refreshSaleArtwork: handleRefresh, + }, + }) + } + const bids = compact(saleArtworkData.increments) || [] + + const bidOptions = useMemo( + () => bids.map((b) => ({ label: b.display || "", value: b.cents })), + [bids] + ) + + return ( + + + (null) const postalCodeRef = useRef(null) const phoneRef = useRef(null) - const countryRef = useRef>(null) return ( @@ -120,34 +125,36 @@ export const CreditCardForm: React.FC = ({ )} - addressLine1Ref.current?.focus()} + submitBehavior="submit" /> - addressLine2Ref.current?.focus()} + submitBehavior="submit" /> - = ({ onBlur={handleBlur("addressLine2")} returnKeyType="next" onSubmitEditing={() => cityRef.current?.focus()} + submitBehavior="submit" /> - stateRef.current?.focus()} + submitBehavior="submit" /> - postalCodeRef.current?.focus()} + submitBehavior="submit" /> - phoneRef.current?.focus()} + submitBehavior="submit" /> - = ({ setFieldValue("country", { shortName: countryCode, diff --git a/src/app/Components/CreditCardField/CreditCardField.tsx b/src/app/Components/CreditCardField/CreditCardField.tsx index 7ccf0182bd1..cbaf206806f 100644 --- a/src/app/Components/CreditCardField/CreditCardField.tsx +++ b/src/app/Components/CreditCardField/CreditCardField.tsx @@ -15,7 +15,7 @@ import { import { THEME } from "@artsy/palette-tokens" import { CardField } from "@stripe/stripe-react-native" import { Details } from "@stripe/stripe-react-native/lib/typescript/src/types/components/CardFieldInput" -import { useState } from "react" +import { useMemo, useState } from "react" import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated" interface CreditCardFieldProps { @@ -26,38 +26,35 @@ const STRIPE_CREDIT_CARD_ICON_CONTAINER_WIDTH = 60 export const CreditCardField: React.FC = ({ onCardChange }) => { const color = useColor() + const { theme } = useTheme() const [cardDetails, setCardDetails] = useState
() const [isFocused, setIsFocused] = useState(false) const textStyle = useTextStyleForPalette("sm") - const variant: InputVariant = getInputVariant({ - hasError: false, - disabled: false, - }) + const variant: InputVariant = getInputVariant({ hasError: false, disabled: false }) const animatedState = useSharedValue( - getInputState({ isFocused: isFocused, value: cardDetails?.number }) + getInputState({ isFocused, value: cardDetails?.number }) ) - animatedState.value = getInputState({ - isFocused: isFocused, - value: cardDetails?.number, - }) + animatedState.set(getInputState({ isFocused, value: cardDetails?.number })) - const hasSelectedValue = - cardDetails !== undefined && - (!!cardDetails.last4 || - cardDetails.validNumber !== "Incomplete" || - !!cardDetails.expiryMonth || - !!cardDetails.expiryYear) + const hasSelectedValue = useMemo(() => { + return ( + cardDetails !== undefined && + (!!cardDetails.last4 || + cardDetails.validNumber !== "Incomplete" || + !!cardDetails.expiryMonth || + !!cardDetails.expiryYear) + ) + }, [cardDetails]) - const { theme } = useTheme() const inputVariants = getInputVariants(theme) const animatedStyles = useAnimatedStyle(() => { return { - borderColor: withTiming(inputVariants[variant][animatedState.value].inputBorderColor), + borderColor: withTiming(inputVariants[variant][animatedState.get()].inputBorderColor), } }) @@ -69,7 +66,7 @@ export const CreditCardField: React.FC = ({ onCardChange } hasSelectedValue || isFocused ? 15 : STRIPE_CREDIT_CARD_ICON_CONTAINER_WIDTH ), paddingHorizontal: withTiming(hasSelectedValue || isFocused ? 5 : 0), - color: withTiming(inputVariants[variant][animatedState.value].labelColor), + color: withTiming(inputVariants[variant][animatedState.get()].labelColor), top: withTiming(hasSelectedValue || isFocused ? -INPUT_MIN_HEIGHT / 4 : 14), fontSize: withTiming( hasSelectedValue || isFocused @@ -83,11 +80,7 @@ export const CreditCardField: React.FC = ({ onCardChange } = ({ onCardChange } textColor: color("black100"), placeholderColor: color("black60"), }} - style={{ - width: "100%", - height: INPUT_MIN_HEIGHT, - }} + style={{ width: "100%", height: INPUT_MIN_HEIGHT }} postalCodeEnabled={false} onCardChange={(cardDetails) => { setCardDetails(cardDetails) @@ -116,6 +106,7 @@ export const CreditCardField: React.FC = ({ onCardChange } onBlur={() => setIsFocused(false)} /> + {hasSelectedValue || isFocused ? "Credit Card" : ""} From c08d68c82fe905ae8fae40b2f5b6d0b93c2153d1 Mon Sep 17 00:00:00 2001 From: Sultan Date: Fri, 7 Feb 2025 12:18:01 +0100 Subject: [PATCH 05/26] add BidFlowContextStore --- .../Context/BidFlowContextProvider.tests.tsx | 89 +++++++++++++++++++ .../Context/BidFlowContextProvider.tsx | 52 +++++++++++ .../Components/Bidding/Screens/BidResult.tsx | 4 +- .../Bidding/Screens/ConfirmBid/index.tsx | 45 ++++++---- .../Bidding/Screens/SelectMaxBid.tsx | 22 +++-- src/app/Components/Containers/BidFlow.tsx | 6 +- 6 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx create mode 100644 src/app/Components/Bidding/Context/BidFlowContextProvider.tsx diff --git a/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx b/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx new file mode 100644 index 00000000000..fd6cec5edb4 --- /dev/null +++ b/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx @@ -0,0 +1,89 @@ +import { + BidFlowContextModel, + getBidFlowContextStore, +} from "app/Components/Bidding/Context/BidFlowContextProvider" +import { createStore, State } from "easy-peasy" + +const createBidFlowStore = (state: any) => + createStore({ ...getBidFlowContextStore(), ...state }) + +describe("Bid Flow Context Store", () => { + it("sets values as expected", () => { + const bidFlowStates = { + saleArtworkIncrements: mockIncrements, + selectedBidIndex: 1, + billingAddress: mockBillingAddress, + creditCardToken: mockStripeToken, + biddingEndAt: "2021-12-31T23:59:59Z", + } + + const store = createBidFlowStore(bidFlowStates) + + expect(store.getState().saleArtworkIncrements).toEqual(mockIncrements) + expect(store.getState().selectedBidIndex).toEqual(1) + expect(store.getState().billingAddress).toEqual(mockBillingAddress) + expect(store.getState().creditCardToken).toEqual(mockStripeToken) + expect(store.getState().biddingEndAt).toEqual("2021-12-31T23:59:59Z") + }) + + it("updates values as expected", () => { + const store = createBidFlowStore({}) + + store.getActions().setSaleArtworkIncrements(mockIncrements) + store.getActions().setSelectedBidIndex(1) + store.getActions().setBillingAddress(mockBillingAddress) + store.getActions().setCreditCardToken(mockStripeToken as any) + store.getActions().setBiddingEndAt("2021-12-31T23:59:59Z") + + expect(store.getState().saleArtworkIncrements).toEqual(mockIncrements) + expect(store.getState().selectedBidIndex).toEqual(1) + expect(store.getState().billingAddress).toEqual(mockBillingAddress) + expect(store.getState().creditCardToken).toEqual(mockStripeToken) + expect(store.getState().biddingEndAt).toEqual("2021-12-31T23:59:59Z") + }) + + it("gets the correct selectedBid", () => { + const store = createBidFlowStore({}) + + store.getActions().setSaleArtworkIncrements(mockIncrements) + store.getActions().setSelectedBidIndex(1) + + expect(store.getState().selectedBid).toEqual(mockIncrements[1]) + + store.getActions().setSelectedBidIndex(2) + + expect(store.getState().selectedBid).toEqual(mockIncrements[2]) + }) +}) + +const mockBillingAddress = { + fullName: "John Doe", + addressLine1: "123 Main St", + addressLine2: "Apt 1", + city: "New York", + state: "NY", + country: { + longName: "United States", + shortName: "US", + }, + postalCode: "10001", + phoneNumber: "123-456-7890", +} + +const mockStripeToken = { + id: "fake-token", + created: "1528229731", + livemode: 0, + card: { + brand: "VISA", + last4: "4242", + }, + bankAccount: null, + extra: null, +} + +const mockIncrements = [ + { cents: 100, display: "100" }, + { cents: 200, display: "200" }, + { cents: 300, display: "300" }, +] diff --git a/src/app/Components/Bidding/Context/BidFlowContextProvider.tsx b/src/app/Components/Bidding/Context/BidFlowContextProvider.tsx new file mode 100644 index 00000000000..396d64d17da --- /dev/null +++ b/src/app/Components/Bidding/Context/BidFlowContextProvider.tsx @@ -0,0 +1,52 @@ +import { Token } from "@stripe/stripe-react-native" +import { SelectMaxBid_saleArtwork$data } from "__generated__/SelectMaxBid_saleArtwork.graphql" +import { Address, PaymentCardTextFieldParams } from "app/Components/Bidding/types" +import { action, Action, Computed, computed, createContextStore } from "easy-peasy" + +type Increments = NonNullable[number]>[] + +export interface BidFlowContextModel { + saleArtworkIncrements: Increments + selectedBidIndex: number + billingAddress?: Address + creditCardFormParams?: PaymentCardTextFieldParams + creditCardToken?: Token.Result + biddingEndAt?: string | null + selectedBid: Computed + setSaleArtworkIncrements: Action + setSelectedBidIndex: Action + setBillingAddress: Action + setCreditCardToken: Action + setBiddingEndAt: Action +} + +export const getBidFlowContextStore = (): BidFlowContextModel => ({ + saleArtworkIncrements: [], + selectedBidIndex: 0, + billingAddress: undefined, + creditCardToken: undefined, + biddingEndAt: null, + selectedBid: computed((state) => state.saleArtworkIncrements[state.selectedBidIndex]), + setSaleArtworkIncrements: action((state, payload) => { + state.saleArtworkIncrements = payload + }), + setSelectedBidIndex: action((state, payload) => { + state.selectedBidIndex = payload + }), + setBillingAddress: action((state, payload) => { + state.billingAddress = payload + }), + setCreditCardToken: action((state, payload) => { + state.creditCardToken = payload + }), + setBiddingEndAt: action((state, payload) => { + state.biddingEndAt = payload + }), +}) + +export const BidFlowContextStore = createContextStore((runtimeModel) => ({ + ...getBidFlowContextStore(), + ...runtimeModel, +})) + +export const BidFlowContextProvider = BidFlowContextStore.Provider diff --git a/src/app/Components/Bidding/Screens/BidResult.tsx b/src/app/Components/Bidding/Screens/BidResult.tsx index 62a3559ffc6..c0f701bedd8 100644 --- a/src/app/Components/Bidding/Screens/BidResult.tsx +++ b/src/app/Components/Bidding/Screens/BidResult.tsx @@ -1,6 +1,7 @@ import { Button, Flex, Text } from "@artsy/palette-mobile" import { BidResult_saleArtwork$key } from "__generated__/BidResult_saleArtwork.graphql" import { Timer } from "app/Components/Bidding/Components/Timer" +import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" import { BidderPositionResult } from "app/Components/Bidding/types" import { Markdown } from "app/Components/Markdown" import { NavigationHeader } from "app/Components/NavigationHeader" @@ -20,7 +21,6 @@ interface BidResultProps { navigator: NavigatorIOS refreshBidderInfo?: () => void refreshSaleArtwork?: () => void - biddingEndAt?: string } const POLLING_TIMEOUT_MESSAGES = { @@ -43,8 +43,8 @@ export const BidResult: React.FC = ({ navigator, refreshBidderInfo, refreshSaleArtwork, - biddingEndAt, }) => { + const biddingEndAt = BidFlowContextStore.useStoreState((state) => state.biddingEndAt) const saleArtworkData = useFragment(bidResultFragment, saleArtwork) const { status, messageHeader, messageDescriptionMD } = bidderPositionResult diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index af70bbc65ae..c8d7dccc9f4 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -1,5 +1,4 @@ import { Box, Button, Checkbox, LinkText, Text } from "@artsy/palette-mobile" -import { Token } from "@stripe/stripe-react-native" import { BidderPositionQuery, BidderPositionQuery$data, @@ -14,11 +13,11 @@ import { BidInfoRow } from "app/Components/Bidding/Components/BidInfoRow" import { Divider } from "app/Components/Bidding/Components/Divider" import { PaymentInfo } from "app/Components/Bidding/Components/PaymentInfo" import { Timer } from "app/Components/Bidding/Components/Timer" +import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" import { Flex } from "app/Components/Bidding/Elements/Flex" import { BidResult } from "app/Components/Bidding/Screens/BidResult" import { bidderPositionQuery } from "app/Components/Bidding/Screens/ConfirmBid/BidderPositionQuery" import { PriceSummary } from "app/Components/Bidding/Screens/ConfirmBid/PriceSummary" -import { Address } from "app/Components/Bidding/types" import { Modal } from "app/Components/Modal" import { NavigationHeader } from "app/Components/NavigationHeader" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" @@ -30,7 +29,7 @@ import { useCreateBidderPosition } from "app/utils/mutations/useCreateBidderPosi import { useCreateCreditCard } from "app/utils/mutations/useCreateCreditCard" import { useUpdateUserPhoneNumber } from "app/utils/mutations/useUpdateUserPhoneNumber" import { Schema } from "app/utils/track" -import React, { useMemo, useRef, useState } from "react" +import React, { useEffect, useMemo, useRef, useState } from "react" import { Image, ScrollView } from "react-native" import { graphql, useRefetchableFragment } from "react-relay" import { useTracking } from "react-tracking" @@ -53,15 +52,11 @@ export interface ConfirmBidProps { me: ConfirmBid_me$key navigator?: NavigatorIOS refreshSaleArtwork?: () => void - increments: any - selectedBidIndex: number } export const ConfirmBid: React.FC = ({ - increments, me, saleArtwork, - selectedBidIndex, refreshSaleArtwork, navigator, }) => { @@ -74,11 +69,21 @@ export const ConfirmBid: React.FC = ({ const [meData, refetchMe] = useRefetchableFragment(confirmBidMeFragment, me) const { sale, endAt, extendedBiddingEndAt } = saleArtworkData - const [currentBiddingEndAt, setBiddingEndAt] = useState( - extendedBiddingEndAt || endAt || sale?.endAt + // store states + const biddingEndAt = BidFlowContextStore.useStoreState((state) => state.biddingEndAt) + const creditCardToken = BidFlowContextStore.useStoreState((state) => state.creditCardToken) + const billingAddress = BidFlowContextStore.useStoreState((state) => state.billingAddress) + const selectedBid = BidFlowContextStore.useStoreState((state) => state.selectedBid) + + // store actions + const setBiddingEndAt = BidFlowContextStore.useStoreActions((actions) => actions.setBiddingEndAt) + const setCreditCardToken = BidFlowContextStore.useStoreActions( + (actions) => actions.setCreditCardToken + ) + const setBillingAddress = BidFlowContextStore.useStoreActions( + (actions) => actions.setBillingAddress ) - const [creditCardToken, setCreditCardToken] = useState() - const [billingAddress, setBillingAddress] = useState
() + const [errorMessage, setErrorMessage] = useState("") const [errorModalVisible, setErrorModalVisible] = useState(false) const [conditionsOfSaleChecked, setConditionsOfSaleChecked] = useState(false) @@ -90,6 +95,10 @@ export const ConfirmBid: React.FC = ({ const requiresCheckbox = !sale?.bidder const requiresPaymentInformation = !(sale?.bidder || meData.hasQualifiedCreditCards) + useEffect(() => { + setBiddingEndAt(extendedBiddingEndAt || endAt || sale?.endAt) + }, [saleArtworkData]) + const canPlaceBid = useMemo(() => { switch (true) { case requiresPaymentInformation: @@ -156,12 +165,16 @@ export const ConfirmBid: React.FC = ({ }) } + if (selectedBid?.cents == null) { + throw new Error("Selected bid amount is not valid") + } + createBidderPosition({ variables: { input: { saleID: sale.slug, artworkID: saleArtworkData?.artwork?.slug as string, - maxBidAmountCents: increments[selectedBidIndex].cents, + maxBidAmountCents: selectedBid.cents, }, }, onCompleted: (results, errors) => { @@ -234,7 +247,7 @@ export const ConfirmBid: React.FC = ({ passProps: { saleArtwork: saleArtworkData, bidderPositionResult: resultForNetworkError, - biddingEndAt: currentBiddingEndAt, + biddingEndAt, }, }) } else { @@ -260,7 +273,7 @@ export const ConfirmBid: React.FC = ({ bidderPositionResult, refreshBidderInfo, refreshSaleArtwork, - biddingEndAt: currentBiddingEndAt, + biddingEndAt, }, }) } @@ -287,7 +300,7 @@ export const ConfirmBid: React.FC = ({ @@ -334,7 +347,7 @@ export const ConfirmBid: React.FC = ({ (mutationInProgress ? null : navigator?.pop())} /> diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx index 6827ddc5e35..98602cd97a6 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx @@ -2,13 +2,14 @@ import { Button, Flex, useScreenDimensions } from "@artsy/palette-mobile" import { SelectMaxBidQuery } from "__generated__/SelectMaxBidQuery.graphql" import { SelectMaxBid_me$key } from "__generated__/SelectMaxBid_me.graphql" import { SelectMaxBid_saleArtwork$key } from "__generated__/SelectMaxBid_saleArtwork.graphql" +import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" import { NavigationHeader } from "app/Components/NavigationHeader" import { Select } from "app/Components/Select" import { dismissModal } from "app/system/navigation/navigate" import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" import { compact } from "lodash" -import React, { useMemo, useState } from "react" +import React, { useEffect, useMemo } from "react" import { graphql, useFragment, useLazyLoadQuery, useRefetchableFragment } from "react-relay" import { ConfirmBid } from "./ConfirmBid" @@ -19,8 +20,14 @@ interface SelectMaxBidProps { } export const SelectMaxBid: React.FC = ({ navigator, me, saleArtwork }) => { + const selectedBidIndex = BidFlowContextStore.useStoreState((state) => state.selectedBidIndex) + const bids = BidFlowContextStore.useStoreState((state) => state.saleArtworkIncrements) + const setSelectedBidIndex = BidFlowContextStore.useStoreActions( + (actions) => actions.setSelectedBidIndex + ) + const setBids = BidFlowContextStore.useStoreActions((actions) => actions.setSaleArtworkIncrements) + const { height } = useScreenDimensions() - const [selectedBidIndex, setSelectedBidIndex] = useState(0) const [saleArtworkData, refetch] = useRefetchableFragment( selectMaxBidSaleArtworkFragment, @@ -29,22 +36,25 @@ export const SelectMaxBid: React.FC = ({ navigator, me, saleA const meData = useFragment(selectMaxBidMeFragment, me) const handleRefresh = () => { - refetch({}, { fetchPolicy: "network-only" }) + refetch({}, { fetchPolicy: "store-and-network" }) } + useEffect(() => { + if (saleArtworkData.increments?.length) { + setBids(compact(saleArtworkData.increments)) + } + }, [saleArtworkData]) + const handleNext = () => { navigator.push({ component: ConfirmBid, passProps: { me: meData, saleArtwork: saleArtworkData, - increments: saleArtworkData.increments, - selectedBidIndex, refreshSaleArtwork: handleRefresh, }, }) } - const bids = compact(saleArtworkData.increments) || [] const bidOptions = useMemo( () => bids.map((b) => ({ label: b.display || "", value: b.cents })), diff --git a/src/app/Components/Containers/BidFlow.tsx b/src/app/Components/Containers/BidFlow.tsx index 1bf6f53ec08..70057fb8f94 100644 --- a/src/app/Components/Containers/BidFlow.tsx +++ b/src/app/Components/Containers/BidFlow.tsx @@ -1,5 +1,5 @@ import { Screen } from "@artsy/palette-mobile" -import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" +import { BidFlowContextProvider } from "app/Components/Bidding/Context/BidFlowContextProvider" import { TimeOffsetProvider } from "app/Components/Bidding/Context/TimeOffsetProvider" import { SelectMaxBidQueryRenderer } from "app/Components/Bidding/Screens/SelectMaxBid" import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" @@ -11,13 +11,13 @@ export const BidFlow: React.FC< return ( - + - + ) From 2b77420b182979db842af7cabbe883dd3861229b Mon Sep 17 00:00:00 2001 From: Sultan Date: Fri, 7 Feb 2025 12:18:55 +0100 Subject: [PATCH 06/26] refactor Price Summary --- .../Context/BidFlowContextProvider.tests.tsx | 2 +- .../Screens/ConfirmBid/PriceSummary.tsx | 116 ++++++++++++++++-- .../Bidding/Screens/ConfirmBid/index.tsx | 2 +- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx b/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx index fd6cec5edb4..1912bda3f66 100644 --- a/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx +++ b/src/app/Components/Bidding/Context/BidFlowContextProvider.tests.tsx @@ -2,7 +2,7 @@ import { BidFlowContextModel, getBidFlowContextStore, } from "app/Components/Bidding/Context/BidFlowContextProvider" -import { createStore, State } from "easy-peasy" +import { createStore } from "easy-peasy" const createBidFlowStore = (state: any) => createStore({ ...getBidFlowContextStore(), ...state }) diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/PriceSummary.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/PriceSummary.tsx index 93ee741ce7a..42d58d4fab5 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/PriceSummary.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/PriceSummary.tsx @@ -1,11 +1,110 @@ -import { Flex, Box, Text } from "@artsy/palette-mobile" +import { Box, Flex, Text } from "@artsy/palette-mobile" import { PriceSummaryQuery } from "__generated__/PriceSummaryQuery.graphql" -import { PriceSummary_calculatedCost$data } from "__generated__/PriceSummary_calculatedCost.graphql" -import { Bid } from "app/Components/Bidding/types" -import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" -import renderWithLoadProgress from "app/utils/renderWithLoadProgress" -import { createFragmentContainer, graphql, QueryRenderer } from "react-relay" +import { PriceSummary_calculatedCost$key } from "__generated__/PriceSummary_calculatedCost.graphql" +import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" +import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" +import { graphql, useFragment, useLazyLoadQuery } from "react-relay" +interface PriceSummaryProps { + calculatedCost: PriceSummary_calculatedCost$key +} + +const PriceSummary_: React.FC = ({ calculatedCost }) => { + const selectedBid = BidFlowContextStore.useStoreState((state) => state.selectedBid) + const { subtotal, buyersPremium } = useFragment(priceSummaryFragment, calculatedCost) + + if (!subtotal || !buyersPremium) { + return null + } + + return ( + + + Summary + + + + + Your max bid + + + {`${selectedBid.display}.00`} + + + + + + Buyer’s premium + + + {buyersPremium.display} + + + + + + Subtotal + + + {subtotal.display} + + + + + Plus any applicable shipping, taxes, and fees. + + + ) +} + +interface PriceSummaryQRProps { + saleArtworkId: string +} + +export const PriceSummary = withSuspense({ + Component: ({ saleArtworkId }) => { + const selectedBid = BidFlowContextStore.useStoreState((state) => state.selectedBid) + + const initialData = useLazyLoadQuery(priceSummaryQuery, { + saleArtworkId: saleArtworkId, + bidAmountMinor: selectedBid.cents ?? 0, + }) + + if (!initialData?.node?.calculatedCost) { + return null + } + + return + }, + ErrorFallback: NoFallback, + LoadingFallback: SpinnerFallback, +}) + +const priceSummaryQuery = graphql` + query PriceSummaryQuery($saleArtworkId: ID!, $bidAmountMinor: Int!) { + node(id: $saleArtworkId) { + ... on SaleArtwork { + calculatedCost(bidAmountMinor: $bidAmountMinor) { + ...PriceSummary_calculatedCost + } + } + } + } +` + +const priceSummaryFragment = graphql` + fragment PriceSummary_calculatedCost on CalculatedCost { + buyersPremium { + display + } + subtotal { + display + } + } +` + +// TODO: Clean up old code +/* interface PriceSummaryViewProps { calculatedCost: PriceSummary_calculatedCost$data bid: Bid @@ -31,7 +130,7 @@ const _PriceSummary = ({ bid, calculatedCost }: PriceSummaryViewProps) => ( Buyer’s premium - {calculatedCost.buyersPremium! /* STRICTNESS_MIGRATION */.display} + {calculatedCost.buyersPremium! /* STRICTNESS_MIGRATION * /.display} @@ -40,7 +139,7 @@ const _PriceSummary = ({ bid, calculatedCost }: PriceSummaryViewProps) => ( Subtotal - {calculatedCost.subtotal! /* STRICTNESS_MIGRATION */.display} + {calculatedCost.subtotal! /* STRICTNESS_MIGRATION * /.display} @@ -90,3 +189,4 @@ export const PriceSummary = ({ saleArtworkId, bid }: PriceSummaryProps) => ( ))} /> ) +*/ diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index c8d7dccc9f4..95ced860f25 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -366,7 +366,7 @@ export const ConfirmBid: React.FC = ({ )} - + Date: Mon, 10 Feb 2025 12:24:40 +0100 Subject: [PATCH 07/26] fix confirmBid tests --- .../Bidding/Screens/ConfirmBid.tests.tsx | 610 +++++++----------- .../Bidding/Screens/ConfirmBid/index.tsx | 18 +- src/app/utils/hooks/withSuspense.tsx | 2 +- .../utils/mutations/useCreateCreditCard.ts | 2 + 4 files changed, 261 insertions(+), 371 deletions(-) diff --git a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx index 6d65b6ff429..6df78b678d8 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx @@ -1,16 +1,22 @@ -import { Button, Checkbox, Text } from "@artsy/palette-mobile" -import { createToken } from "@stripe/stripe-react-native" -import { fireEvent, screen, waitFor } from "@testing-library/react-native" +import { Button, Checkbox } from "@artsy/palette-mobile" +import { + fireEvent, + screen, + waitFor, + waitForElementToBeRemoved, +} from "@testing-library/react-native" import { BidderPositionQuery$data } from "__generated__/BidderPositionQuery.graphql" import { ConfirmBid_saleArtwork$data } from "__generated__/ConfirmBid_saleArtwork.graphql" import { useCreateBidderPositionMutation } from "__generated__/useCreateBidderPositionMutation.graphql" import { useCreateCreditCardMutation } from "__generated__/useCreateCreditCardMutation.graphql" import { useUpdateUserPhoneNumberMutation } from "__generated__/useUpdateUserPhoneNumberMutation.graphql" -import { FakeNavigator } from "app/Components/Bidding/Helpers/FakeNavigator" -import { ConfirmBid, ConfirmBidProps } from "app/Components/Bidding/Screens/ConfirmBid" +import { + BidFlowContextProvider, + BidFlowContextStore, +} from "app/Components/Bidding/Context/BidFlowContextProvider" +import { ConfirmBid } from "app/Components/Bidding/Screens/ConfirmBid" import { bidderPositionQuery } from "app/Components/Bidding/Screens/ConfirmBid/BidderPositionQuery" import { Address } from "app/Components/Bidding/types" -import { Modal } from "app/Components/Modal" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import * as navigation from "app/system/navigation/navigate" import NavigatorIOS, { @@ -19,15 +25,12 @@ import NavigatorIOS, { import { useCreateBidderPosition } from "app/utils/mutations/useCreateBidderPosition" import { useCreateCreditCard } from "app/utils/mutations/useCreateCreditCard" import { useUpdateUserPhoneNumber } from "app/utils/mutations/useUpdateUserPhoneNumber" -import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" +import { CleanRelayFragment } from "app/utils/relayHelpers" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { merge } from "lodash" -import { TouchableWithoutFeedback } from "react-native" import relay, { graphql } from "react-relay" -import { ReactTestRenderer } from "react-test-renderer" import { BidResult } from "./BidResult" import { CreditCardForm } from "./CreditCardForm" -import { SelectMaxBid } from "./SelectMaxBid" jest.mock("app/Components/Bidding/Screens/ConfirmBid/BidderPositionQuery", () => ({ bidderPositionQuery: jest.fn(), @@ -63,10 +66,6 @@ describe("ConfirmBid", () => { const mockPostNotificationName = LegacyNativeModules.ARNotificationsManager .postNotificationName as jest.Mock - const findPlaceBidButton = (component: ReactTestRenderer) => { - return component.root.findByProps({ testID: "bid-button" }) - } - const mockCreateBidderMutation = jest.fn() const useCreateBidderPositionMock = useCreateBidderPosition as jest.Mock const mockCreateCreditCardMutation = jest.fn() @@ -74,14 +73,34 @@ describe("ConfirmBid", () => { const mockUpdateUserPhoneNumberMutation = jest.fn() const useUpdateUserPhoneNumberMock = useUpdateUserPhoneNumber as jest.Mock const navigateSpy = jest.spyOn(navigation, "navigate") + let mockStore: ReturnType - // const mountConfirmBidComponent = (props: ConfirmBidProps) => { - // return renderWithWrappersLEGACY() - // } + const MockStoreInstance = () => { + mockStore = BidFlowContextStore.useStore() + return null + } const { renderWithRelay } = setupTestWrapper({ Component: (props: any) => { - return + return ( + + + + + ) }, query: graphql` query ConfirmBidTestQuery($artworkID: String!, $saleID: String!) @relay_test_operation { @@ -115,19 +134,13 @@ describe("ConfirmBid", () => { describe("disclaimer", () => { describe("when the user is not registered", () => { it("displays a checkbox", () => { - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) expect(screen.getByTestId("disclaimer-checkbox")).toBeOnTheScreen() }) it("displays a disclaimer", () => { - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) expect(screen.getByTestId("disclaimer")).toHaveTextContent( /I agree to Artsy's and Christie's General Terms and Conditions of Sale. I understand that all bids are binding and may not be retracted./ @@ -135,10 +148,7 @@ describe("ConfirmBid", () => { }) it("navigates to the conditions of sale when the user taps the link", () => { - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) fireEvent.press( screen.getByText("Artsy's and Christie's General Terms and Conditions of Sale") @@ -150,25 +160,13 @@ describe("ConfirmBid", () => { describe("when the user is registered", () => { it("does not display a checkbox", () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtworkRegisteredForBidding, - }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }) expect(screen.queryByTestId("disclaimer-checkbox")).not.toBeOnTheScreen() }) it("displays a disclaimer", () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtworkRegisteredForBidding, - }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }) expect( screen.getByText( @@ -178,13 +176,7 @@ describe("ConfirmBid", () => { }) it("navigates to the conditions of sale when the user taps the link", () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtworkRegisteredForBidding, - }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }) fireEvent.press( screen.getByText("Artsy's and Christie's General Terms and Conditions of Sale") @@ -196,13 +188,10 @@ describe("ConfirmBid", () => { }) it("enables the bid button when checkbox is ticked", () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) expect(screen.getByTestId("bid-button")).toBeDisabled() @@ -212,13 +201,13 @@ describe("ConfirmBid", () => { }) it("enables the bid button by default if the user is registered", () => { - renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }, initialProps) + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }) expect(screen.getByTestId("bid-button")).toBeEnabled() }) it("displays the artwork title correctly with date", () => { - renderWithRelay({ SaleArtwork: () => saleArtwork }, initialProps) + renderWithRelay({ SaleArtwork: () => saleArtwork }) expect(screen.getByText("Meteor Shower, 2015")).toBeOnTheScreen() }) @@ -226,22 +215,16 @@ describe("ConfirmBid", () => { it("displays the artwork title correctly without date", () => { const datelessSaleArtwork = merge({}, saleArtwork, { artwork: { date: null } }) - renderWithRelay({ SaleArtwork: () => datelessSaleArtwork }, initialProps) + renderWithRelay({ SaleArtwork: () => datelessSaleArtwork }) expect(screen.queryByText("Meteor Shower, 2015")).not.toBeOnTheScreen() expect(screen.getByText("Meteor Shower")).toBeOnTheScreen() }) it("can load and display price summary", async () => { - const { mockResolveLastOperation } = renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + const { mockResolveLastOperation } = renderWithRelay({ SaleArtwork: () => saleArtwork }) - expect(screen.getByTestId("relay-loading")).toBeOnTheScreen() + expect(screen.getByTestId("default-loading-feedback")).toBeOnTheScreen() mockResolveLastOperation({ SaleArtwork: () => ({ @@ -252,6 +235,8 @@ describe("ConfirmBid", () => { }), }) + await waitForElementToBeRemoved(() => screen.queryByTestId("default-loading-feedback")) + expect(screen.getByText("Your max bid")).toBeOnTheScreen() expect(screen.getByText("$45,000.00")).toBeOnTheScreen() @@ -264,7 +249,7 @@ describe("ConfirmBid", () => { describe("checkbox and payment info display", () => { it("shows no checkbox or payment info if the user is registered", () => { - renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }, initialProps) + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }) expect(screen.queryByTestId("disclaimer-checkbox")).not.toBeOnTheScreen() expect(screen.queryByTestId("payment-info-row")).not.toBeOnTheScreen() @@ -272,25 +257,17 @@ describe("ConfirmBid", () => { }) it("shows a checkbox but no payment info if the user is not registered and has cc on file", () => { - renderWithRelay( - { - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) expect(screen.getByTestId("disclaimer-checkbox")).toBeOnTheScreen() expect(screen.queryByTestId("payment-info-row")).not.toBeOnTheScreen() }) it("shows a checkbox and payment info if the user is not registered and has no cc on file", async () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: false }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) expect(screen.getByTestId("disclaimer-checkbox")).toBeOnTheScreen() expect(screen.getByText("Max bid")).toBeOnTheScreen() @@ -300,13 +277,10 @@ describe("ConfirmBid", () => { describe("when pressing bid button", () => { it("commits mutation", () => { - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -314,34 +288,25 @@ describe("ConfirmBid", () => { expect(mockCreateBidderMutation).toHaveBeenCalled() }) - it("shows a spinner", async () => { + it("shows a spinner", () => { useCreateBidderPositionMock.mockReturnValue([mockCreateBidderMutation, true]) - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: true }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) - const button = screen.UNSAFE_getByType(Button) + const placeBidButton = screen.UNSAFE_getByType(Button) - fireEvent.press(button) + fireEvent.press(placeBidButton) - expect(button.props.loading).toEqual(true) + expect(placeBidButton.props.loading).toEqual(true) }) - it("disables tap events while a spinner is being shown", async () => { + it("disables tap events while a spinner is being shown", () => { const navigator = { push: jest.fn(), pop: jest.fn() } useCreateBidderPositionMock.mockReturnValue([mockCreateBidderMutation, true]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - { ...initialProps, navigator } - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }, { navigator }) const yourMaxBidRow = screen.getByText("Max bid") const conditionsOfSaleLink = screen.getByText( @@ -365,10 +330,7 @@ describe("ConfirmBid", () => { describe("when pressing bid", () => { it("commits the mutation", () => { - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ SaleArtwork: () => saleArtwork }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -384,14 +346,13 @@ describe("ConfirmBid", () => { describe("when mutation fails", () => { it("does not verify bid position", () => { // Probably due to a network problem. - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) - console.error = jest.fn() // Silences component logging. relay.commitMutation = commitMutationMock((_, { onError }) => { onError!(new Error("An error occurred.")) return { dispose: jest.fn() } @@ -411,13 +372,12 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([erroredCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) - console.error = jest.fn() // Silences component logging. fireEvent.press(screen.getByTestId("bid-button")) @@ -446,10 +406,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([erroredCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) fireEvent.press(screen.getByTestId("bid-button")) @@ -471,47 +431,22 @@ describe("ConfirmBid", () => { }) }) - xdescribe("editing bid amount", () => { - it("allows you to go to the max bid edit screen and select a new max bid", async () => { - const fakeNavigator = new FakeNavigator() - const fakeNavigatorProps = { - ...initialPropsForRegisteredUser, - navigator: fakeNavigator, - } - fakeNavigator.push({ - component: SelectMaxBid, - id: "", - title: "", - passProps: fakeNavigatorProps, - }) - fakeNavigator.push({ - component: ConfirmBid, - id: "", - title: "", - passProps: fakeNavigatorProps, - }) - - const view = mountConfirmBidComponent({ - ...initialPropsForRegisteredUser, - navigator: fakeNavigator, - }) + describe("editing bid amount", () => { + it("allows you to go to the max bid edit screen and select a new max bid", () => { + const navigator = { pop: jest.fn() } - const selectMaxBidRow = (await view.root.findAllByType(TouchableWithoutFeedback))[0] + renderWithRelay({ SaleArtwork: () => saleArtworkRegisteredForBidding }, { navigator }) - // eslint-disable-next-line testing-library/no-node-access - expect((await selectMaxBidRow.findAllByType(Text))[1].props.children).toEqual("$45,000") + fireEvent.press(screen.getByText("Edit")) + expect(navigator.pop).toHaveBeenCalled() - selectMaxBidRow.props.onPress() + expect(mockStore.getState().selectedBidIndex).toEqual(0) + expect(mockStore.getState().selectedBid).toEqual({ cents: 450000, display: "$45,000" }) - const editScreen = await fakeNavigator.nextStep().root.findByType(SelectMaxBid) + mockStore.getActions().setSelectedBidIndex(1) - expect(editScreen.props.selectedBidIndex).toEqual(0) - - editScreen.instance.setState({ selectedBidIndex: 1 }) - ;(await editScreen.findByType(Button)).props.onPress() - - const { selectedBidIndex } = fakeNavigator.nextRoute().passProps as any - expect(selectedBidIndex).toEqual(1) + expect(mockStore.getState().selectedBidIndex).toEqual(1) + expect(mockStore.getState().selectedBid).toEqual({ cents: 460000, display: "$46,000" }) }) }) @@ -525,10 +460,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -564,10 +499,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -594,10 +529,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -625,10 +560,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -655,10 +590,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -689,7 +624,7 @@ describe("ConfirmBid", () => { renderWithRelay( { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - { ...initialProps, navigator } + { navigator } ) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -722,7 +657,7 @@ describe("ConfirmBid", () => { }, status: "RESERVE_NOT_MET", }, - sale_artwork: expect.objectContaining({ + saleArtwork: expect.objectContaining({ artwork: { artistNames: "Makiko Kudo", date: "2015", @@ -765,10 +700,10 @@ describe("ConfirmBid", () => { }) useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) - renderWithRelay( - { Me: () => ({ hasQualifiedCreditCards: true }), SaleArtwork: () => saleArtwork }, - initialProps - ) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: true }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByTestId("disclaimer-checkbox")) @@ -787,54 +722,40 @@ describe("ConfirmBid", () => { }) }) - // TODO: Update after adding bidflow state - describe.skip("ConfirmBid for unqualified user", () => { - const fillOutFormAndSubmit = async (component: ReactTestRenderer) => { - const confirmBidComponent = await component.root.findByType(ConfirmBid) - // manually setting state to avoid duplicating tests for skipping UI interaction, but practically better not to do this. - confirmBidComponent.instance.setState({ billingAddress }) - confirmBidComponent.instance.setState({ creditCardToken: stripeToken.token }) + describe("ConfirmBid for unqualified user", () => { + const mockFillAndSubmit = () => { + // updating bid flow state + mockStore.getActions().setBillingAddress(billingAddress) + mockStore.getActions().setCreditCardToken(stripeToken.token as any) - const checkbox = await component.root.findByType(Checkbox) - checkbox.props.onPress() - - const bidButton = await findPlaceBidButton(component) - bidButton.props.onPress() + // check the checkbox and press the Bid button + fireEvent.press(screen.getByTestId("disclaimer-checkbox")) + fireEvent.press(screen.getByTestId("bid-button")) } - it("shows the credit card form when the user tap the edit text in the credit card row", async () => { - // const view = mountConfirmBidComponent(initialPropsForUnqualifiedUser) - // const creditcardRow = (await view.root.findAllByType(TouchableWithoutFeedback))[1] - renderWithRelay( - { - Me: () => ({ hasQualifiedCreditCards: false }), - SaleArtwork: () => saleArtwork, - }, - initialProps - ) + it("shows the credit card form when the user tap the edit text in the credit card row", () => { + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) fireEvent.press(screen.getByText("Add")) expect(nextStep?.component).toEqual(CreditCardForm) }) - xit("shows the error screen when stripe's API returns an error", async () => { - renderWithWrappers() - relay.commitMutation = commitMutationMock((_, { onCompleted }) => { - onCompleted!({}, null) - return { dispose: jest.fn() } - }) as any - ;(createToken as jest.Mock).mockImplementationOnce(() => { - throw new Error("Error tokenizing card") + it("shows the error screen when stripe's API returns an error", async () => { + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { + onCompleted({}, [new Error("Stripe API error")]) }) + useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) - // UNSAFELY getting the component instance to set state for testing purposes only - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ billingAddress }) - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ creditCardToken: stripeToken }) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - // Check the checkbox and press the Bid button - fireEvent.press(screen.UNSAFE_getByType(Checkbox)) - fireEvent.press(screen.getByTestId("bid-button")) + mockFillAndSubmit() // wait for modal to be displayed await screen.findByText( @@ -852,21 +773,18 @@ describe("ConfirmBid", () => { ).not.toBeOnTheScreen() }) - xit("shows the error screen with the correct error message on a createCreditCard mutation failure", async () => { - renderWithWrappers() - ;(createToken as jest.Mock).mockReturnValueOnce(stripeToken) - relay.commitMutation = commitMutationMock((_, { onCompleted }) => { - onCompleted!(mockRequestResponses.creatingCreditCardError, null) - return { dispose: jest.fn() } - }) as any + it("shows the error screen with the correct error message on a createCreditCard mutation failure", async () => { + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { + onCompleted(mockRequestResponses.creatingCreditCardError, null) + }) + useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) - // UNSAFELY getting the component instance to set state for testing purposes only - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ billingAddress }) - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ creditCardToken: stripeToken }) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - // Check the checkbox and press the Bid button - fireEvent.press(screen.UNSAFE_getByType(Checkbox)) - fireEvent.press(screen.getByTestId("bid-button")) + mockFillAndSubmit() await screen.findByText("Your card's security code is incorrect.") @@ -877,50 +795,47 @@ describe("ConfirmBid", () => { expect(screen.queryByText("Your card's security code is incorrect.")).not.toBeOnTheScreen() }) - xit("shows the error screen with the default error message if there are unhandled errors from the createCreditCard mutation", async () => { + it("shows the error screen with the default error message if there are unhandled errors from the createCreditCard mutation", async () => { const errors = [{ message: "malformed error" }] + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { + onCompleted({}, errors) + }) + useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) - console.error = jest.fn() // Silences component logging. - ;(createToken as jest.Mock).mockReturnValueOnce(stripeToken) - relay.commitMutation = commitMutationMock((_, { onCompleted }) => { - onCompleted!({}, errors) - return { dispose: jest.fn() } - }) as any - - const view = mountConfirmBidComponent(initialPropsForUnqualifiedUser) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - await fillOutFormAndSubmit(view) + mockFillAndSubmit() - const modal = await view.root.findByType(Modal) - const modalText = await modal.findAllByType(Text) - const modalButton = await modal.findByType(Button) + await screen.findByText( + "There was a problem processing your information. Check your payment details and try again." + ) - // eslint-disable-next-line testing-library/no-node-access - expect(modalText[1].props.children).toEqual([ - "There was a problem processing your information. Check your payment details and try again.", - ]) - modalButton.props.onPress() + // press the dismiss modal button + fireEvent.press(screen.getByText("Ok")) - // it dismisses the modal - expect(modal.props.visible).toEqual(false) + // error modal is dismissed + expect( + screen.queryByText( + "There was a problem processing your information. Check your payment details and try again." + ) + ).not.toBeOnTheScreen() }) - xit("shows the error screen with the default error message if the creditCardMutation error message is empty", async () => { - renderWithWrappers() - ;(createToken as jest.Mock).mockReturnValueOnce(stripeToken) - - relay.commitMutation = commitMutationMock((_, { onCompleted }) => { - onCompleted!(mockRequestResponses.creatingCreditCardEmptyError, null) - return { dispose: jest.fn() } - }) as any + it("shows the error screen with the default error message if the creditCardMutation error message is empty", async () => { + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { + onCompleted(mockRequestResponses.creatingCreditCardEmptyError, null) + }) + useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) - // UNSAFELY getting the component instance to set state for testing purposes only - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ billingAddress }) - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ creditCardToken: stripeToken }) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - // Check the checkbox and press the Bid button - fireEvent.press(screen.UNSAFE_getByType(Checkbox)) - fireEvent.press(screen.getByTestId("bid-button")) + mockFillAndSubmit() await screen.findByText( "There was a problem processing your information. Check your payment details and try again." @@ -937,44 +852,54 @@ describe("ConfirmBid", () => { ).not.toBeOnTheScreen() }) - xit("shows the generic error screen on a createCreditCard mutation network failure", async () => { - console.error = jest.fn() // Silences component logging. - ;(createToken as jest.Mock).mockReturnValueOnce(stripeToken) - relay.commitMutation = commitMutationMock((_, { onError }) => { - onError!(new TypeError("Network request failed")) - return { dispose: jest.fn() } - }) as any + it("shows the generic error screen on a createCreditCard mutation network failure", () => { + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onError }) => { + onError([new TypeError("Network request failed")]) + }) + useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) - const view = mountConfirmBidComponent(initialPropsForUnqualifiedUser) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - await fillOutFormAndSubmit(view) + mockFillAndSubmit() expect(nextStep?.component).toEqual(BidResult) expect(nextStep?.passProps).toEqual( expect.objectContaining({ bidderPositionResult: { - message_header: "An error occurred", - message_description_md: + messageHeader: "An error occurred", + messageDescriptionMD: "Your bid couldn’t be placed. Please\ncheck your internet connection\nand try again.", }, }) ) }) - xdescribe("After successful mutations", () => { + describe("After successful mutations", () => { + const completedUpdateUserPhoneNumberMutation = jest + .fn() + .mockImplementation(({ onCompleted }) => { + onCompleted(mockRequestResponses.updateMyUserProfile, null) + }) + + const completedCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { + onCompleted(mockRequestResponses.creatingCreditCardSuccess, null) + }) + const completedCreateBidderPositionMutation = jest + .fn() + .mockImplementation(({ onCompleted }) => { + onCompleted(mockRequestResponses.placingBid.bidAccepted, null) + }) + beforeEach(() => { - ;(createToken as jest.Mock).mockReturnValueOnce(stripeToken) - relay.commitMutation = jest - .fn() - .mockImplementationOnce((_, { onCompleted }) => - onCompleted(mockRequestResponses.updateMyUserProfile) - ) - .mockImplementationOnce((_, { onCompleted }) => - onCompleted(mockRequestResponses.creatingCreditCardSuccess) - ) - .mockImplementationOnce((_, { onCompleted }) => - onCompleted(mockRequestResponses.placingBid.bidAccepted) - ) + useUpdateUserPhoneNumberMock.mockReturnValue([ + completedUpdateUserPhoneNumberMutation, + false, + ]) + useCreateCreditCardMock.mockReturnValue([completedCreateCreditCardMutation, false]) + useCreateBidderPositionMock.mockReturnValue([completedCreateBidderPositionMutation, false]) }) it("commits two mutations, createCreditCard followed by createBidderPosition on a successful bid", async () => { @@ -982,43 +907,22 @@ describe("ConfirmBid", () => { .mockReturnValueOnce(Promise.resolve(mockRequestResponses.pollingForBid.pending)) .mockReturnValueOnce(Promise.resolve(mockRequestResponses.pollingForBid.highestBidder)) - renderWithWrappers() - - // UNSAFELY getting the component instance to set state for testing purposes only - screen.UNSAFE_getByType(ConfirmBid).instance.setState({ billingAddress }) - screen - .UNSAFE_getByType(ConfirmBid) - .instance.setState({ creditCardToken: stripeToken.token }) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) - // Check the checkbox and press the Bid button - fireEvent.press(screen.UNSAFE_getByType(Checkbox)) - fireEvent.press(screen.getByTestId("bid-button")) + mockFillAndSubmit() - await waitFor(() => expect(relay.commitMutation).toHaveBeenCalled()) - expect(relay.commitMutation).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ - variables: { - input: { - phone: "111 222 4444", - }, - }, - }) + expect(completedUpdateUserPhoneNumberMutation).toHaveBeenCalledWith( + expect.objectContaining({ variables: { input: { phone: "111 222 4444" } } }) ) - expect(relay.commitMutation).toHaveBeenCalledWith( - expect.any(Object), - expect.objectContaining({ - variables: { - input: { - token: "fake-token", - }, - }, - }) + expect(completedCreateCreditCardMutation).toHaveBeenCalledWith( + expect.objectContaining({ variables: { input: { token: "fake-token" } } }) ) - expect(relay.commitMutation).toHaveBeenCalledWith( - expect.any(Object), + expect(completedCreateBidderPositionMutation).toHaveBeenCalledWith( expect.objectContaining({ variables: { input: { @@ -1043,20 +947,23 @@ describe("ConfirmBid", () => { }) it("displays an error message on polling failure", async () => { - console.error = jest.fn() // Silences component logging. bidderPositionQueryMock.mockReturnValueOnce(Promise.reject({ message: "error" })) - const view = mountConfirmBidComponent(initialPropsForUnqualifiedUser) + renderWithRelay({ + Me: () => ({ hasQualifiedCreditCards: false }), + SaleArtwork: () => saleArtwork, + }) + + mockFillAndSubmit() - fillOutFormAndSubmit(view) await waitFor(() => !!nextStep) expect(nextStep?.component).toEqual(BidResult) expect(nextStep?.passProps).toEqual( expect.objectContaining({ bidderPositionResult: { - message_header: "An error occurred", - message_description_md: + messageHeader: "An error occurred", + messageDescriptionMD: "Your bid couldn’t be placed. Please\ncheck your internet connection\nand try again.", }, }) @@ -1067,15 +974,13 @@ describe("ConfirmBid", () => { describe("cascading end times", () => { it("sale endtime defaults to extendedBiddingEndtime", () => { - // renderWithWrappers() - renderWithRelay({ SaleArtwork: () => cascadingEndTimeSaleArtwork }, initialProps) + renderWithRelay({ SaleArtwork: () => cascadingEndTimeSaleArtwork }) expect(screen.getByText("00d 00h 00m 10s")).toBeOnTheScreen() }) it("shows the sale's end time if the sale does not have cascading end times", () => { - // renderWithWrappers() - renderWithRelay({ SaleArtwork: () => nonCascadeSaleArtwork }, initialProps) + renderWithRelay({ SaleArtwork: () => nonCascadeSaleArtwork }) expect(screen.getByText("00d 00h 00m 10s")).toBeOnTheScreen() }) @@ -1107,7 +1012,7 @@ describe("ConfirmBid", () => { lotLabel: "538", } - const saleArtwork: ConfirmBid_saleArtwork$data = { + const saleArtwork: CleanRelayFragment = { ...baseSaleArtwork, endAt: null, extendedBiddingEndAt: null, @@ -1116,11 +1021,9 @@ describe("ConfirmBid", () => { liveStartAt: "2018-05-09T20:22:42+00:00", cascadingEndTimeIntervalMinutes: null, }, - " $fragmentSpreads": null as any, // needs this to keep TS happy - " $fragmentType": null as any, // needs this to keep TS happy } - const nonCascadeSaleArtwork: ConfirmBid_saleArtwork$data = { + const nonCascadeSaleArtwork: CleanRelayFragment = { ...baseSaleArtwork, endAt: null, extendedBiddingEndAt: null, @@ -1130,11 +1033,9 @@ describe("ConfirmBid", () => { liveStartAt: null, cascadingEndTimeIntervalMinutes: null, }, - " $fragmentSpreads": null as any, // needs this to keep TS happy - " $fragmentType": null as any, // needs this to keep TS happy } - const cascadingEndTimeSaleArtwork: ConfirmBid_saleArtwork$data = { + const cascadingEndTimeSaleArtwork: CleanRelayFragment = { ...saleArtwork, endAt: "2018-05-13T20:22:42+00:00", extendedBiddingEndAt: new Date(Date.now() + 10000).toISOString(), @@ -1145,7 +1046,7 @@ describe("ConfirmBid", () => { }, } - const saleArtworkRegisteredForBidding: ConfirmBid_saleArtwork$data = { + const saleArtworkRegisteredForBidding: CleanRelayFragment = { ...saleArtwork, endAt: "2018-05-13T20:22:42+00:00", extendedBiddingEndAt: null, @@ -1298,31 +1199,4 @@ describe("ConfirmBid", () => { extra: null, }, } - - const initialProps: ConfirmBidProps = { - increments: [ - { - cents: 450000, - display: "$45,000", - }, - { - cents: 460000, - display: "$46,000", - }, - ], - selectedBidIndex: 0, - navigator: mockNavigator, - } as any - - const initialPropsForUnqualifiedUser = { - ...initialProps, - me: { - has_qualified_credit_cards: false, - }, - } as any - - const initialPropsForRegisteredUser = { - ...initialProps, - sale_artwork: saleArtworkRegisteredForBidding, - } as any }) diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index 95ced860f25..3f34f4cba35 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -138,6 +138,15 @@ export const ConfirmBid: React.FC = ({ updateUserPhoneNumber({ variables: { input: { phone: billingAddress?.phoneNumber } }, + onCompleted: (_, errors) => { + if (errors && errors.length) { + console.error("ConfirmBid.tsx: updateUserPhoneNumber", errors) + setErrorMessage( + "There was a problem processing your information. Check your payment details and try again." + ) + setErrorModalVisible(true) + } + }, onError: (errors) => { console.error("ConfirmBid.tsx: updateUserPhoneNumber", errors) }, @@ -147,11 +156,15 @@ export const ConfirmBid: React.FC = ({ variables: { input: { token: creditCardToken.id } }, onError: (errors) => { console.error("ConfirmBid.tsx: createCreditCard", errors) + navigateToBidScreen(undefined, errors) }, onCompleted: (results, error) => { const mutationError = results?.createCreditCard?.creditCardOrError?.mutationError - if (mutationError?.message) { - setErrorMessage(mutationError.message) + if (mutationError) { + setErrorMessage( + mutationError.detail || + "There was a problem processing your information. Check your payment details and try again." + ) setErrorModalVisible(true) console.error("ConfirmBid.tsx: createCreditCard", mutationError) } else if (error) { @@ -186,6 +199,7 @@ export const ConfirmBid: React.FC = ({ } }, onError: (errors) => { + console.error("ConfirmBid.tsx: createBidderPosition", errors) navigateToBidScreen(undefined, errors) }, }) diff --git a/src/app/utils/hooks/withSuspense.tsx b/src/app/utils/hooks/withSuspense.tsx index 50611183aec..7aa158db649 100644 --- a/src/app/utils/hooks/withSuspense.tsx +++ b/src/app/utils/hooks/withSuspense.tsx @@ -35,7 +35,7 @@ type WithSuspenseOptions = { } const DefaultLoadingFallback: React.FC = () => ( - + ) diff --git a/src/app/utils/mutations/useCreateCreditCard.ts b/src/app/utils/mutations/useCreateCreditCard.ts index 64e9ef8e81d..1ebb2d3ce5f 100644 --- a/src/app/utils/mutations/useCreateCreditCard.ts +++ b/src/app/utils/mutations/useCreateCreditCard.ts @@ -17,7 +17,9 @@ export const useCreateCreditCard = () => { } ... on CreditCardMutationFailure { mutationError { + type message + detail } } } From 6ba62ecc023f91dd80e454bbaeb672d6980c7e7b Mon Sep 17 00:00:00 2001 From: Sultan Date: Mon, 10 Feb 2025 12:52:59 +0100 Subject: [PATCH 08/26] fix BidResult tests --- .../Bidding/Screens/BidResult.tests.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/Components/Bidding/Screens/BidResult.tests.tsx b/src/app/Components/Bidding/Screens/BidResult.tests.tsx index 1b3872544a6..b65faaaf51e 100644 --- a/src/app/Components/Bidding/Screens/BidResult.tests.tsx +++ b/src/app/Components/Bidding/Screens/BidResult.tests.tsx @@ -1,5 +1,6 @@ import { fireEvent, screen } from "@testing-library/react-native" import { BidResult_saleArtwork$data } from "__generated__/BidResult_saleArtwork.graphql" +import { BidFlowContextProvider } from "app/Components/Bidding/Context/BidFlowContextProvider" import { BidderPositionResult } from "app/Components/Bidding/types" import { dismissModal, navigate } from "app/system/navigation/navigate" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" @@ -21,12 +22,14 @@ describe("BidResult component", () => { const { renderWithRelay } = setupTestWrapper({ Component: (props: any) => ( - + + + ), query: graphql` query BidResultTestsQuery @relay_test_operation { From 0db582f51c9f30295d38fdd7d1fde8cd1f8ade9b Mon Sep 17 00:00:00 2001 From: Sultan Date: Mon, 10 Feb 2025 21:13:17 +0100 Subject: [PATCH 09/26] refactor bidding navigation --- .../Bidding/Components/PaymentInfo.tsx | 18 ++-- .../Bidding/Components/PhoneInfo.tsx | 17 ++-- .../Components/Bidding/Screens/BidResult.tsx | 33 ++++--- .../Bidding/Screens/ConfirmBid/index.tsx | 59 +++++------- .../Bidding/Screens/CreditCardForm.tsx | 26 +++--- .../Bidding/Screens/PhoneNumberForm.tsx | 23 ++--- .../Bidding/Screens/Registration.tsx | 45 +++++----- .../Bidding/Screens/RegistrationResult.tsx | 29 +++--- .../Bidding/Screens/SelectMaxBid.tsx | 47 +++++----- src/app/Components/Containers/BidFlow.tsx | 14 ++- .../Containers/BiddingNavigator.tsx | 89 +++++++++++++++++++ .../Containers/RegistrationFlow.tsx | 10 +-- 12 files changed, 229 insertions(+), 181 deletions(-) create mode 100644 src/app/Components/Containers/BiddingNavigator.tsx diff --git a/src/app/Components/Bidding/Components/PaymentInfo.tsx b/src/app/Components/Bidding/Components/PaymentInfo.tsx index d8cc0cbcb30..3414ca77180 100644 --- a/src/app/Components/Bidding/Components/PaymentInfo.tsx +++ b/src/app/Components/Bidding/Components/PaymentInfo.tsx @@ -1,18 +1,17 @@ import { bullet } from "@artsy/palette-mobile" +import { NavigationProp } from "@react-navigation/native" import { Token } from "@stripe/stripe-react-native" import { Card } from "@stripe/stripe-react-native/lib/typescript/src/types/Token" import { FlexProps } from "app/Components/Bidding/Elements/Flex" -import { CreditCardForm } from "app/Components/Bidding/Screens/CreditCardForm" import { Address, PaymentCardTextFieldParams } from "app/Components/Bidding/types" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import React from "react" import { View } from "react-native" - import { BidInfoRow } from "./BidInfoRow" import { Divider } from "./Divider" interface PaymentInfoProps extends FlexProps { - navigator?: NavigatorIOS + navigator?: NavigationProp onCreditCardAdded: (t: Token.Result, a: Address) => void billingAddress?: Address | null creditCardFormParams?: PaymentCardTextFieldParams | null @@ -25,14 +24,9 @@ export class PaymentInfo extends React.Component { } presentCreditCardForm() { - this.props.navigator?.push({ - component: CreditCardForm, - title: "", - passProps: { - onSubmit: (token: Token.Result, address: Address) => this.onCreditCardAdded(token, address), - billingAddress: this.props.billingAddress, - navigator: this.props.navigator, - }, + this.props.navigator?.navigate("CreditCardForm", { + onSubmit: (token: Token.Result, address: Address) => this.onCreditCardAdded(token, address), + billingAddress: this.props.billingAddress, }) } diff --git a/src/app/Components/Bidding/Components/PhoneInfo.tsx b/src/app/Components/Bidding/Components/PhoneInfo.tsx index 2fb71b25b72..d0f7674df7a 100644 --- a/src/app/Components/Bidding/Components/PhoneInfo.tsx +++ b/src/app/Components/Bidding/Components/PhoneInfo.tsx @@ -1,27 +1,22 @@ +import { NavigationProp } from "@react-navigation/native" import { FlexProps } from "app/Components/Bidding/Elements/Flex" -import { PhoneNumberForm } from "app/Components/Bidding/Screens/PhoneNumberForm" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { View } from "react-native" import { BidInfoRow } from "./BidInfoRow" import { Divider } from "./Divider" interface PhoneInfoProps extends FlexProps { - navigator: NavigatorIOS + navigator: NavigationProp onPhoneAdded: (phoneNumber: string) => void phoneNumber?: string } export const PhoneInfo: React.FC = (props) => { const presentPhoneForm = (): void => { - props.navigator.push({ - component: PhoneNumberForm, - title: "", - passProps: { - onSubmit: (phone: string) => props.onPhoneAdded(phone), - billingAddress: props.phoneNumber, - navigator: props.navigator, - }, + props.navigator.navigate("PhoneNumberForm", { + onSubmit: (phone: string) => props.onPhoneAdded(phone), + phoneNumber: props.phoneNumber, }) } diff --git a/src/app/Components/Bidding/Screens/BidResult.tsx b/src/app/Components/Bidding/Screens/BidResult.tsx index c0f701bedd8..224aec5b4cc 100644 --- a/src/app/Components/Bidding/Screens/BidResult.tsx +++ b/src/app/Components/Bidding/Screens/BidResult.tsx @@ -1,13 +1,13 @@ import { Button, Flex, Text } from "@artsy/palette-mobile" -import { BidResult_saleArtwork$key } from "__generated__/BidResult_saleArtwork.graphql" +import { StackActions } from "@react-navigation/native" +import { NativeStackScreenProps } from "@react-navigation/native-stack" import { Timer } from "app/Components/Bidding/Components/Timer" import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" -import { BidderPositionResult } from "app/Components/Bidding/types" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { Markdown } from "app/Components/Markdown" import { NavigationHeader } from "app/Components/NavigationHeader" import { unsafe__getEnvironment } from "app/store/GlobalStore" import { dismissModal, navigate } from "app/system/navigation/navigate" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" import { useBackHandler } from "app/utils/hooks/useBackHandler" import React from "react" import { Image, ImageRequireSource } from "react-native" @@ -15,13 +15,7 @@ import { graphql, useFragment } from "react-relay" const SHOW_TIMER_STATUSES = ["WINNING", "OUTBID", "RESERVE_NOT_MET"] -interface BidResultProps { - saleArtwork: BidResult_saleArtwork$key - bidderPositionResult: BidderPositionResult - navigator: NavigatorIOS - refreshBidderInfo?: () => void - refreshSaleArtwork?: () => void -} +type BidResultProps = NativeStackScreenProps const POLLING_TIMEOUT_MESSAGES = { title: "Bid processing", @@ -38,13 +32,15 @@ const ICONS: Record = { } export const BidResult: React.FC = ({ - bidderPositionResult, - saleArtwork, - navigator, - refreshBidderInfo, - refreshSaleArtwork, + navigation, + route: { + params: { bidderPositionResult, saleArtwork, refreshBidderInfo, refreshSaleArtwork }, + }, }) => { const biddingEndAt = BidFlowContextStore.useStoreState((state) => state.biddingEndAt) + const setSelectedBidIndex = BidFlowContextStore.useStoreActions( + (actions) => actions.setSelectedBidIndex + ) const saleArtworkData = useFragment(bidResultFragment, saleArtwork) const { status, messageHeader, messageDescriptionMD } = bidderPositionResult @@ -71,7 +67,8 @@ export const BidResult: React.FC = ({ refreshSaleArtwork() } - navigator.popToTop() + setSelectedBidIndex(0) + navigation.dispatch(StackActions.popToTop()) } const onContinue = () => { @@ -104,7 +101,9 @@ export const BidResult: React.FC = ({ {status !== "WINNING" && ( - {status === "PENDING" ? POLLING_TIMEOUT_MESSAGES.description : messageDescriptionMD} + {status === "PENDING" + ? POLLING_TIMEOUT_MESSAGES.description + : messageDescriptionMD ?? ""} )} diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index 3f34f4cba35..975a614992f 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -1,10 +1,9 @@ import { Box, Button, Checkbox, LinkText, Text } from "@artsy/palette-mobile" +import { NativeStackScreenProps } from "@react-navigation/native-stack" import { BidderPositionQuery, BidderPositionQuery$data, } from "__generated__/BidderPositionQuery.graphql" -import { ConfirmBid_me$key } from "__generated__/ConfirmBid_me.graphql" -import { ConfirmBid_saleArtwork$key } from "__generated__/ConfirmBid_saleArtwork.graphql" import { useCreateBidderPositionMutation, useCreateBidderPositionMutation$data, @@ -15,16 +14,15 @@ import { PaymentInfo } from "app/Components/Bidding/Components/PaymentInfo" import { Timer } from "app/Components/Bidding/Components/Timer" import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" import { Flex } from "app/Components/Bidding/Elements/Flex" -import { BidResult } from "app/Components/Bidding/Screens/BidResult" import { bidderPositionQuery } from "app/Components/Bidding/Screens/ConfirmBid/BidderPositionQuery" import { PriceSummary } from "app/Components/Bidding/Screens/ConfirmBid/PriceSummary" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { Modal } from "app/Components/Modal" import { NavigationHeader } from "app/Components/NavigationHeader" import { LegacyNativeModules } from "app/NativeModules/LegacyNativeModules" import { partnerName } from "app/Scenes/Artwork/Components/ArtworkExtraLinks/partnerName" import { navigate } from "app/system/navigation/navigate" import { AuctionWebsocketContextProvider } from "app/utils/Websockets/auctions/AuctionSocketContext" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" import { useCreateBidderPosition } from "app/utils/mutations/useCreateBidderPosition" import { useCreateCreditCard } from "app/utils/mutations/useCreateCreditCard" import { useUpdateUserPhoneNumber } from "app/utils/mutations/useUpdateUserPhoneNumber" @@ -35,7 +33,7 @@ import { graphql, useRefetchableFragment } from "react-relay" import { useTracking } from "react-tracking" import { PayloadError } from "relay-runtime" -type BidderPositionResult = +export type BidderPositionResult = | NonNullable["result"] | NonNullable["bidderPosition"] @@ -45,20 +43,17 @@ const resultForNetworkError = { messageHeader: "An error occurred", messageDescriptionMD: "Your bid couldn’t be placed. Please\ncheck your internet connection\nand try again.", + position: null, + status: "ERROR", } -export interface ConfirmBidProps { - saleArtwork: ConfirmBid_saleArtwork$key - me: ConfirmBid_me$key - navigator?: NavigatorIOS - refreshSaleArtwork?: () => void -} +type ConfirmBidProps = NativeStackScreenProps export const ConfirmBid: React.FC = ({ - me, - saleArtwork, - refreshSaleArtwork, - navigator, + navigation, + route: { + params: { me, saleArtwork, refreshSaleArtwork }, + }, }) => { const pollCount = useRef(0) const { trackEvent } = useTracking() @@ -252,17 +247,12 @@ export const ConfirmBid: React.FC = ({ bidderPositionResult?: BidderPositionResult, error?: Error | ReadonlyArray | null ) => { - if (error) { + if (error || !bidderPositionResult) { console.error("ConfirmBid.tsx: navigateToBidScreen", error) - navigator?.push({ - component: BidResult, - title: "", - passProps: { - saleArtwork: saleArtworkData, - bidderPositionResult: resultForNetworkError, - biddingEndAt, - }, + navigation.navigate("BidResult", { + saleArtwork: saleArtworkData, + bidderPositionResult: resultForNetworkError, }) } else { LegacyNativeModules.ARNotificationsManager.postNotificationName( @@ -279,16 +269,11 @@ export const ConfirmBid: React.FC = ({ } ) - navigator?.push({ - component: BidResult, - title: "", - passProps: { - saleArtwork: saleArtworkData, - bidderPositionResult, - refreshBidderInfo, - refreshSaleArtwork, - biddingEndAt, - }, + navigation.navigate("BidResult", { + saleArtwork: saleArtworkData, + bidderPositionResult, + refreshBidderInfo, + refreshSaleArtwork, }) } } @@ -305,7 +290,7 @@ export const ConfirmBid: React.FC = ({ }, }} > - navigator?.pop()}> + navigation.goBack()}> Confirm your bid @@ -362,12 +347,12 @@ export const ConfirmBid: React.FC = ({ (mutationInProgress ? null : navigator?.pop())} + onPress={() => (mutationInProgress ? null : navigation.goBack())} /> {requiresPaymentInformation ? ( { setCreditCardToken(token) setBillingAddress(address) diff --git a/src/app/Components/Bidding/Screens/CreditCardForm.tsx b/src/app/Components/Bidding/Screens/CreditCardForm.tsx index 01e4eb036f9..d5640821e28 100644 --- a/src/app/Components/Bidding/Screens/CreditCardForm.tsx +++ b/src/app/Components/Bidding/Screens/CreditCardForm.tsx @@ -1,5 +1,6 @@ import { Box, Button, Flex, Input, Spacer, Text, useSpace } from "@artsy/palette-mobile" -import { createToken, Token } from "@stripe/stripe-react-native" +import { NativeStackScreenProps } from "@react-navigation/native-stack" +import { createToken } from "@stripe/stripe-react-native" import { CreateCardTokenParams } from "@stripe/stripe-react-native/lib/typescript/src/types/Token" import { Details } from "@stripe/stripe-react-native/lib/typescript/src/types/components/CardFieldInput" import { @@ -9,26 +10,23 @@ import { import { findCountryNameByCountryCode } from "app/Components/Bidding/Utils/findCountryNameByCountryCode" import { creditCardFormValidationSchema } from "app/Components/Bidding/Validators/creditCardFormFieldsValidationSchema" import { Address } from "app/Components/Bidding/types" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { CountrySelect } from "app/Components/CountrySelect" import { CreditCardField } from "app/Components/CreditCardField/CreditCardField" import { NavigationHeader } from "app/Components/NavigationHeader" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" import { useFormik } from "formik" import { memo, useCallback, useRef } from "react" import { KeyboardAvoidingView, ScrollView } from "react-native" -interface CreditCardFormProps { - navigator: NavigatorIOS - billingAddress?: Address | null - onSubmit: (t: Token.Result, a: Address) => void -} +type CreditCardFormProps = NativeStackScreenProps const MemoizedInput = memo(Input) export const CreditCardForm: React.FC = ({ - onSubmit, - billingAddress, - navigator, + navigation, + route: { + params: { onSubmit, billingAddress }, + }, }) => { const space = useSpace() const initialValues: CreditCardFormValues = { @@ -47,13 +45,13 @@ export const CreditCardForm: React.FC = ({ } onSubmit(token.token, buildBillingAddress(values)) - navigator.pop() + navigation.goBack() } catch (error) { setErrors({ creditCard: { valid: "There was an error. Please try again." } }) console.error("CreditCardForm.tsx", error) } }, - [onSubmit, navigator, buildTokenParams, buildBillingAddress] + [onSubmit, buildTokenParams, buildBillingAddress] ) const { @@ -107,7 +105,9 @@ export const CreditCardForm: React.FC = ({ return ( - navigator.pop()}>Add Credit Card + navigation.goBack()}> + Add Credit Card + void - navigator: NavigatorIOS - phoneNumber?: string -} + +type PhoneNumberFormProps = NativeStackScreenProps export const PhoneNumberForm: React.FC = (props) => { - const { onSubmit, navigator, phoneNumber } = props + const { + navigation, + route: { + params: { phoneNumber, onSubmit }, + }, + } = props const phoneRef = useRef(null) @@ -28,7 +31,7 @@ export const PhoneNumberForm: React.FC = (props) => { const handleAddPhoneNumberClick = (): void => { onSubmit(enteredPhone) - navigator?.pop() + navigation.goBack() track({ action_type: Schema.ActionTypes.Success, action_name: Schema.ActionNames.BidFlowSavePhoneNumber, @@ -58,7 +61,7 @@ export const PhoneNumberForm: React.FC = (props) => { }} > - navigator?.pop()}> + navigation.goBack()}> Add phone number { sale: Registration_sale$data me: Registration_me$data relay: RelayProp - navigator?: NavigatorIOS } interface RegistrationState { @@ -344,16 +346,12 @@ export class Registration extends React.Component null } as any) : this.props.navigator} + navigator={isLoading ? ({ push: () => null } as any) : this.props.navigation} onCreditCardAdded={this.onCreditCardAdded.bind(this)} billingAddress={this.state.billingAddress} creditCardFormParams={this.state.creditCardFormParams} @@ -394,7 +392,7 @@ export class Registration extends React.Component null } as any) : this.props.navigator} + navigator={isLoading ? ({ push: () => null } as any) : this.props.navigation} onPhoneAdded={this.onPhoneAdded.bind(this)} phoneNumber={this.state.phoneNumber} /> @@ -521,10 +519,11 @@ const RegistrationContainer = createFragmentContainer(Registration, { `, }) -export const RegistrationQueryRenderer: React.FC<{ saleID: string; navigator: NavigatorIOS }> = ({ - saleID, - navigator, -}) => { +export const RegistrationQueryRenderer: React.FC< + NativeStackScreenProps +> = (screenProps) => { + const { saleID } = screenProps.route.params + return ( ( - + ))} /> diff --git a/src/app/Components/Bidding/Screens/RegistrationResult.tsx b/src/app/Components/Bidding/Screens/RegistrationResult.tsx index cb4cb9bede2..ad7d4dc933c 100644 --- a/src/app/Components/Bidding/Screens/RegistrationResult.tsx +++ b/src/app/Components/Bidding/Screens/RegistrationResult.tsx @@ -1,9 +1,11 @@ import { Text, Button } from "@artsy/palette-mobile" +import { NativeStackScreenProps } from "@react-navigation/native-stack" import { Icon20 } from "app/Components/Bidding/Components/Icon" import { Title } from "app/Components/Bidding/Components/Title" import { Flex } from "app/Components/Bidding/Elements/Flex" -import { NavigationHeader } from "app/Components/NavigationHeader" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { Markdown } from "app/Components/Markdown" +import { NavigationHeader } from "app/Components/NavigationHeader" import { dismissModal } from "app/system/navigation/navigate" import { defaultRules } from "app/utils/renderMarkdown" import { Schema, screenTrack } from "app/utils/track" @@ -11,10 +13,10 @@ import React from "react" import { BackHandler, NativeEventSubscription, View } from "react-native" import { blockRegex } from "simple-markdown" -interface RegistrationResultProps { - status: RegistrationStatus - needsIdentityVerification?: boolean -} +type RegistrationResultProps = NativeStackScreenProps< + BiddingNavigationStackParams, + "RegistrationResult" +> export enum RegistrationStatus { RegistrationStatusComplete = "RegistrationStatusComplete", @@ -95,13 +97,10 @@ const resultEnumToPageName = (result: RegistrationStatus) => { return pageName } -@screenTrack( - (props: RegistrationResultProps) => - ({ - context_screen: resultEnumToPageName(props.status), - context_screen_owner_type: null, - }) as any /* STRICTNESS_MIGRATION */ -) +@screenTrack((props: RegistrationResultProps) => ({ + context_screen: resultEnumToPageName(props.route.params.status), + context_screen_owner_type: null, +})) export class RegistrationResult extends React.Component { backButtonListener?: NativeEventSubscription = undefined @@ -118,8 +117,8 @@ export class RegistrationResult extends React.Component handleBackButton = () => { if ( - this.props.status === RegistrationStatus.RegistrationStatusComplete || - this.props.status === RegistrationStatus.RegistrationStatusPending + this.props.route.params.status === RegistrationStatus.RegistrationStatusComplete || + this.props.route.params.status === RegistrationStatus.RegistrationStatusPending ) { dismissModal() return true @@ -129,7 +128,7 @@ export class RegistrationResult extends React.Component } render() { - const { status, needsIdentityVerification } = this.props + const { status, needsIdentityVerification } = this.props.route.params let title: string let msg: string diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx index 98602cd97a6..a1d2cc92537 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx @@ -1,31 +1,32 @@ import { Button, Flex, useScreenDimensions } from "@artsy/palette-mobile" +import { NativeStackScreenProps } from "@react-navigation/native-stack" import { SelectMaxBidQuery } from "__generated__/SelectMaxBidQuery.graphql" import { SelectMaxBid_me$key } from "__generated__/SelectMaxBid_me.graphql" import { SelectMaxBid_saleArtwork$key } from "__generated__/SelectMaxBid_saleArtwork.graphql" import { BidFlowContextStore } from "app/Components/Bidding/Context/BidFlowContextProvider" +import { BiddingNavigationStackParams } from "app/Components/Containers/BiddingNavigator" import { NavigationHeader } from "app/Components/NavigationHeader" import { Select } from "app/Components/Select" import { dismissModal } from "app/system/navigation/navigate" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" + import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" import { compact } from "lodash" import React, { useEffect, useMemo } from "react" import { graphql, useFragment, useLazyLoadQuery, useRefetchableFragment } from "react-relay" -import { ConfirmBid } from "./ConfirmBid" -interface SelectMaxBidProps { +interface SelectMaxBidProps + extends NativeStackScreenProps { saleArtwork: SelectMaxBid_saleArtwork$key me: SelectMaxBid_me$key - navigator: NavigatorIOS } -export const SelectMaxBid: React.FC = ({ navigator, me, saleArtwork }) => { - const selectedBidIndex = BidFlowContextStore.useStoreState((state) => state.selectedBidIndex) - const bids = BidFlowContextStore.useStoreState((state) => state.saleArtworkIncrements) +export const SelectMaxBid: React.FC = ({ me, saleArtwork, navigation }) => { + const setBids = BidFlowContextStore.useStoreActions((actions) => actions.setSaleArtworkIncrements) const setSelectedBidIndex = BidFlowContextStore.useStoreActions( (actions) => actions.setSelectedBidIndex ) - const setBids = BidFlowContextStore.useStoreActions((actions) => actions.setSaleArtworkIncrements) + const bids = BidFlowContextStore.useStoreState((state) => state.saleArtworkIncrements) + const selectedBidIndex = BidFlowContextStore.useStoreState((state) => state.selectedBidIndex) const { height } = useScreenDimensions() @@ -33,6 +34,7 @@ export const SelectMaxBid: React.FC = ({ navigator, me, saleA selectMaxBidSaleArtworkFragment, saleArtwork ) + const meData = useFragment(selectMaxBidMeFragment, me) const handleRefresh = () => { @@ -46,13 +48,10 @@ export const SelectMaxBid: React.FC = ({ navigator, me, saleA }, [saleArtworkData]) const handleNext = () => { - navigator.push({ - component: ConfirmBid, - passProps: { - me: meData, - saleArtwork: saleArtworkData, - refreshSaleArtwork: handleRefresh, - }, + navigation.navigate("ConfirmBid", { + me: meData, + saleArtwork: saleArtworkData, + refreshSaleArtwork: handleRefresh, }) } @@ -80,17 +79,13 @@ export const SelectMaxBid: React.FC = ({ navigator, me, saleA ) } -interface SelectMaxBidQRProps { - artworkID: string - saleID: string - navigator: NavigatorIOS -} - -export const SelectMaxBidQueryRenderer = withSuspense({ - Component: (props) => { +export const SelectMaxBidQueryRenderer = withSuspense< + NativeStackScreenProps +>({ + Component: (screenProps) => { const initialData = useLazyLoadQuery(selectMaxBidQuery, { - artworkID: props.artworkID, - saleID: props.saleID, + artworkID: screenProps.route.params.artworkID, + saleID: screenProps.route.params.saleID, }) if (!initialData || !initialData.artwork?.saleArtwork || !initialData.me) { @@ -110,7 +105,7 @@ export const SelectMaxBidQueryRenderer = withSuspense({ ) diff --git a/src/app/Components/Containers/BidFlow.tsx b/src/app/Components/Containers/BidFlow.tsx index 70057fb8f94..37a0dfdf3ec 100644 --- a/src/app/Components/Containers/BidFlow.tsx +++ b/src/app/Components/Containers/BidFlow.tsx @@ -1,21 +1,19 @@ import { Screen } from "@artsy/palette-mobile" import { BidFlowContextProvider } from "app/Components/Bidding/Context/BidFlowContextProvider" import { TimeOffsetProvider } from "app/Components/Bidding/Context/TimeOffsetProvider" -import { SelectMaxBidQueryRenderer } from "app/Components/Bidding/Screens/SelectMaxBid" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" +import { + BiddingNavigationStackParams, + BiddingNavigator, +} from "app/Components/Containers/BiddingNavigator" import { SafeAreaView } from "react-native-safe-area-context" -export const BidFlow: React.FC< - Omit, "navigator"> -> = (props) => { +export const BidFlow: React.FC = (props) => { return ( - + diff --git a/src/app/Components/Containers/BiddingNavigator.tsx b/src/app/Components/Containers/BiddingNavigator.tsx new file mode 100644 index 00000000000..50575ee62c2 --- /dev/null +++ b/src/app/Components/Containers/BiddingNavigator.tsx @@ -0,0 +1,89 @@ +import { useColor } from "@artsy/palette-mobile" +import { NavigationContainer } from "@react-navigation/native" +import { createNativeStackNavigator } from "@react-navigation/native-stack" +import { Token } from "@stripe/stripe-react-native" +import { BidResult_saleArtwork$key } from "__generated__/BidResult_saleArtwork.graphql" +import { ConfirmBid_me$key } from "__generated__/ConfirmBid_me.graphql" +import { ConfirmBid_saleArtwork$key } from "__generated__/ConfirmBid_saleArtwork.graphql" +import { BidResult } from "app/Components/Bidding/Screens/BidResult" +import { BidderPositionResult, ConfirmBid } from "app/Components/Bidding/Screens/ConfirmBid" +import { CreditCardForm } from "app/Components/Bidding/Screens/CreditCardForm" +import { PhoneNumberForm } from "app/Components/Bidding/Screens/PhoneNumberForm" +import { RegistrationQueryRenderer } from "app/Components/Bidding/Screens/Registration" +import { + RegistrationResult, + RegistrationStatus, +} from "app/Components/Bidding/Screens/RegistrationResult" +import { SelectMaxBidQueryRenderer } from "app/Components/Bidding/Screens/SelectMaxBid" +import { Address } from "app/Components/Bidding/types" + +export type BiddingNavigationStackParams = { + RegisterToBid: { saleID: string } + SelectMaxBid: { artworkID: string; saleID: string } + PhoneNumberForm: { onSubmit: (phoneNumber: string) => void; phoneNumber?: string } + CreditCardForm: { + onSubmit: (t: Token.Result, a: Address) => void + billingAddress?: Address | null + } + ConfirmBid: { + saleArtwork: ConfirmBid_saleArtwork$key + me: ConfirmBid_me$key + refreshSaleArtwork?: () => void + } + BidResult: { + saleArtwork: BidResult_saleArtwork$key + bidderPositionResult: NonNullable + refreshBidderInfo?: () => void + refreshSaleArtwork?: () => void + } + RegistrationResult: { + status: RegistrationStatus + needsIdentityVerification?: boolean + } +} + +const BiddingNavigationStack = createNativeStackNavigator() + +type BiddingNavigatorProps = + | { + initialRouteName: "RegisterToBid" + saleID: string + } + | { + initialRouteName: "SelectMaxBid" + artworkID: string + saleID: string + } + +export const BiddingNavigator: React.FC = (props) => { + const color = useColor() + + return ( + + + + + + + + + + + + ) +} diff --git a/src/app/Components/Containers/RegistrationFlow.tsx b/src/app/Components/Containers/RegistrationFlow.tsx index 3c44332c3d3..c0bcbc623f3 100644 --- a/src/app/Components/Containers/RegistrationFlow.tsx +++ b/src/app/Components/Containers/RegistrationFlow.tsx @@ -1,18 +1,12 @@ import { Screen } from "@artsy/palette-mobile" import { TimeOffsetProvider } from "app/Components/Bidding/Context/TimeOffsetProvider" -import { RegistrationQueryRenderer } from "app/Components/Bidding/Screens/Registration" -import NavigatorIOS from "app/utils/__legacy_do_not_use__navigator-ios-shim" +import { BiddingNavigator } from "app/Components/Containers/BiddingNavigator" export const RegistrationFlow: React.FC<{ saleID: string }> = (props) => { return ( - + ) From e79048975bf2946b93985b10e7f1a8078b43b32e Mon Sep 17 00:00:00 2001 From: Sultan Date: Mon, 10 Feb 2025 21:13:41 +0100 Subject: [PATCH 10/26] fix issue with Select options not rendering --- src/app/Components/Select/Components/SelectModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/Components/Select/Components/SelectModal.tsx b/src/app/Components/Select/Components/SelectModal.tsx index 82c148adebf..ec02d24d104 100644 --- a/src/app/Components/Select/Components/SelectModal.tsx +++ b/src/app/Components/Select/Components/SelectModal.tsx @@ -80,7 +80,7 @@ export const SelectModal: React.FC<{ const autocompleteResults = useMemo(() => { return searchTerm && autocomplete ? autocomplete.getSuggestions(searchTerm) : options - }, [autocomplete, searchTerm]) + }, [autocomplete, searchTerm, options]) const flatListRef = useRef(null) const flatListHeight = useRef(null) @@ -149,7 +149,9 @@ export const SelectModal: React.FC<{ )} + + Date: Mon, 10 Feb 2025 21:58:03 +0100 Subject: [PATCH 11/26] refactor and fix select max bid tests --- .../Bidding/Screens/SelectMaxBid.tests.tsx | 65 +++++++++++++++---- .../Bidding/Screens/SelectMaxBid.tsx | 1 + 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx index eeea44cc90d..874acb55be9 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tests.tsx @@ -1,13 +1,23 @@ -import { screen } from "@testing-library/react-native" +import { fireEvent, screen } from "@testing-library/react-native" import { SelectMaxBidTestsQuery } from "__generated__/SelectMaxBidTestsQuery.graphql" +import { BidFlowContextProvider } from "app/Components/Bidding/Context/BidFlowContextProvider" import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" import { graphql } from "react-relay" import { SelectMaxBid } from "./SelectMaxBid" describe("SelectMaxBid", () => { + const mockNavigator = { navigate: jest.fn() } + const { renderWithRelay } = setupTestWrapper({ Component: ({ me, saleArtwork }) => ( - + + + ), query: graphql` query SelectMaxBidTestsQuery @relay_test_operation { @@ -21,17 +31,50 @@ describe("SelectMaxBid", () => { `, }) - it("renders without throwing an error", () => { - renderWithRelay() + it("can update the max bid", async () => { + renderWithRelay({ + SaleArtwork: () => ({ + increments: [ + { display: "£100", cents: 10000 }, + { display: "£200", cents: 20000 }, + { display: "£300", cents: 30000 }, + ], + }), + }) + + expect(screen.getByText("£100")).toBeOnTheScreen() + + expect(screen.queryByTestId("modal-max-bid")).not.toBeVisible() + + // open the modal + fireEvent.press(screen.getByTestId("max-bid")) + + await screen.findByTestId("modal-max-bid") + + // expect(screen.getByText("£100")).toBeOnTheScreen() + expect(screen.getByText("£200")).toBeOnTheScreen() + expect(screen.getByText("£300")).toBeOnTheScreen() + + // select a new max bid + fireEvent.press(screen.getByText("£200")) + + expect(screen.queryByTestId("modal-max-bid")).not.toBeVisible() - expect(screen.queryByTestId("spinner")).toBeFalsy() + expect(screen.queryByText("£100")).not.toBeOnTheScreen() + expect(screen.getByText("£200")).toBeOnTheScreen() - // shows a spinner while fetching new bid increments - screen.UNSAFE_getByType(SelectMaxBid).instance._test_refreshSaleArtwork(true) // hacky way to call this, but its an old component that needs refactoring - expect(screen.getByTestId("spinner")).toBeTruthy() + fireEvent.press(screen.getByText("Next")) - // removes the spinner once the refetch is complete - screen.UNSAFE_getByType(SelectMaxBid).instance._test_refreshSaleArtwork(false) // hacky way to call this, but its an old component that needs refactoring - expect(screen.queryByTestId("spinner")).toBeFalsy() + expect(mockNavigator.navigate).toHaveBeenCalledWith("ConfirmBid", { + refreshSaleArtwork: expect.any(Function), + me: expect.any(Object), + saleArtwork: expect.objectContaining({ + increments: [ + { cents: 10000, display: "£100" }, + { cents: 20000, display: "£200" }, + { cents: 30000, display: "£300" }, + ], + }), + }) }) }) diff --git a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx index a1d2cc92537..6220da0031a 100644 --- a/src/app/Components/Bidding/Screens/SelectMaxBid.tsx +++ b/src/app/Components/Bidding/Screens/SelectMaxBid.tsx @@ -64,6 +64,7 @@ export const SelectMaxBid: React.FC = ({ me, saleArtwork, nav (null) return ( - + navigation.goBack()}> Add Credit Card From ef8067a819eb01bda265cbe3f5bbfdc84403cc9d Mon Sep 17 00:00:00 2001 From: Sultan Date: Fri, 14 Feb 2025 14:50:32 +0100 Subject: [PATCH 25/26] promisify useCreateCreditCard hook --- .../Bidding/Screens/ConfirmBid.tests.tsx | 8 ++++-- .../Bidding/Screens/ConfirmBid/index.tsx | 25 +++++++++--------- src/app/utils/hooks/usePromisedMutation.ts | 26 +++++++++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 src/app/utils/hooks/usePromisedMutation.ts diff --git a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx index 99f9f153778..5b5d9fc1099 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx @@ -845,9 +845,9 @@ describe("ConfirmBid", () => { ).not.toBeOnTheScreen() }) - it("shows the generic error screen on a createCreditCard mutation network failure", () => { + it("shows the generic error screen on a createCreditCard mutation network failure", async () => { const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onError }) => { - onError([new TypeError("Network request failed")]) + onError(new TypeError("Network request failed")) }) useCreateCreditCardMock.mockReturnValue([erroredCreateCreditCardMutation, false]) @@ -858,6 +858,8 @@ describe("ConfirmBid", () => { mockFillAndSubmit() + await waitFor(() => expect(mockNavigator.navigate).toHaveBeenCalled()) + expect(mockNavigator.navigate).toHaveBeenCalledWith( "BidResult", expect.objectContaining({ @@ -917,6 +919,8 @@ describe("ConfirmBid", () => { expect.objectContaining({ variables: { input: { token: "fake-token" } } }) ) + await waitFor(() => expect(completedCreateBidderPositionMutation).toHaveBeenCalled()) + expect(completedCreateBidderPositionMutation).toHaveBeenCalledWith( expect.objectContaining({ variables: { diff --git a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx index 6776dc5de64..7c9fddc9472 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid/index.tsx @@ -24,6 +24,7 @@ import { BiddingNavigationStackParams } from "app/Navigation/AuthenticatedRoutes import { partnerName } from "app/Scenes/Artwork/Components/ArtworkExtraLinks/partnerName" import { navigate } from "app/system/navigation/navigate" import { AuctionWebsocketContextProvider } from "app/utils/Websockets/auctions/AuctionSocketContext" +import { usePromisedMutation } from "app/utils/hooks/usePromisedMutation" import { useCreateBidderPosition } from "app/utils/mutations/useCreateBidderPosition" import { useCreateCreditCard } from "app/utils/mutations/useCreateCreditCard" import { useUpdateUserPhoneNumber } from "app/utils/mutations/useUpdateUserPhoneNumber" @@ -85,7 +86,7 @@ export const ConfirmBid: React.FC = ({ const [conditionsOfSaleChecked, setConditionsOfSaleChecked] = useState(false) const [updateUserPhoneNumber, updatingPhoneNumber] = useUpdateUserPhoneNumber() - const [createCreditCard, creatingCreditCard] = useCreateCreditCard() + const [createCreditCard, creatingCreditCard] = usePromisedMutation(useCreateCreditCard) const [createBidderPosition, creatingBidderPosition] = useCreateBidderPosition() const requiresCheckbox = !sale?.bidder @@ -152,14 +153,8 @@ export const ConfirmBid: React.FC = ({ }, }) - createCreditCard({ - variables: { input: { token: creditCardToken.id } }, - onError: (errors) => { - captureException(errors, { tags: { source: "ConfirmBid.tsx: createCreditCard" } }) - console.error("ConfirmBid.tsx: createCreditCard", errors) - navigateToBidScreen(undefined, errors) - }, - onCompleted: (results, error) => { + await createCreditCard({ variables: { input: { token: creditCardToken.id } } }) + .then((results) => { const mutationError = results?.createCreditCard?.creditCardOrError?.mutationError if (mutationError) { captureMessage( @@ -172,7 +167,10 @@ export const ConfirmBid: React.FC = ({ ) setErrorModalVisible(true) console.error("ConfirmBid.tsx: createCreditCard", mutationError) - } else if (error) { + } + }) + .catch((error) => { + if (error?.length) { captureMessage( `ConfirmBid.tsx: #createCreditCard error ${JSON.stringify(error)}`, "error" @@ -182,9 +180,12 @@ export const ConfirmBid: React.FC = ({ "There was a problem processing your information. Check your payment details and try again." ) setErrorModalVisible(true) + } else { + captureMessage(`ConfirmBid.tsx: #createCreditCard ${JSON.stringify(error)}`, "error") + console.error("ConfirmBid.tsx: createCreditCard", error) + navigateToBidScreen(undefined, error) } - }, - }) + }) } if (selectedBid?.cents == null) { diff --git a/src/app/utils/hooks/usePromisedMutation.ts b/src/app/utils/hooks/usePromisedMutation.ts new file mode 100644 index 00000000000..dd60d5ef342 --- /dev/null +++ b/src/app/utils/hooks/usePromisedMutation.ts @@ -0,0 +1,26 @@ +import { Disposable, UseMutationConfig } from "react-relay" +import { MutationParameters } from "relay-runtime" + +export function usePromisedMutation( + useMutation: () => [(config: UseMutationConfig) => Disposable, boolean] +): [(args: UseMutationConfig) => Promise, boolean] { + const [commit, isInFlight] = useMutation() + + return [ + async (args: UseMutationConfig) => { + return await new Promise((resolve, reject) => { + commit({ + ...args, + onCompleted: (response, errors) => { + if (errors) { + reject(errors) + } + resolve(response) + }, + onError: (error) => reject(error), + }) + }) + }, + isInFlight, + ] +} From ab9ef836ea5deaf9e7b847b185776fb5034a5248 Mon Sep 17 00:00:00 2001 From: Sultan Date: Fri, 14 Feb 2025 15:32:32 +0100 Subject: [PATCH 26/26] add sentry mocks in ConfirmBid tests --- .../Bidding/Screens/ConfirmBid.tests.tsx | 48 +++++++++++++++++++ src/setupJest.tsx | 1 + 2 files changed, 49 insertions(+) diff --git a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx index 5b5d9fc1099..8ed00fa0a7f 100644 --- a/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx +++ b/src/app/Components/Bidding/Screens/ConfirmBid.tests.tsx @@ -1,4 +1,5 @@ import { Button, Checkbox } from "@artsy/palette-mobile" +import Sentry from "@sentry/react-native" import { fireEvent, screen, @@ -358,6 +359,7 @@ describe("ConfirmBid", () => { }) it("displays an error message on a network failure", () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") const erroredCreateBidderPositionMutation = jest .fn() .mockImplementation(({ onError }) => { @@ -374,6 +376,10 @@ describe("ConfirmBid", () => { fireEvent.press(screen.getByTestId("bid-button")) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("#createBidderPosition"), + "error" + ) expect(mockNavigator.navigate).toHaveBeenCalledWith( "BidResult", expect.objectContaining({ @@ -389,6 +395,7 @@ describe("ConfirmBid", () => { }) it("displays an error message on a createBidderPosition mutation failure", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") const error = { message: 'GraphQL Timeout Error: Mutation.createBidderPosition has timed out after waiting for 5000ms"}', @@ -409,6 +416,11 @@ describe("ConfirmBid", () => { fireEvent.press(screen.getByTestId("disclaimer-checkbox")) fireEvent.press(screen.getByTestId("bid-button")) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("GraphQL Timeout Error"), + "error" + ) + await waitFor(() => expect(mockNavigator.navigate).toHaveBeenCalled()) expect(mockNavigator.navigate).toHaveBeenCalledWith( @@ -738,6 +750,7 @@ describe("ConfirmBid", () => { }) it("shows the error screen when stripe's API returns an error", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { onCompleted({}, [new Error("Stripe API error")]) }) @@ -755,6 +768,11 @@ describe("ConfirmBid", () => { "There was a problem processing your information. Check your payment details and try again." ) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("#createCreditCard error"), + "error" + ) + // press the dismiss modal button fireEvent.press(screen.getByText("Ok")) @@ -767,6 +785,7 @@ describe("ConfirmBid", () => { }) it("shows the error screen with the correct error message on a createCreditCard mutation failure", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { onCompleted(mockRequestResponses.creatingCreditCardError, null) }) @@ -781,6 +800,11 @@ describe("ConfirmBid", () => { await screen.findByText("Your card's security code is incorrect.") + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("Your card's security code is incorrect"), + "error" + ) + // press the dismiss modal button fireEvent.press(screen.getByText("Ok")) @@ -789,6 +813,8 @@ describe("ConfirmBid", () => { }) it("shows the error screen with the default error message if there are unhandled errors from the createCreditCard mutation", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") + const errors = [{ message: "malformed error" }] const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { onCompleted({}, errors) @@ -806,6 +832,11 @@ describe("ConfirmBid", () => { "There was a problem processing your information. Check your payment details and try again." ) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("malformed error"), + "error" + ) + // press the dismiss modal button fireEvent.press(screen.getByText("Ok")) @@ -818,6 +849,8 @@ describe("ConfirmBid", () => { }) it("shows the error screen with the default error message if the creditCardMutation error message is empty", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") + const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onCompleted }) => { onCompleted(mockRequestResponses.creatingCreditCardEmptyError, null) }) @@ -834,6 +867,11 @@ describe("ConfirmBid", () => { "There was a problem processing your information. Check your payment details and try again." ) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("Payment information could not be processed"), + "error" + ) + // press the dismiss modal button fireEvent.press(screen.getByText("Ok")) @@ -846,6 +884,7 @@ describe("ConfirmBid", () => { }) it("shows the generic error screen on a createCreditCard mutation network failure", async () => { + const captureMessageSpy = jest.spyOn(Sentry, "captureMessage") const erroredCreateCreditCardMutation = jest.fn().mockImplementation(({ onError }) => { onError(new TypeError("Network request failed")) }) @@ -860,6 +899,10 @@ describe("ConfirmBid", () => { await waitFor(() => expect(mockNavigator.navigate).toHaveBeenCalled()) + expect(captureMessageSpy).toHaveBeenCalledWith( + expect.stringContaining("#createCreditCard"), + "error" + ) expect(mockNavigator.navigate).toHaveBeenCalledWith( "BidResult", expect.objectContaining({ @@ -946,6 +989,7 @@ describe("ConfirmBid", () => { }) it("displays an error message on polling failure", async () => { + const captureExceptionSpy = jest.spyOn(Sentry, "captureException") bidderPositionQueryMock.mockReturnValueOnce(Promise.reject({ message: "error" })) renderWithRelay({ @@ -957,6 +1001,10 @@ describe("ConfirmBid", () => { await waitFor(() => expect(mockNavigator.navigate).toHaveBeenCalled()) + expect(captureExceptionSpy).toHaveBeenCalledWith( + { message: "error" }, + { tags: { source: "ConfirmBid.tsx: verifyBidderPosition" } } + ) expect(mockNavigator.navigate).toHaveBeenCalledWith( "BidResult", expect.objectContaining({ diff --git a/src/setupJest.tsx b/src/setupJest.tsx index 260f039ea3d..874aa792636 100644 --- a/src/setupJest.tsx +++ b/src/setupJest.tsx @@ -221,6 +221,7 @@ jest.mock("@invertase/react-native-apple-authentication", () => ({ jest.mock("@sentry/react-native", () => ({ captureMessage: jest.fn(), + captureException: jest.fn(), init() {}, setUser() {}, addBreadcrumb() {},