From 0fa34fb1c76ab5b11e87f2aed5dfdf5ff0d82929 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2024 17:20:33 +0200 Subject: [PATCH] feat: [IOCOM-1199] Vertically ordered CTAs and Payment button on new DS message details screen (#5636) ## Short description This PR changes the CTAs and Payment buttons alignment on a message details screen with the new DS. |Payment with
double CTAs|Payment with
CTA|No payment
double CTAs| |-|-|-| |![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 03 41](https://github.com/pagopa/io-app/assets/5150343/29de6e5f-f7e5-4593-936e-3b030a54a441)|![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 03 48](https://github.com/pagopa/io-app/assets/5150343/eab13609-742a-4f90-9b83-03627e2ed0d4)|![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 03 56](https://github.com/pagopa/io-app/assets/5150343/36704748-e221-439c-9519-d6e88d312c81)| |Payment
without CTAs|CTA without
payment|No payment
no CTAs| |-|-|-| |![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 04 09](https://github.com/pagopa/io-app/assets/5150343/7d6249ee-e753-4784-9a81-18348c890b86)|![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 04 18](https://github.com/pagopa/io-app/assets/5150343/fe97bf76-ef3f-4bd1-952e-74dbc83139ff)|![Simulator Screenshot - iPhone 15 - 2024-03-27 at 13 04 23](https://github.com/pagopa/io-app/assets/5150343/852615b9-1c00-4d79-a660-9aefa7515957)| ## List of changes proposed in this pull request The algorithm is a follow: - if there is a payment and two CTAs, the payment button is shown on top (solid), the first CTA button is on the middle (Outline) and the second CTA button is on the bottom (as a link) - if there is a payment and a single CTA, the payment button is shown on top (solid) and the CTA button is on the bottom (as a link) - if there is no payment and two CTAs, the first CTA button in on top (solid) and the second CTA button is on the bottom (as a link) - if there is a payment and no CTAs, the payment button is displayed (solid) - if there is no payment and a single CTA, the CTA button is displayed (solid) - if there is no payment and no CTAs, the sticky footer is not displayed Keep in mind that the payment button may disappear asynchronously if the payment is later checked to be paid. ## How to test Using the io-dev-api-server, generate some messages with payment and CTAs and check that each of the conditions described above is verified. --- ts/components/cta/CTAsBar.tsx | 70 - ts/components/cta/ExtractedCTABar.tsx | 4 +- ts/components/cta/__test__/CTAsBar.test.tsx | 38 - .../MessageDetailsPaymentButton.tsx | 46 + ...essageDetailsScrollViewAdditionalSpace.tsx | 18 +- .../MessageDetailsStickyFooter.tsx | 336 ++++- .../MessageDetailsPaymentButton.test.tsx | 50 + ...eDetailsScrollViewAdditionalSpace.test.tsx | 32 +- .../MessageDetailsStickyFooter.test.tsx | 2 + ...MessageDetailsPaymentButton.test.tsx.snap} | 578 ++++---- ...ilsScrollViewAdditionalSpace.test.tsx.snap | 692 +++++++++- .../MessageDetailsStickyFooter.test.tsx.snap | 1159 ++++++++--------- .../messages/screens/MessageDetailsScreen.tsx | 27 +- ts/features/messages/utils/index.ts | 3 +- ts/features/pn/hooks/usePNOptInMessage.ts | 31 - ts/features/pn/utils/__tests__/index.test.ts | 48 +- ts/features/pn/utils/index.ts | 16 +- 17 files changed, 1996 insertions(+), 1154 deletions(-) delete mode 100644 ts/components/cta/CTAsBar.tsx delete mode 100644 ts/components/cta/__test__/CTAsBar.test.tsx create mode 100644 ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx create mode 100644 ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx rename ts/{components/cta/__test__/__snapshots__/CTAsBar.test.tsx.snap => features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap} (59%) delete mode 100644 ts/features/pn/hooks/usePNOptInMessage.ts diff --git a/ts/components/cta/CTAsBar.tsx b/ts/components/cta/CTAsBar.tsx deleted file mode 100644 index 2ae53aba35b..00000000000 --- a/ts/components/cta/CTAsBar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useCallback } from "react"; -import { View } from "react-native"; -import { useLinkTo } from "@react-navigation/native"; -import { - ButtonOutline, - ButtonSolid, - HSpacer, - IOStyles -} from "@pagopa/io-app-design-system"; -import { CTA, CTAS } from "../../features/messages/types/MessageCTA"; -import { ServiceId } from "../../../definitions/backend/ServiceId"; -import { trackPNOptInMessageAccepted } from "../../features/pn/analytics"; -import { handleCtaAction } from "../../features/messages/utils/messages"; -import { usePNOptInMessage } from "../../features/pn/hooks/usePNOptInMessage"; - -type CTAsBarProps = { - ctas: CTAS; - serviceId: ServiceId; -}; - -/** - * render cta_1 and cta_2 if they are defined in the message content as front-matter - * or if they are defined on cta attribute in ServiceMetadata in the ServiceDetailScreen - */ -export const CTAsBar = ({ ctas, serviceId }: CTAsBarProps) => { - const { cta_1, cta_2 } = ctas; - const { - cta1HasServiceNavigationLink, - cta2HasServiceNavigationLink, - isPNOptInMessage - } = usePNOptInMessage(ctas, serviceId); - - const linkTo = useLinkTo(); - - const handleOnPress = useCallback( - (cta: CTA, isServiceNavigationLink: boolean) => { - if (isPNOptInMessage && isServiceNavigationLink) { - trackPNOptInMessageAccepted(); - } - handleCtaAction(cta, linkTo, serviceId); - }, - [isPNOptInMessage, linkTo, serviceId] - ); - - return ( - - {cta_2 && ( - <> - - handleOnPress(cta_2, cta2HasServiceNavigationLink)} - /> - - - - )} - - handleOnPress(cta_1, cta1HasServiceNavigationLink)} - /> - - - ); -}; diff --git a/ts/components/cta/ExtractedCTABar.tsx b/ts/components/cta/ExtractedCTABar.tsx index 52853f482eb..1e9bb07f129 100644 --- a/ts/components/cta/ExtractedCTABar.tsx +++ b/ts/components/cta/ExtractedCTABar.tsx @@ -68,7 +68,7 @@ const ExtractedCTABar: React.FunctionComponent = ( props, linkTo, false, - props.isPNOptInMessage?.cta2HasServiceNavigationLink ?? false, + props.isPNOptInMessage?.cta2LinksToPNService ?? false, ctas.cta_2 ), [ctas.cta_2, linkTo, props] @@ -79,7 +79,7 @@ const ExtractedCTABar: React.FunctionComponent = ( props, linkTo, true, - props.isPNOptInMessage?.cta1HasServiceNavigationLink ?? false, + props.isPNOptInMessage?.cta1LinksToPNService ?? false, ctas.cta_1 ), [ctas.cta_1, linkTo, props] diff --git a/ts/components/cta/__test__/CTAsBar.test.tsx b/ts/components/cta/__test__/CTAsBar.test.tsx deleted file mode 100644 index d495bc76c4b..00000000000 --- a/ts/components/cta/__test__/CTAsBar.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"; -import { createStore } from "redux"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { CTAS } from "../../../features/messages/types/MessageCTA"; -import { appReducer } from "../../../store/reducers"; -import { applicationChangeState } from "../../../store/actions/application"; -import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper"; -import { CTAsBar } from "../CTAsBar"; - -describe("CTAsBar", () => { - it("should match snapshot with one CTA", () => { - const serviceId = "01HRW50F08QYXNP518JYCKVSHP" as ServiceId; - const ctas = { cta_1: { text: "My CTA 1", action: "" } } as CTAS; - const component = renderComponent(serviceId, ctas); - expect(component.toJSON()).toMatchSnapshot(); - }); - it("should match snapshot with both CTAs", () => { - const serviceId = "01HRW50F08QYXNP518JYCKVSHP" as ServiceId; - const ctas = { - cta_1: { text: "My CTA 1", action: "" }, - cta_2: { text: "My CTA 2", action: "" } - } as CTAS; - const component = renderComponent(serviceId, ctas); - expect(component.toJSON()).toMatchSnapshot(); - }); -}); - -const renderComponent = (serviceId: ServiceId, ctas: CTAS) => { - const globalState = appReducer(undefined, applicationChangeState("active")); - const store = createStore(appReducer, globalState as any); - - return renderScreenWithNavigationStoreContext( - () => , - "DUMMY", - {}, - store - ); -}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx new file mode 100644 index 00000000000..09098d972ce --- /dev/null +++ b/ts/features/messages/components/MessageDetail/MessageDetailsPaymentButton.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { ButtonSolid, useIOToast } from "@pagopa/io-app-design-system"; +import { PaymentData, UIMessageId } from "../../types"; +import { useIODispatch } from "../../../../store/hooks"; +import I18n from "../../../../i18n"; +import { + getRptIdStringFromPaymentData, + initializeAndNavigateToWalletForPayment +} from "../../utils"; + +type MessageDetailsPaymentButtonProps = { + messageId: UIMessageId; + paymentData: PaymentData; + canNavigateToPayment: boolean; + isLoading: boolean; +}; + +export const MessageDetailsPaymentButton = ({ + messageId, + paymentData, + canNavigateToPayment, + isLoading +}: MessageDetailsPaymentButtonProps) => { + const dispatch = useIODispatch(); + const toast = useIOToast(); + return ( + + initializeAndNavigateToWalletForPayment( + messageId, + getRptIdStringFromPaymentData(paymentData), + false, + paymentData.amount, + canNavigateToPayment, + dispatch, + false, + () => toast.error(I18n.t("genericError")) + ) + } + fullWidth + loading={isLoading} + /> + ); +}; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx index faf56918188..dba6e46b300 100644 --- a/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx +++ b/ts/features/messages/components/MessageDetail/MessageDetailsScrollViewAdditionalSpace.tsx @@ -9,25 +9,29 @@ import { gapBetweenItemsInAGrid } from "../../utils"; type ScrollViewAdditionalSpaceProps = { messageId: UIMessageId; - hasCTAS: boolean; + hasCTA1: boolean; + hasCTA2: boolean; }; export const MessageDetailsScrollViewAdditionalSpace = ({ messageId, - hasCTAS + hasCTA1, + hasCTA2 }: ScrollViewAdditionalSpaceProps) => { const safeAreaInsets = useSafeAreaInsets(); const isShowingPaymentButton = useIOSelector(state => isPaymentsButtonVisibleSelector(state, messageId) ); - const stickyFooterRowHeight = - IOStyles.footer.paddingBottom + buttonSolidHeight + gapBetweenItemsInAGrid; + const hasAtLeastAButton = isShowingPaymentButton || hasCTA1 || hasCTA2; const height = - safeAreaInsets.bottom + + (hasAtLeastAButton ? IOStyles.footer.paddingBottom : 0) + + (isShowingPaymentButton ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + (hasCTA1 ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + (hasCTA2 ? buttonSolidHeight + gapBetweenItemsInAGrid : 0) + + gapBetweenItemsInAGrid + IOStyles.footer.paddingBottom + - (isShowingPaymentButton ? stickyFooterRowHeight : 0) + - (hasCTAS ? stickyFooterRowHeight : 0); + safeAreaInsets.bottom; return ( + footerData.tag === "None"; + +const foldFooterData = ( + footerData: FooterData, + onPaymentWithDoubleCTA: ( + paymentWithDoubleCTA: FooterPaymentWithDoubleCTA + ) => JSX.Element, + onPaymentWithCTA: (paymentWithCTA: FooterPaymentWithCTA) => JSX.Element, + onDoubleCTA: (doubleCTA: FooterDoubleCTA) => JSX.Element, + onPayment: (paymentCTA: FooterPayment) => JSX.Element, + onCTA: (cta: FooterCTA) => JSX.Element, + onNone: () => JSX.Element | null +) => { + switch (footerData.tag) { + case "PaymentWithDoubleCTA": + return onPaymentWithDoubleCTA(footerData); + case "PaymentWithCTA": + return onPaymentWithCTA(footerData); + case "DoubleCTA": + return onDoubleCTA(footerData); + case "Payment": + return onPayment(footerData); + case "CTA": + return onCTA(footerData); + } + return onNone(); +}; + +const computeFooterData = ( + paymentData: PaymentData | undefined, + paymentButtonStatus: "hidden" | "loading" | "enabled", + ctas: CTAS | undefined +): FooterData => { + const isPaymentButtonVisible = + paymentData && paymentButtonStatus !== "hidden"; + const isCTA1Visible = !!ctas?.cta_1; + const cta2 = ctas?.cta_2; + const isCTA2Visible = !!cta2; + if (isPaymentButtonVisible && isCTA1Visible && isCTA2Visible) { + return { + tag: "PaymentWithDoubleCTA", + cta1: ctas.cta_1, + cta2, + paymentData + }; + } else if (isPaymentButtonVisible && isCTA1Visible) { + return { + tag: "PaymentWithCTA", + cta1: ctas.cta_1, + paymentData + }; + } else if (isCTA1Visible && isCTA2Visible) { + return { + tag: "DoubleCTA", + cta1: ctas.cta_1, + cta2 + }; + } else if (isPaymentButtonVisible) { + return { + tag: "Payment", + paymentData + }; + } else if (isCTA1Visible) { + return { + tag: "CTA", + cta1: ctas.cta_1 + }; + } + return { tag: "None" }; +}; + +const renderPaymentWithDoubleCTA = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean, + cta1: CTA, + cta1IsPNOptInMessage: boolean, + cta2: CTA, + cta2IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + + + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + + onCTAPress(cta2, cta2IsPNOptInMessage)} + /> + + +); +const renderPaymentWithCTA = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean, + cta1: CTA, + cta1IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + + + + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + +); +const renderDoubleCTA = ( + cta1: CTA, + cta1IsPNOptInMessage: boolean, + cta2: CTA, + cta2IsPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + <> + onCTAPress(cta1, cta1IsPNOptInMessage)} + /> + + + onCTAPress(cta2, cta2IsPNOptInMessage)} + /> + + +); +const renderPayment = ( + messageId: UIMessageId, + paymentData: PaymentData, + canNavigateToPayment: boolean, + isLoadingPayment: boolean +) => ( + +); +const renderCTA = ( + cta: CTA, + isPNOptInMessage: boolean, + onCTAPress: (cta: CTA, isPNOptInMessage: boolean) => void +) => ( + onCTAPress(cta, isPNOptInMessage)} + /> +); + export const MessageDetailsStickyFooter = ({ ctas, + firstCTAIsPNOptInMessage, messageId, + secondCTAIsPNOptInMessage, serviceId }: MessageDetailsPaymentButtonProps) => { - const dispatch = useIODispatch(); - const toast = useIOToast(); const safeAreaInsets = useSafeAreaInsets(); const paymentData = useIOSelector(state => messagePaymentDataSelector(state, messageId) ); - const componentVisibility = useIOSelector(state => + const paymentButtonStatus = useIOSelector(state => paymentsButtonStateSelector(state, messageId) ); const canNavigateToPayment = useIOSelector(state => canNavigateToPaymentFromMessageSelector(state) ); - const hidePaymentButton = !paymentData || componentVisibility === "hidden"; - if (!ctas && hidePaymentButton) { + + const linkTo = useLinkTo(); + const handleOnPress = React.useCallback( + (cta: CTA, isPNOptInMessage: boolean) => { + if (isPNOptInMessage) { + trackPNOptInMessageAccepted(); + } + handleCtaAction(cta, linkTo, serviceId); + }, + [linkTo, serviceId] + ); + + const footerData = computeFooterData(paymentData, paymentButtonStatus, ctas); + if (isNone(footerData)) { return null; } + + const isPaymentLoading = paymentButtonStatus === "loading"; return ( - {ctas && ( - <> - - {!hidePaymentButton && } - - )} - {!hidePaymentButton && ( - - initializeAndNavigateToWalletForPayment( - messageId, - getRptIdStringFromPaymentData(paymentData), - false, - paymentData.amount, - canNavigateToPayment, - dispatch, - false, - () => toast.error(I18n.t("genericError")) - ) - } - fullWidth - loading={componentVisibility === "loading"} - /> + {foldFooterData( + footerData, + paymentWithDoubleCTA => + renderPaymentWithDoubleCTA( + messageId, + paymentWithDoubleCTA.paymentData, + canNavigateToPayment, + isPaymentLoading, + paymentWithDoubleCTA.cta1, + firstCTAIsPNOptInMessage, + paymentWithDoubleCTA.cta2, + secondCTAIsPNOptInMessage, + handleOnPress + ), + paymentWithCTA => + renderPaymentWithCTA( + messageId, + paymentWithCTA.paymentData, + canNavigateToPayment, + isPaymentLoading, + paymentWithCTA.cta1, + firstCTAIsPNOptInMessage, + handleOnPress + ), + doubleCTA => + renderDoubleCTA( + doubleCTA.cta1, + firstCTAIsPNOptInMessage, + doubleCTA.cta2, + secondCTAIsPNOptInMessage, + handleOnPress + ), + payment => + renderPayment( + messageId, + payment.paymentData, + canNavigateToPayment, + isPaymentLoading + ), + cta => renderCTA(cta.cta1, firstCTAIsPNOptInMessage, handleOnPress), + () => null )} ); diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx new file mode 100644 index 00000000000..7a1c32f65c9 --- /dev/null +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsPaymentButton.test.tsx @@ -0,0 +1,50 @@ +import * as React from "react"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../../../store/actions/persistedPreferences"; +import { appReducer } from "../../../../../store/reducers"; +import { MessageDetailsPaymentButton } from "../MessageDetailsPaymentButton"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { PaymentData, UIMessageId } from "../../../types"; + +describe("MessageDetailsPaymentButton", () => { + it("should match snapshot when not loading", () => { + const screen = renderScreen(false); + expect(screen.toJSON()).toMatchSnapshot(); + }); + it("should match snapshot when loading", () => { + const screen = renderScreen(true); + expect(screen.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = (isLoading: boolean) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const store = createStore(appReducer, designSystemState as any); + + return renderScreenWithNavigationStoreContext( + () => ( + + ), + "DUMMY", + {}, + store + ); +}; diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx index c5c66592afc..bc1d8571115 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsScrollViewAdditionalSpace.test.tsx @@ -16,34 +16,49 @@ describe("MessageDetailsScrollViewAdditionalSpace", () => { jest .spyOn(payments, "isPaymentsButtonVisibleSelector") .mockReturnValue(false); - const component = renderComponent(false); + const component = renderComponent(false, false); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match snapshot with hidden button and CTAs", () => { + it("Should match snapshot with hidden button and both CTAs", () => { jest .spyOn(payments, "isPaymentsButtonVisibleSelector") .mockReturnValue(false); - const component = renderComponent(true); + const component = renderComponent(true, true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with hidden button and a single CTA", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(false); + const component = renderComponent(true, false); expect(component.toJSON()).toMatchSnapshot(); }); it("Should match snapshot with button and no CTAs", () => { jest .spyOn(payments, "isPaymentsButtonVisibleSelector") .mockReturnValue(true); - const component = renderComponent(false); + const component = renderComponent(false, false); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match snapshot with button and both CTA", () => { + jest + .spyOn(payments, "isPaymentsButtonVisibleSelector") + .mockReturnValue(true); + const component = renderComponent(true, true); expect(component.toJSON()).toMatchSnapshot(); }); - it("Should match snapshot with button and CTAs", () => { + it("Should match snapshot with button and a single CTA", () => { jest .spyOn(payments, "isPaymentsButtonVisibleSelector") .mockReturnValue(true); - const component = renderComponent(true); + const component = renderComponent(true, false); expect(component.toJSON()).toMatchSnapshot(); }); }); const renderComponent = ( - hasCTAS: boolean, + hasCTA1: boolean, + hasCTA2: boolean, messageId: UIMessageId = "01HRW5J2QYMH3FWAA5CYGXSC84" as UIMessageId ) => { const globalState = appReducer(undefined, applicationChangeState("active")); @@ -52,7 +67,8 @@ const renderComponent = ( () => ( ), "DUMMY", diff --git a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx index 7e076a8e5c8..0e8103073f8 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx +++ b/ts/features/messages/components/MessageDetail/__tests__/MessageDetailsStickyFooter.test.tsx @@ -128,6 +128,8 @@ const renderComponent = (ctas?: CTAS) => { messageId={"01HRW6GJBD594Z0K9B4D6KAERC" as UIMessageId} serviceId={"01HRW6GS171WY97ZBJ5BPFJ625" as ServiceId} ctas={ctas} + firstCTAIsPNOptInMessage={false} + secondCTAIsPNOptInMessage={false} /> ), "DUMMY", diff --git a/ts/components/cta/__test__/__snapshots__/CTAsBar.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap similarity index 59% rename from ts/components/cta/__test__/__snapshots__/CTAsBar.test.tsx.snap rename to ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap index e5823eeaabd..b66722bdcd5 100644 --- a/ts/components/cta/__test__/__snapshots__/CTAsBar.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsPaymentButton.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CTAsBar should match snapshot with both CTAs 1`] = ` +exports[`MessageDetailsPaymentButton should match snapshot when loading 1`] = ` - - - - My CTA 2 - - - - - - + - - - My CTA 1 - + + + + + + + + + + + + @@ -560,7 +578,7 @@ exports[`CTAsBar should match snapshot with both CTAs 1`] = ` `; -exports[`CTAsBar should match snapshot with one CTA 1`] = ` +exports[`MessageDetailsPaymentButton should match snapshot when not loading 1`] = ` - - - - My CTA 1 - - - + Pay + diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap index 38203e85a2b..c9138d1c9bf 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsScrollViewAdditionalSpace.test.tsx.snap @@ -1,6 +1,684 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and CTAs 1`] = ` +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and a single CTA 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and both CTA 1`] = ` + + + + + + + + + + + + + + + DUMMY + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and no CTAs 1`] = ` @@ -339,7 +1017,7 @@ exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with butt `; -exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with button and no CTAs 1`] = ` +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and a single CTA 1`] = ` @@ -678,7 +1356,7 @@ exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with butt `; -exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and CTAs 1`] = ` +exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidden button and both CTAs 1`] = ` @@ -1339,7 +2017,7 @@ exports[`MessageDetailsScrollViewAdditionalSpace Should match snapshot with hidd diff --git a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap index 86f13b0a1a6..93ca6778f3c 100644 --- a/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap +++ b/ts/features/messages/components/MessageDetail/__tests__/__snapshots__/MessageDetailsStickyFooter.test.tsx.snap @@ -349,230 +349,213 @@ exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and no } > - - - CTA 2 - - + CTA 1 + + + + - - - - - CTA 1 - - - + CTA 2 + @@ -939,230 +922,110 @@ exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and vis } > - - - - CTA 2 - - - - - - - - - - CTA 1 - - - + Pay + @@ -1174,11 +1037,10 @@ exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and vis } /> + + CTA 1 + + + + + + @@ -1245,38 +1202,28 @@ exports[`MessageDetailsStickyFooter should match snapshot with both CTAs and vis accessibilityElementsHidden={true} accessible={false} allowFontScaling={false} - color="white" - defaultColor="white" - defaultWeight="Bold" ellipsizeMode="tail" - font="TitilliumWeb" - fontStyle={ - Object { - "fontSize": 16, - } - } importantForAccessibility="no-hide-descendants" maxFontSizeMultiplier={1.3} numberOfLines={1} style={ Array [ Object { - "alignSelf": "center", - }, - Object { - "fontSize": 16, - }, - Object { - "color": "#FFFFFF", "fontFamily": "Titillium Web", + "fontSize": 16, "fontStyle": "normal", "fontWeight": "700", }, + Object { + "color": "#0073E6", + }, + Object { + "color": undefined, + }, ] } - weight="Bold" > - Pay + CTA 2 @@ -3384,126 +3331,110 @@ exports[`MessageDetailsStickyFooter should match snapshot with one CTA and no pa } > - - - - CTA 1 - - - + CTA 1 + @@ -3869,137 +3800,6 @@ exports[`MessageDetailsStickyFooter should match snapshot with one CTA and visib ] } > - - - - - - - CTA 1 - - - - - - - + + + + + + CTA 1 + + + + diff --git a/ts/features/messages/screens/MessageDetailsScreen.tsx b/ts/features/messages/screens/MessageDetailsScreen.tsx index 6005a87a353..6eebac0b4b7 100644 --- a/ts/features/messages/screens/MessageDetailsScreen.tsx +++ b/ts/features/messages/screens/MessageDetailsScreen.tsx @@ -41,6 +41,12 @@ import { userSelectedPaymentRptIdSelector } from "../store/reducers/payments"; import { MessageDetailsStickyFooter } from "../components/MessageDetail/MessageDetailsStickyFooter"; import { MessageDetailsScrollViewAdditionalSpace } from "../components/MessageDetail/MessageDetailsScrollViewAdditionalSpace"; import { serviceMetadataByIdSelector } from "../../../store/reducers/entities/services/servicesById"; +import { isPNOptInMessage } from "../../pn/utils"; +import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; +import { + trackPNOptInMessageCTADisplaySuccess, + trackPNOptInMessageOpened +} from "../../pn/analytics"; const styles = StyleSheet.create({ scrollContentContainer: { @@ -112,13 +118,27 @@ export const MessageDetailsScreen = (props: MessageDetailsScreenProps) => { [messageMarkdown, serviceId, serviceMetadata] ); + // Use the store since `isPNOptInMessage` is not a selector but an utility + // that uses a backend status configuration that is normally updated every + // minute. We do not want to cause a re-rendering or recompute the value + const store = useIOStore(); + const state = store.getState(); + const pnOptInMessageInfo = isPNOptInMessage(maybeCTAs, serviceId, state); + useHeaderSecondLevel({ title: "", goBack, supportRequest: true }); - const store = useIOStore(); + useOnFirstRender( + () => { + trackPNOptInMessageOpened(); + trackPNOptInMessageCTADisplaySuccess(); + }, + () => pnOptInMessageInfo.isPNOptInMessage + ); + useFocusEffect( useCallback(() => { const globalState = store.getState(); @@ -184,13 +204,16 @@ export const MessageDetailsScreen = (props: MessageDetailsScreenProps) => { diff --git a/ts/features/messages/utils/index.ts b/ts/features/messages/utils/index.ts index 86d14002ed7..4e29b4fb26e 100644 --- a/ts/features/messages/utils/index.ts +++ b/ts/features/messages/utils/index.ts @@ -16,6 +16,7 @@ import { PaymentAmount } from "../../../../definitions/backend/PaymentAmount"; import { getAmountFromPaymentAmount } from "../../../utils/payment"; import { trackPNPaymentStart } from "../../pn/analytics"; import { addUserSelectedPaymentRptId } from "../store/actions"; +import { Action } from "../../../store/actions/types"; import { MessagePaymentExpirationInfo } from "./messages"; export const gapBetweenItemsInAGrid = 8; @@ -55,7 +56,7 @@ export const initializeAndNavigateToWalletForPayment = ( isPaidOrHasAnError: boolean, paymentAmount: PaymentAmount | undefined, canNavigateToPayment: boolean, - dispatch: Dispatch, + dispatch: Dispatch, isPNPayment: boolean, decodeErrorCallback: (() => void) | undefined, preNavigationCallback: (() => void) | undefined = undefined diff --git a/ts/features/pn/hooks/usePNOptInMessage.ts b/ts/features/pn/hooks/usePNOptInMessage.ts deleted file mode 100644 index 47c87c297ea..00000000000 --- a/ts/features/pn/hooks/usePNOptInMessage.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isPNOptInMessage } from "../utils"; -import { useOnFirstRender } from "../../../utils/hooks/useOnFirstRender"; -import { - trackPNOptInMessageCTADisplaySuccess, - trackPNOptInMessageOpened -} from "../analytics"; -import { ServiceId } from "../../../../definitions/backend/ServiceId"; -import { useIOStore } from "../../../store/hooks"; -import { CTAS } from "../../messages/types/MessageCTA"; - -export const usePNOptInMessage = ( - ctas: CTAS | undefined, - serviceId: ServiceId -) => { - // Use the store since `isPNOptInMessage` is not a selector but an utility - // that uses a backend status configuration that is normally updated every - // minute. We do not want to cause a re-rendering or recompute the value - const store = useIOStore(); - const state = store.getState(); - const pnOptInMessageInfo = isPNOptInMessage(ctas, serviceId, state); - - useOnFirstRender( - () => { - trackPNOptInMessageOpened(); - trackPNOptInMessageCTADisplaySuccess(); - }, - () => pnOptInMessageInfo.isPNOptInMessage - ); - - return pnOptInMessageInfo; -}; diff --git a/ts/features/pn/utils/__tests__/index.test.ts b/ts/features/pn/utils/__tests__/index.test.ts index 23bccc5f4d6..9877ffdf70e 100644 --- a/ts/features/pn/utils/__tests__/index.test.ts +++ b/ts/features/pn/utils/__tests__/index.test.ts @@ -43,8 +43,8 @@ type IsPNOptInMessageTestInputType = { }; output: { isPNOptInMessage: boolean; - cta1HasServiceNavigationLink: boolean; - cta2HasServiceNavigationLink: boolean; + cta1LinksToPNService: boolean; + cta2LinksToPNService: boolean; }; }; @@ -58,8 +58,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: true + cta1LinksToPNService: true, + cta2LinksToPNService: true } }, { @@ -75,8 +75,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: true, + cta2LinksToPNService: false } }, { @@ -95,8 +95,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: true, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: true, + cta2LinksToPNService: false } }, { @@ -115,8 +115,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: true, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: true + cta1LinksToPNService: false, + cta2LinksToPNService: true } }, { @@ -134,8 +134,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -157,8 +157,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -171,8 +171,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -185,8 +185,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -199,8 +199,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -224,8 +224,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } }, { @@ -243,8 +243,8 @@ const isPNOptInMessageTestInput: Array = [ }, output: { isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false } } ]; diff --git a/ts/features/pn/utils/index.ts b/ts/features/pn/utils/index.ts index d70616217f3..48fea153997 100644 --- a/ts/features/pn/utils/index.ts +++ b/ts/features/pn/utils/index.ts @@ -24,8 +24,8 @@ export function getNotificationStatusInfo(status: NotificationStatus) { export type PNOptInMessageInfo = { isPNOptInMessage: boolean; - cta1HasServiceNavigationLink: boolean; - cta2HasServiceNavigationLink: boolean; + cta1LinksToPNService: boolean; + cta2LinksToPNService: boolean; }; export const isPNOptInMessage = ( @@ -50,24 +50,24 @@ export const isPNOptInMessage = ( ctas, O.fromNullable, O.map(ctas => ({ - cta1HasServiceNavigationLink: isServiceDetailNavigationLink( + cta1LinksToPNService: isServiceDetailNavigationLink( ctas.cta_1.action ), - cta2HasServiceNavigationLink: + cta2LinksToPNService: !!ctas.cta_2 && isServiceDetailNavigationLink(ctas.cta_2.action) })), O.map(ctaNavigationLinkInfo => ({ isPNOptInMessage: - ctaNavigationLinkInfo.cta1HasServiceNavigationLink || - ctaNavigationLinkInfo.cta2HasServiceNavigationLink, + ctaNavigationLinkInfo.cta1LinksToPNService || + ctaNavigationLinkInfo.cta2LinksToPNService, ...ctaNavigationLinkInfo })) ) ), O.getOrElse(() => ({ isPNOptInMessage: false, - cta1HasServiceNavigationLink: false, - cta2HasServiceNavigationLink: false + cta1LinksToPNService: false, + cta2LinksToPNService: false })) );