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 {