From dc9b14698e73d1cb13ecf669492620aa88bec50e Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Mon, 15 Apr 2024 16:58:35 +0200 Subject: [PATCH] chore: [IOBP-617] Add expired state to payment card component (#5674) > [!WARNING] > This PR depends on https://github.com/pagopa/io-app/pull/5663, https://github.com/pagopa/io-dev-api-server/pull/361 ## Short description This PR adds the error state to payment cards which are expired. ## List of changes proposed in this pull request - Removed `isError` from `PaymentCardSmallProps`, which is now based on payment expiry date. ## How to test With the` io-dev-api-server`, checkout this PR https://github.com/pagopa/io-dev-api-server/pull/361, enable the **New wallet section** FF from the profile screen in the IO app. Check that in the payments landing screen you can see an error card. ## Preview --------- Co-authored-by: Martino Cesari Tomba <60693085+forrest57@users.noreply.github.com> Co-authored-by: Mario Perrotta --- ts/features/design-system/core/DSCards.tsx | 20 +++++---- .../common/components/PaymentCardSmall.tsx | 21 ++++++--- .../__tests__/PaymentCardSmall.test.tsx | 4 +- .../PaymentCardSmall.test.tsx.snap | 2 +- ts/features/payments/common/utils/index.ts | 20 ++++++++- .../screens/PaymentsMethodDetailsScreen.tsx | 21 +-------- .../PaymentsHomeUserMethodsList.tsx | 19 +++----- .../__tests__/PaymentCardsCarousel.test.tsx | 3 +- ts/utils/__tests__/dates.test.ts | 44 ++++++++++++++++++- ts/utils/dates.ts | 14 +++--- 10 files changed, 105 insertions(+), 63 deletions(-) diff --git a/ts/features/design-system/core/DSCards.tsx b/ts/features/design-system/core/DSCards.tsx index 9e4b1991c35..e82163501f0 100644 --- a/ts/features/design-system/core/DSCards.tsx +++ b/ts/features/design-system/core/DSCards.tsx @@ -32,13 +32,13 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = { cards: [ { hpan: "9999", - isError: false, + expireDate: new Date(2021, 10), brand: "maestro", onPress }, { holderEmail: "test@test.it", - isError: true, + expireDate: new Date(2021, 10), onPress }, { @@ -52,7 +52,7 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = { }, { hpan: "9999", - isError: true, + expireDate: new Date(2021, 10), onPress }, { @@ -146,7 +146,11 @@ export const DSCards = () => ( - + @@ -163,7 +167,7 @@ export const DSCards = () => ( brand="pagoBancomat" bankName="Intesa San Paolo" onPress={onPress} - isError + expireDate={new Date(2021, 10)} /> @@ -177,7 +181,7 @@ export const DSCards = () => ( @@ -193,7 +197,7 @@ export const DSCards = () => ( bankName="Intesa San Paolo" brand="maestro" onPress={onPress} - isError + expireDate={new Date(2021, 10)} /> @@ -209,7 +213,7 @@ export const DSCards = () => ( holderName="Anna Verdi" holderPhone="+39 340 *** **62" onPress={onPress} - isError + expireDate={new Date(2021, 10)} /> diff --git a/ts/features/payments/common/components/PaymentCardSmall.tsx b/ts/features/payments/common/components/PaymentCardSmall.tsx index 2bacc3c953e..6e8d383ee5b 100644 --- a/ts/features/payments/common/components/PaymentCardSmall.tsx +++ b/ts/features/payments/common/components/PaymentCardSmall.tsx @@ -5,11 +5,14 @@ import { Icon, VSpacer } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; import * as React from "react"; import { StyleSheet, View } from "react-native"; import Placeholder from "rn-placeholder"; import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; import { WithTestID } from "../../../../types/WithTestID"; +import { isExpiredDate } from "../../../../utils/dates"; import { PaymentCardProps } from "./PaymentCard"; import { PaymentCardPressableBase } from "./PaymentCardPressableBase"; @@ -17,7 +20,6 @@ export type PaymentCardSmallProps = WithTestID< PaymentCardProps & { bankName?: string; onPress?: () => void; - isError?: boolean; accessibilityLabel?: string; } >; @@ -25,7 +27,6 @@ export type PaymentCardSmallProps = WithTestID< const PaymentCardSmall = ({ testID, onPress, - isError, accessibilityLabel, ...props }: PaymentCardSmallProps) => { @@ -61,16 +62,26 @@ const PaymentCardSmall = ({ return props.brand; }, [props]); + const isExpired = pipe( + props.expireDate, + O.fromNullable, + O.chainNullableK(isExpiredDate), + O.getOrElse(() => false) + ); + return ( - + - {isError && ( + {isExpired && ( {labelText} diff --git a/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx b/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx index a1e3e481579..084820b5dc1 100644 --- a/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx +++ b/ts/features/payments/common/components/__tests__/PaymentCardSmall.test.tsx @@ -44,9 +44,9 @@ describe("PaymentCardSmall", () => { const { queryByText, queryByTestId } = renderCard({ hpan: "9900", brand: "maestro", + expireDate: new Date(2023, 10), onPress: () => undefined, - testID, - isError: true + testID }); expect(queryByText("•••• 9900")).not.toBeNull(); expect(queryByTestId(`${testID}-errorIcon`)).not.toBeNull(); diff --git a/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap index 0d45a3982e5..d220816565a 100644 --- a/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap +++ b/ts/features/payments/common/components/__tests__/__snapshots__/PaymentCardSmall.test.tsx.snap @@ -360,7 +360,7 @@ exports[`PaymentCardSmall should match the snapshot 1`] = ` "padding": 16, "width": 127, }, - undefined, + false, ] } testID="PaymentCardSmallTestID" diff --git a/ts/features/payments/common/utils/index.ts b/ts/features/payments/common/utils/index.ts index 61d88efd94e..15a353f18b9 100644 --- a/ts/features/payments/common/utils/index.ts +++ b/ts/features/payments/common/utils/index.ts @@ -11,10 +11,11 @@ import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; import { WalletApplicationStatusEnum } from "../../../../../definitions/pagopa/walletv3/WalletApplicationStatus"; import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import { PaymentSupportStatus } from "../../../../types/paymentMethodCapabilities"; -import { isExpiredDate } from "../../../../utils/dates"; +import { getDateFromExpiryDate, isExpiredDate } from "../../../../utils/dates"; import { findFirstCaseInsensitive } from "../../../../utils/object"; import { WalletPaymentPspSortType } from "../../checkout/types"; import { UIWalletInfoDetails } from "../types/UIWalletInfoDetails"; +import { PaymentCardProps } from "../components/PaymentCard"; /** * A simple function to get the corresponding translated badge text, @@ -49,7 +50,7 @@ export const isPaymentMethodExpired = ( ): boolean => pipe( details?.expiryDate, - O.fromNullable, + O.chainNullableK(getDateFromExpiryDate), O.map(isExpiredDate), O.getOrElse(() => false) ); @@ -146,3 +147,18 @@ export const getSortedPspList = ( return _.orderBy(pspList, ["onUs", "taxPayerFee"]); } }; + +export const getPaymentCardPropsFromWalletInfo = ( + wallet: WalletInfo +): PaymentCardProps => { + const details = wallet.details as UIWalletInfoDetails; + + return { + hpan: details.lastFourDigits, + abiCode: "", // TODO IOBP-622 refactor payment card + brand: details.brand, + expireDate: getDateFromExpiryDate(details.expiryDate), + holderEmail: details.maskedEmail, + holderPhone: details.maskedNumber + }; +}; diff --git a/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx b/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx index ef2b65f0327..73805d65f77 100644 --- a/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx +++ b/ts/features/payments/details/screens/PaymentsMethodDetailsScreen.tsx @@ -2,15 +2,13 @@ import { VSpacer } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { RouteProp, useRoute } from "@react-navigation/native"; import * as React from "react"; -import { WalletInfo } from "../../../../../definitions/pagopa/walletv3/WalletInfo"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { isIdPayEnabledSelector } from "../../../../store/reducers/backendStatus"; -import { getDateFromExpiryDate } from "../../../../utils/dates"; import { capitalize } from "../../../../utils/strings"; import { idPayInitiativesFromInstrumentGet } from "../../../idpay/wallet/store/actions"; import { idPayAreInitiativesFromInstrumentLoadingSelector } from "../../../idpay/wallet/store/reducers"; -import { PaymentCardProps } from "../../common/components/PaymentCard"; import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { getPaymentCardPropsFromWalletInfo } from "../../common/utils"; import { PaymentsMethodDetailsBaseScreenComponent } from "../components/PaymentsMethodDetailsBaseScreenComponent"; import { PaymentsMethodDetailsDeleteButton } from "../components/PaymentsMethodDetailsDeleteButton"; import { PaymentsMethodDetailsErrorContent } from "../components/PaymentsMethodDetailsErrorContent"; @@ -66,7 +64,7 @@ const PaymentsMethodDetailsScreen = () => { if (pot.isSome(walletDetailsPot) && !isLoading) { const paymentMethod = walletDetailsPot.value; - const cardProps = getPaymentCardPropsFromWallet(paymentMethod); + const cardProps = getPaymentCardPropsFromWalletInfo(paymentMethod); const headerTitle = getCardHeaderTitle(paymentMethod.details); return ( @@ -95,19 +93,4 @@ const getCardHeaderTitle = (details?: UIWalletInfoDetails) => { return ""; }; -const getPaymentCardPropsFromWallet = ( - wallet: WalletInfo -): PaymentCardProps => { - const details = wallet.details as UIWalletInfoDetails; - - return { - hpan: details.lastFourDigits, - abiCode: details.abi, - brand: details.brand, - expireDate: getDateFromExpiryDate(details.expiryDate), - holderEmail: details.maskedEmail, - holderPhone: details.maskedNumber - }; -}; - export default PaymentsMethodDetailsScreen; diff --git a/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx b/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx index 46939d79d15..95a57ee0a76 100644 --- a/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx +++ b/ts/features/payments/home/components/PaymentsHomeUserMethodsList.tsx @@ -13,7 +13,7 @@ import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { PaymentCardSmallProps } from "../../common/components/PaymentCardSmall"; -import { UIWalletInfoDetails } from "../../common/types/UIWalletInfoDetails"; +import { getPaymentCardPropsFromWalletInfo } from "../../common/utils"; import { PaymentsMethodDetailsRoutes } from "../../details/navigation/routes"; import { PaymentsOnboardingRoutes } from "../../onboarding/navigation/routes"; import { getPaymentsWalletUserMethods } from "../../wallet/store/actions"; @@ -69,19 +69,10 @@ const PaymentsHomeUserMethodsList = ({ enforcedLoadingState }: Props) => { }; const userMethods = paymentMethods.map( - (method: WalletInfo): PaymentCardSmallProps => { - const details = method.details as UIWalletInfoDetails; - - return { - onPress: handleOnMethodPress(method.walletId), - abiCode: details.abi, - brand: details.brand, - bankName: details.bankName, - holderEmail: details.maskedEmail, - holderPhone: details.maskedNumber, - hpan: details.lastFourDigits - }; - } + (method: WalletInfo): PaymentCardSmallProps => ({ + ...getPaymentCardPropsFromWalletInfo(method), + onPress: handleOnMethodPress(method.walletId) + }) ); if (!isLoading && isEmpty) { diff --git a/ts/features/payments/home/components/__tests__/PaymentCardsCarousel.test.tsx b/ts/features/payments/home/components/__tests__/PaymentCardsCarousel.test.tsx index 44869aaddd3..4ecac09268c 100644 --- a/ts/features/payments/home/components/__tests__/PaymentCardsCarousel.test.tsx +++ b/ts/features/payments/home/components/__tests__/PaymentCardsCarousel.test.tsx @@ -17,14 +17,13 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = { cards: [ { hpan: "9900", - isError: false, brand: "maestro", onPress, testID: "card-1" }, { holderEmail: "test@test.it", - isError: true, + expireDate: new Date(2023, 10), onPress, testID: "card-2" }, diff --git a/ts/utils/__tests__/dates.test.ts b/ts/utils/__tests__/dates.test.ts index 2d44f9b2a0f..14d78bb135a 100644 --- a/ts/utils/__tests__/dates.test.ts +++ b/ts/utils/__tests__/dates.test.ts @@ -1,7 +1,12 @@ -import { getMonth, getYear } from "date-fns"; +import { format, getMonth, getYear } from "date-fns"; import * as E from "fp-ts/lib/Either"; import MockDate from "mockdate"; -import { getExpireStatus, isExpired } from "../dates"; +import { + getDateFromExpiryDate, + getExpireStatus, + isExpired, + isExpiredDate +} from "../dates"; describe("getExpireStatus", () => { it("should be VALID", () => { @@ -73,3 +78,38 @@ describe("getExpireStatus", () => { ).toEqual(E.right(false)); }); }); + +describe("isExpiredDate", () => { + it("should mark the card as valid, not expired", () => { + const today = new Date(); + expect(isExpiredDate(today)).toEqual(false); + }); + + it("should mark the card as expired, not valid", () => { + const today = new Date(); + expect( + isExpiredDate(new Date(today.getFullYear(), today.getMonth() - 1)) + ).toEqual(true); + }); +}); + +describe("getDateFromExpiryDate", () => { + it("should return undefined is invalid input", () => { + const date = getDateFromExpiryDate("invalid"); + expect(date).toBeUndefined(); + }); + + it("should mark the card as valid, not expired", () => { + const today = new Date(); + const date = getDateFromExpiryDate(format(today, "YYYYMM")); + expect(isExpiredDate(date!)).toEqual(false); + }); + + it("should mark the card as expired, not valid", () => { + const today = new Date(); + const date = getDateFromExpiryDate( + format(new Date(today.getFullYear(), today.getMonth() - 1), "YYYYMM") + ); + expect(isExpiredDate(date!)).toEqual(true); + }); +}); diff --git a/ts/utils/dates.ts b/ts/utils/dates.ts index faba844cd94..ac7d69af32b 100644 --- a/ts/utils/dates.ts +++ b/ts/utils/dates.ts @@ -227,16 +227,13 @@ export const isExpired = ( }; /** - * This function returns true or false is the provided expiryDate in format "YYYYMM" is expired or not + * This function returns true or false is the provided expiryDate is expired or not * @param expiryDate */ -export const isExpiredDate = (expiryDate: string): boolean => { - const year = +expiryDate.slice(0, 4); - const month = +expiryDate.slice(4, 6); +export const isExpiredDate = (expiryDate: Date): boolean => { const now = new Date(); - const nowYearMonth = new Date(now.getFullYear(), now.getMonth() + 1); - const cardExpirationDate = new Date(year, month); - return nowYearMonth > cardExpirationDate; + const nowYearMonth = new Date(now.getFullYear(), now.getMonth()); + return nowYearMonth > expiryDate; }; /** @@ -329,7 +326,8 @@ export const getDateFromExpiryDate = (expiryDate: string): Date | undefined => { try { const year = +expiryDate.slice(0, 4); const month = +expiryDate.slice(4, 6); - return new Date(year, month - 1); + const date = new Date(year, month - 1); + return isNaN(date.getDate()) ? undefined : date; } catch { return undefined; }