From 0416b85299d6827c33b9468b91a2ca4c1f3120ec Mon Sep 17 00:00:00 2001 From: Rachel Quartararo <50849237+rquartararo@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:43:34 -0400 Subject: [PATCH] feat(EMI-2128): adds payment failure banner (#11006) * feat: add payment banner banner component * chore: add feature flag * use payment failed filters param * tests: add tests for banner * update tests * fix tests and add suspense * chore: add tracking * add links and useFocusEffect * move tracking and add useFretchableFragment * minor updates * fix text alignment * update fetchPolicy * test: add tests for links and tracking * update tracking calls * memoize handleBannerLinkClick * add order type to tracking --- .../Scenes/HomeView/Components/HomeHeader.tsx | 37 +++-- .../Components/PaymentFailureBanner.tsx | 102 ++++++++++++ .../__tests__/PaymentFailureBanner.tests.tsx | 146 ++++++++++++++++++ .../Scenes/HomeView/useHomeViewTracking.ts | 32 ++++ src/app/store/config/features.ts | 5 + 5 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 src/app/Scenes/HomeView/Components/PaymentFailureBanner.tsx create mode 100644 src/app/Scenes/HomeView/Components/__tests__/PaymentFailureBanner.tests.tsx diff --git a/src/app/Scenes/HomeView/Components/HomeHeader.tsx b/src/app/Scenes/HomeView/Components/HomeHeader.tsx index d0f7761a944..b2ff142a17c 100644 --- a/src/app/Scenes/HomeView/Components/HomeHeader.tsx +++ b/src/app/Scenes/HomeView/Components/HomeHeader.tsx @@ -1,25 +1,34 @@ -import { ArtsyLogoBlackIcon, Flex, Box, useSpace } from "@artsy/palette-mobile" +import { ArtsyLogoBlackIcon, Flex, Box } from "@artsy/palette-mobile" +import { PaymentFailureBanner } from "app/Scenes/HomeView/Components/PaymentFailureBanner" import { GlobalStore } from "app/store/GlobalStore" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { Suspense } from "react" import { ActivityIndicator } from "./ActivityIndicator" export const HomeHeader: React.FC = () => { + const showPaymentFailureBanner = useFeatureFlag("AREnablePaymentFailureBanner") const hasUnseenNotifications = GlobalStore.useAppState( (state) => state.bottomTabs.hasUnseenNotifications ) - const space = useSpace() - return ( - - - - - - - - - - - + <> + {!!showPaymentFailureBanner && ( + + + + )} + + + + + + + + + + + + ) } diff --git a/src/app/Scenes/HomeView/Components/PaymentFailureBanner.tsx b/src/app/Scenes/HomeView/Components/PaymentFailureBanner.tsx new file mode 100644 index 00000000000..20d18e61390 --- /dev/null +++ b/src/app/Scenes/HomeView/Components/PaymentFailureBanner.tsx @@ -0,0 +1,102 @@ +import { Banner, LinkText, Text } from "@artsy/palette-mobile" +import { useIsFocused } from "@react-navigation/native" +import { PaymentFailureBannerQuery } from "__generated__/PaymentFailureBannerQuery.graphql" +import { PaymentFailureBannerRefetchQuery } from "__generated__/PaymentFailureBannerRefetchQuery.graphql" +import { PaymentFailureBanner_Fragment$key } from "__generated__/PaymentFailureBanner_Fragment.graphql" +import { useHomeViewTracking } from "app/Scenes/HomeView/useHomeViewTracking" +import { navigate } from "app/system/navigation/navigate" +import { extractNodes } from "app/utils/extractNodes" +import { useCallback, useEffect } from "react" +import { graphql, useLazyLoadQuery, useRefetchableFragment } from "react-relay" + +export const PaymentFailureBanner: React.FC = () => { + const tracking = useHomeViewTracking() + const isFocused = useIsFocused() + + const initialData = useLazyLoadQuery( + graphql` + query PaymentFailureBannerQuery { + ...PaymentFailureBanner_Fragment + } + `, + {} + ) + + const [data, refetch] = useRefetchableFragment< + PaymentFailureBannerRefetchQuery, + PaymentFailureBanner_Fragment$key + >( + graphql` + fragment PaymentFailureBanner_Fragment on Query + @refetchable(queryName: "PaymentFailureBannerRefetchQuery") { + commerceMyOrders(first: 10, filters: [PAYMENT_FAILED]) { + edges { + node { + code + internalID + } + } + } + } + `, + initialData + ) + + useEffect(() => { + if (isFocused) { + refetch( + {}, + { + fetchPolicy: "store-and-network", + } + ) + } + }, [isFocused]) + + const failedPayments = extractNodes(data?.commerceMyOrders) + + useEffect(() => { + if (failedPayments.length > 0) { + tracking.bannerViewed(failedPayments) + } + }, [failedPayments, tracking]) + + const handleBannerLinkClick = useCallback(() => { + tracking.tappedChangePaymentMethod(failedPayments) + + const route = + failedPayments.length === 1 + ? `orders/${failedPayments[0].internalID}/payment/new` + : `settings/purchases` + + navigate(route) + }, [failedPayments, tracking]) + + if (failedPayments.length === 0) { + return null + } + + const bannerText = + failedPayments.length === 1 + ? "Payment failed for your recent order." + : "Payment failed for your recent orders." + + const linkText = + failedPayments.length === 1 ? "Update payment method." : "Update payment method for each order." + + return ( + + + {bannerText} + + handleBannerLinkClick()} + > + {linkText} + + + ) +} diff --git a/src/app/Scenes/HomeView/Components/__tests__/PaymentFailureBanner.tests.tsx b/src/app/Scenes/HomeView/Components/__tests__/PaymentFailureBanner.tests.tsx new file mode 100644 index 00000000000..c048491fcba --- /dev/null +++ b/src/app/Scenes/HomeView/Components/__tests__/PaymentFailureBanner.tests.tsx @@ -0,0 +1,146 @@ +import { fireEvent, screen } from "@testing-library/react-native" +import { PaymentFailureBanner } from "app/Scenes/HomeView/Components/PaymentFailureBanner" +import { useHomeViewTracking } from "app/Scenes/HomeView/useHomeViewTracking" +import { navigate } from "app/system/navigation/navigate" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "react-relay" + +const mockUseIsFocusedMock = jest.fn() + +jest.mock("app/Scenes/HomeView/useHomeViewTracking", () => ({ + useHomeViewTracking: jest.fn(), +})) + +jest.mock("@react-navigation/native", () => ({ + useIsFocused: () => mockUseIsFocusedMock(), +})) + +describe("PaymentFailureBanner", () => { + const { renderWithRelay } = setupTestWrapper({ + Component: PaymentFailureBanner, + query: graphql` + query PaymentFailureBannerTestsQuery { + commerceMyOrders(first: 10, filters: [PAYMENT_FAILED]) { + edges { + node { + code + internalID + } + } + } + } + `, + variables: {}, + }) + + const mockTracking = { + bannerViewed: jest.fn(), + tappedChangePaymentMethod: jest.fn(), + } + + beforeEach(() => { + mockUseIsFocusedMock.mockReturnValue(false) + jest.clearAllMocks() + ;(useHomeViewTracking as jest.Mock).mockReturnValue(mockTracking) + }) + + it("renders the error banner when a single payment has failed", () => { + renderWithRelay({ + CommerceOrderConnectionWithTotalCount: () => ({ + edges: [ + { + node: { + code: "order-1", + internalID: "1", + }, + }, + ], + }), + }) + + const link = screen.getByText("Update payment method.") + const text = screen.getByText("Payment failed for your recent order.") + + expect(link).toBeTruthy() + expect(text).toBeTruthy() + + fireEvent.press(link) + expect(navigate).toHaveBeenCalledWith("orders/1/payment/new") + }) + + it("renders the error banner when multiple payments have failed", () => { + renderWithRelay({ + CommerceOrderConnectionWithTotalCount: () => ({ + edges: [ + { + node: { + code: "order-1", + internalID: "1", + }, + }, + { + node: { + code: "order-2", + internalID: "2", + }, + }, + ], + }), + }) + const link = screen.getByText("Update payment method for each order.") + const text = screen.getByText("Payment failed for your recent orders.") + + expect(link).toBeTruthy() + expect(text).toBeTruthy() + + fireEvent.press(link) + expect(navigate).toHaveBeenCalledWith("settings/purchases") + }) + + it("does not render the banner when there are no payment failures", () => { + renderWithRelay({ + CommerceOrderConnectionWithTotalCount: () => ({ + edges: [], + }), + }) + + expect(screen.queryByTestId("PaymentFailureBanner")).toBeNull() + }) + + it("calls bannerViewed tracking event when the banner is visible", () => { + renderWithRelay({ + CommerceOrderConnectionWithTotalCount: () => ({ + edges: [ + { + node: { + code: "order-1", + internalID: "1", + }, + }, + ], + }), + }) + + expect(mockTracking.bannerViewed).toHaveBeenCalledWith([{ code: "order-1", internalID: "1" }]) + }) + + it("calls tappedChangePaymentMethod tracking event when the link is clicked", () => { + renderWithRelay({ + CommerceOrderConnectionWithTotalCount: () => ({ + edges: [ + { + node: { + code: "order-1", + internalID: "1", + }, + }, + ], + }), + }) + + const link = screen.getByText("Update payment method.") + fireEvent.press(link) + + expect(mockTracking.bannerViewed).toHaveBeenCalledWith([{ code: "order-1", internalID: "1" }]) + }) +}) diff --git a/src/app/Scenes/HomeView/useHomeViewTracking.ts b/src/app/Scenes/HomeView/useHomeViewTracking.ts index 824b47b23b1..8d08c79ffc2 100644 --- a/src/app/Scenes/HomeView/useHomeViewTracking.ts +++ b/src/app/Scenes/HomeView/useHomeViewTracking.ts @@ -1,5 +1,6 @@ import { ActionType, + BannerViewed, ContextModule, OwnerType, RailViewed, @@ -12,6 +13,7 @@ import { TappedAuctionGroup, TappedAuctionResultGroup, TappedCardGroup, + TappedChangePaymentMethod, TappedClearNotification, TappedCollectionGroup, TappedFairGroup, @@ -22,8 +24,10 @@ import { TappedShowMore, TappedViewingRoomGroup, } from "@artsy/cohesion" +import { PaymentFailureBanner_Fragment$data } from "__generated__/PaymentFailureBanner_Fragment.graphql" import { getArtworkSignalTrackingFields } from "app/utils/getArtworkSignalTrackingFields" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { ExtractNodeType } from "app/utils/relayHelpers" import { useTracking } from "react-tracking" export const useHomeViewTracking = () => { @@ -40,6 +44,20 @@ export const useHomeViewTracking = () => { trackEvent(payload) }, + bannerViewed: ( + orders: Array> + ) => { + const payload: BannerViewed = { + action: ActionType.bannerViewed, + context_screen: OwnerType.home, + context_module: ContextModule.paymentFailed, + item_type: orders.length === 1 ? "order" : "orders", + item_id: orders.length === 1 ? orders[0].internalID : "", + } + + trackEvent(payload) + }, + tappedNotificationBell: () => { const payload: TappedNotificationsBell = { action: ActionType.tappedNotificationsBell, @@ -484,5 +502,19 @@ export const useHomeViewTracking = () => { trackEvent(payload) }, + + tappedChangePaymentMethod: ( + orders: Array> + ) => { + const payload: TappedChangePaymentMethod = { + action: ActionType.tappedChangePaymentMethod, + context_screen: OwnerType.home, + context_module: ContextModule.paymentFailed, + item_type: orders.length === 1 ? "order" : "orders", + item_id: orders.length === 1 ? orders[0].internalID : "", + } + + trackEvent(payload) + }, } } diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index db4928294ea..4c44e7891a9 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -305,6 +305,11 @@ export const features = { readyForRelease: false, showInDevMenu: true, }, + AREnablePaymentFailureBanner: { + description: "Enable payment failure banner", + readyForRelease: false, + showInDevMenu: true, + }, } satisfies { [key: string]: FeatureDescriptor } export interface DevToggleDescriptor {