-
Notifications
You must be signed in to change notification settings - Fork 582
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
269ad2a
commit 0416b85
Showing
5 changed files
with
308 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Box style={{ paddingTop: space(2), paddingBottom: space(2) }}> | ||
<Flex flexDirection="row" px={2} justifyContent="space-between" alignItems="center"> | ||
<Box flex={1} /> | ||
<Box> | ||
<ArtsyLogoBlackIcon scale={0.75} /> | ||
</Box> | ||
<Box flex={1} alignItems="flex-end"> | ||
<ActivityIndicator hasUnseenNotifications={hasUnseenNotifications} /> | ||
</Box> | ||
</Flex> | ||
</Box> | ||
<> | ||
{!!showPaymentFailureBanner && ( | ||
<Suspense fallback={null}> | ||
<PaymentFailureBanner /> | ||
</Suspense> | ||
)} | ||
<Box py={2}> | ||
<Flex flexDirection="row" px={2} justifyContent="space-between" alignItems="center"> | ||
<Box flex={1} /> | ||
<Box> | ||
<ArtsyLogoBlackIcon scale={0.75} /> | ||
</Box> | ||
<Box flex={1} alignItems="flex-end"> | ||
<ActivityIndicator hasUnseenNotifications={hasUnseenNotifications} /> | ||
</Box> | ||
</Flex> | ||
</Box> | ||
</> | ||
) | ||
} |
102 changes: 102 additions & 0 deletions
102
src/app/Scenes/HomeView/Components/PaymentFailureBanner.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<PaymentFailureBannerQuery>( | ||
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 ( | ||
<Banner data-testid="PaymentFailureBanner" variant="error" dismissable> | ||
<Text textAlign="left" variant="xs" color="white100"> | ||
{bannerText} | ||
</Text> | ||
<LinkText | ||
variant="xs" | ||
textAlign="left" | ||
color="white100" | ||
onPress={() => handleBannerLinkClick()} | ||
> | ||
{linkText} | ||
</LinkText> | ||
</Banner> | ||
) | ||
} |
146 changes: 146 additions & 0 deletions
146
src/app/Scenes/HomeView/Components/__tests__/PaymentFailureBanner.tests.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" }]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters