Skip to content

Commit

Permalink
chore: [IOBP-617] Add expired state to payment card component (#5674)
Browse files Browse the repository at this point in the history
> [!WARNING]
> This PR depends on #5663,
pagopa/io-dev-api-server#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
pagopa/io-dev-api-server#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

<img
src="https://github.com/pagopa/io-app/assets/6160324/4888a16a-1ce5-4b83-a440-41f73908c593"
width="250" />

---------

Co-authored-by: Martino Cesari Tomba <[email protected]>
Co-authored-by: Mario Perrotta <[email protected]>
  • Loading branch information
3 people authored Apr 15, 2024
1 parent 880c2a0 commit dc9b146
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 63 deletions.
20 changes: 12 additions & 8 deletions ts/features/design-system/core/DSCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = {
cards: [
{
hpan: "9999",
isError: false,
expireDate: new Date(2021, 10),
brand: "maestro",
onPress
},
{
holderEmail: "[email protected]",
isError: true,
expireDate: new Date(2021, 10),
onPress
},
{
Expand All @@ -52,7 +52,7 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = {
},
{
hpan: "9999",
isError: true,
expireDate: new Date(2021, 10),
onPress
},
{
Expand Down Expand Up @@ -146,7 +146,11 @@ export const DSCards = () => (
<View style={styles.content}>
<PaymentCardSmall hpan="9900" brand="maestro" onPress={onPress} />
<HSpacer size={16} />
<PaymentCardSmall hpan="9900" brand="maestro" isError />
<PaymentCardSmall
hpan="9900"
brand="maestro"
expireDate={new Date(2021, 10)}
/>
</View>
</DSComponentViewerBox>
<DSComponentViewerBox name="PagoBANCOMAT">
Expand All @@ -163,7 +167,7 @@ export const DSCards = () => (
brand="pagoBancomat"
bankName="Intesa San Paolo"
onPress={onPress}
isError
expireDate={new Date(2021, 10)}
/>
</View>
</DSComponentViewerBox>
Expand All @@ -177,7 +181,7 @@ export const DSCards = () => (
<PaymentCardSmall
holderEmail="anna_v********@**hoo.it"
onPress={onPress}
isError
expireDate={new Date(2021, 10)}
/>
</View>
</DSComponentViewerBox>
Expand All @@ -193,7 +197,7 @@ export const DSCards = () => (
bankName="Intesa San Paolo"
brand="maestro"
onPress={onPress}
isError
expireDate={new Date(2021, 10)}
/>
</View>
</DSComponentViewerBox>
Expand All @@ -209,7 +213,7 @@ export const DSCards = () => (
holderName="Anna Verdi"
holderPhone="+39 340 *** **62"
onPress={onPress}
isError
expireDate={new Date(2021, 10)}
/>
</View>
</DSComponentViewerBox>
Expand Down
21 changes: 16 additions & 5 deletions ts/features/payments/common/components/PaymentCardSmall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@ 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";

export type PaymentCardSmallProps = WithTestID<
PaymentCardProps & {
bankName?: string;
onPress?: () => void;
isError?: boolean;
accessibilityLabel?: string;
}
>;

const PaymentCardSmall = ({
testID,
onPress,
isError,
accessibilityLabel,
...props
}: PaymentCardSmallProps) => {
Expand Down Expand Up @@ -61,16 +62,26 @@ const PaymentCardSmall = ({
return props.brand;
}, [props]);

const isExpired = pipe(
props.expireDate,
O.fromNullable,
O.chainNullableK(isExpiredDate),
O.getOrElse(() => false)
);

return (
<PaymentCardPressableBase
onPress={onPress}
testID={`${testID}-pressable`}
accessibilityLabel={accessibilityLabel}
>
<View style={[styles.card, isError && styles.cardError]} testID={testID}>
<View
style={[styles.card, isExpired && styles.cardError]}
testID={testID}
>
<View style={[IOStyles.rowSpaceBetween, IOStyles.alignCenter]}>
<LogoPaymentWithFallback brand={iconName} size={24} />
{isError && (
{isExpired && (
<Icon
testID={`${testID}-errorIcon`}
name="errorFilled"
Expand All @@ -84,7 +95,7 @@ const PaymentCardSmall = ({
ellipsizeMode="tail"
weight="Regular"
numberOfLines={1}
color={isError ? "error-850" : "grey-700"}
color={isExpired ? "error-850" : "grey-700"}
>
{labelText}
</Chip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ exports[`PaymentCardSmall should match the snapshot 1`] = `
"padding": 16,
"width": 127,
},
undefined,
false,
]
}
testID="PaymentCardSmallTestID"
Expand Down
20 changes: 18 additions & 2 deletions ts/features/payments/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,7 +50,7 @@ export const isPaymentMethodExpired = (
): boolean =>
pipe(
details?.expiryDate,
O.fromNullable,
O.chainNullableK(getDateFromExpiryDate),
O.map(isExpiredDate),
O.getOrElse(() => false)
);
Expand Down Expand Up @@ -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
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ const cardsDataForCarousel: PaymentCardsCarouselProps = {
cards: [
{
hpan: "9900",
isError: false,
brand: "maestro",
onPress,
testID: "card-1"
},
{
holderEmail: "[email protected]",
isError: true,
expireDate: new Date(2023, 10),
onPress,
testID: "card-2"
},
Expand Down
44 changes: 42 additions & 2 deletions ts/utils/__tests__/dates.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
14 changes: 6 additions & 8 deletions ts/utils/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit dc9b146

Please sign in to comment.