diff --git a/locales/en/index.yml b/locales/en/index.yml index f6f9424d41c..39b9ed94064 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -1894,7 +1894,7 @@ wallet: defaultName: Gestore pspTitle: Gestore pspSortButton: Ordina - featuredReason: Perché sei già cliente + featuredReason: Sei già cliente continueButton: Continua sortBottomSheet: default: Default diff --git a/locales/it/index.yml b/locales/it/index.yml index 1525b138425..d4e93d9deca 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -1894,7 +1894,7 @@ wallet: defaultName: Gestore pspTitle: Gestore pspSortButton: Ordina - featuredReason: Perché sei già cliente + featuredReason: Sei già cliente continueButton: Continua sortBottomSheet: default: Default diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index 3cf2ab01ae0..4f281fe79e4 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -1,6 +1,6 @@ #!/bin/bash -IO_BACKEND_VERSION=v16.4.0-RELEASE +IO_BACKEND_VERSION=v16.7.3-RELEASE # need to change after merge on io-services-metadata IO_SERVICES_METADATA_VERSION=1.0.55 diff --git a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx index 3fadfb9cbc7..62339d0cc65 100644 --- a/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx +++ b/ts/features/bonus/cgn/components/merchants/CgnMerchantsListView.tsx @@ -22,7 +22,11 @@ export const CgnMerchantListViewRenderItem = )} diff --git a/ts/features/idpay/common/components/Table.tsx b/ts/features/idpay/common/components/Table.tsx deleted file mode 100644 index 2001a196722..00000000000 --- a/ts/features/idpay/common/components/Table.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Fragment, ReactNode } from "react"; - -import { StyleSheet, View } from "react-native"; -import { Body, Divider, H6, WithTestID } from "@pagopa/io-app-design-system"; - -export type TableRow = WithTestID<{ - label: string; - value: string | ReactNode; -}>; - -type TableProps = { - title: string; - rows: ReadonlyArray; -}; - -const renderTable = (data: ReadonlyArray): ReactNode => - data.map((item, index) => { - const isLast = data.length === index + 1; - - return ( - - - {item.label} - {item.value} - - {!isLast && } - - ); - }); - -export const Table = (props: TableProps) => ( - <> - -
{props.title}
-
- {renderTable(props.rows)} - -); - -const styles = StyleSheet.create({ - sectionHeader: { - paddingTop: 16, - paddingBottom: 8 - }, - infoRow: { - flex: 1, - flexDirection: "row", - justifyContent: "space-between", - paddingVertical: 12 - } -}); diff --git a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx index a8ff3d5750e..ee5e4d098b1 100644 --- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx +++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx @@ -1,10 +1,20 @@ -import { Body, BodySmall, VSpacer } from "@pagopa/io-app-design-system"; +import { + Body, + BodySmall, + Divider, + ListItemHeader, + ListItemInfo, + ListItemTransaction, + VSpacer, + WithTestID +} from "@pagopa/io-app-design-system"; import { NonEmptyString } from "@pagopa/ts-commons/lib/strings"; import { useNavigation } from "@react-navigation/native"; import { sequenceS } from "fp-ts/lib/Apply"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { StyleSheet, View } from "react-native"; +import { ReactNode } from "react"; +import { View } from "react-native"; import Placeholder from "rn-placeholder"; import { InitiativeDTO, @@ -16,7 +26,6 @@ import { RewardValueDTO, RewardValueTypeEnum } from "../../../../../definitions/idpay/RewardValueDTO"; -import { IOStyles } from "../../../../components/core/variables/IOStyles"; import I18n from "../../../../i18n"; import { AppParamsList, @@ -24,7 +33,6 @@ import { } from "../../../../navigation/params/AppParamsList"; import { format } from "../../../../utils/dates"; import { SERVICES_ROUTES } from "../../../services/common/navigation/routes"; -import { Table, TableRow } from "../../common/components/Table"; import { formatNumberCurrencyOrDefault } from "../../common/utils/strings"; import { IdPayUnsubscriptionRoutes } from "../../unsubscription/navigation/routes"; import { @@ -32,6 +40,11 @@ import { InitiativeRulesInfoBoxSkeleton } from "./InitiativeRulesInfoBox"; +type TableRow = WithTestID<{ + label: string; + value: string | ReactNode; +}>; + export type BeneficiaryDetailsProps = | { isLoading?: false; @@ -209,80 +222,95 @@ const BeneficiaryDetailsContent = (props: BeneficiaryDetailsProps) => { ); }; + const summaryData = [ + { + label: I18n.t("idpay.initiative.beneficiaryDetails.status"), + value: statusString, + testID: "statusTestID" + }, + { + label: I18n.t("idpay.initiative.beneficiaryDetails.endDate"), + value: endDateString, + testID: "endDateTestID" + }, + { + label: I18n.t("idpay.initiative.beneficiaryDetails.amount"), + value: formatNumberCurrencyOrDefault(initiativeDetails.amountCents), + testID: "amountTestID" + }, + ...getTypeDependantTableRows() + ]; + + const enrollmentData = [ + { + label: I18n.t("idpay.initiative.beneficiaryDetails.enrollmentDate"), + value: onboardingDateString, + testID: "onboardingDateTestID" + }, + { + label: I18n.t("idpay.initiative.beneficiaryDetails.protocolNumber"), + value: "-", + testID: "protocolTestID" + } + ]; + + const spendingRulesData = [ + { + label: I18n.t("idpay.initiative.beneficiaryDetails.spendFrom"), + value: fruitionStartDateString, + testID: "fruitionStartDateTestID" + }, + { + label: I18n.t("idpay.initiative.beneficiaryDetails.spendTo"), + value: fruitionEndDateString, + testID: "fruitionEndDateTestID" + }, + rewardRuleRow + ]; + + const renderTableRow = (data: Array) => + data.map((row, i) => ( + <> + + {i !== data.length - 1 && } + + )); + return ( <> {ruleInfoBox} - + {renderTableRow(summaryData)} {lastUpdateString} -
- -
- - - - {I18n.t("idpay.initiative.beneficiaryDetails.buttons.privacy")} - - - - - {I18n.t("idpay.initiative.beneficiaryDetails.buttons.unsubscribe", { - initiativeName - })} - - + {renderTableRow(enrollmentData)} + + + {I18n.t("idpay.initiative.beneficiaryDetails.buttons.privacy")} + + + + {I18n.t("idpay.initiative.beneficiaryDetails.buttons.unsubscribe", { + initiativeName + })} + ); @@ -296,22 +324,17 @@ const BeneficiaryDetailsContentSkeleton = () => ( - {Array.from({ length: 5 }).map((_, j) => ( + {Array.from({ length: 2 }).map((_, j) => ( - - - - + ))} @@ -320,10 +343,4 @@ const BeneficiaryDetailsContentSkeleton = () => ( ); -const styles = StyleSheet.create({ - linkRow: { - paddingVertical: 16 - } -}); - export { BeneficiaryDetailsContent }; diff --git a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx index ceec82a9b83..8417f781888 100644 --- a/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeDiscountSettingsComponent.tsx @@ -1,4 +1,4 @@ -import { H6, ListItemNav, VSpacer } from "@pagopa/io-app-design-system"; +import { ListItemHeader, ListItemNav } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/core"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; @@ -83,12 +83,11 @@ const InitiativeDiscountSettingsComponent = (props: Props) => { return ( -
- {I18n.t( + - + /> {instrumentsSettingsButton} ); diff --git a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx index 9b2c8df12b2..755a2a4bc3e 100644 --- a/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeRefundSettingsComponent.tsx @@ -1,8 +1,7 @@ import { - H6, + ListItemHeader, ListItemNav, - ListItemNavAlert, - VSpacer + ListItemNavAlert } from "@pagopa/io-app-design-system"; import { useNavigation } from "@react-navigation/core"; import * as O from "fp-ts/lib/Option"; @@ -170,12 +169,11 @@ const InitiativeRefundSettingsComponent = (props: Props) => { return ( -
- {I18n.t( + - + /> {instrumentsSettingsButton} {ibanSettingsButton} diff --git a/ts/features/idpay/details/components/InitiativeTimelineComponent.tsx b/ts/features/idpay/details/components/InitiativeTimelineComponent.tsx index 8f7f71cdd62..53cf39fdbf1 100644 --- a/ts/features/idpay/details/components/InitiativeTimelineComponent.tsx +++ b/ts/features/idpay/details/components/InitiativeTimelineComponent.tsx @@ -3,7 +3,8 @@ import { Divider, H6, BodySmall, - VSpacer + VSpacer, + ListItemHeader } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { useNavigation } from "@react-navigation/native"; @@ -73,8 +74,20 @@ const InitiativeTimelineComponent = ({ initiativeId, size = 3 }: Props) => { return ( - - + {renderTimelineContent()} {detailsBottomSheet.bottomSheet} diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index ac76218a535..85d9f501ed7 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -203,18 +203,15 @@ const IdPayInitiativeDetailsScreen = () => { case InitiativeRewardTypeEnum.DISCOUNT: return ( - - - ); diff --git a/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx b/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx index 44d6a000704..fe05d3013f3 100644 --- a/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx +++ b/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx @@ -1,19 +1,18 @@ import { Alert, - Body, - H6, - HSpacer, + Divider, + ListItemHeader, + ListItemInfo, ListItemInfoCopy, VSpacer } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { StyleSheet, View } from "react-native"; +import { View } from "react-native"; import { TransactionDetailDTO, StatusEnum as TransactionStatusEnum } from "../../../../../definitions/idpay/TransactionDetailDTO"; -import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; import I18n from "../../../../i18n"; import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard"; @@ -68,65 +67,46 @@ const TimelineDiscountTransactionDetailsComponent = (props: Props) => { {statusAlertComponent} - - - {I18n.t( - "idpay.initiative.operationDetails.discount.details.labels.totalAmount" - )} - - {formattedAmount} - - - - {I18n.t( - "idpay.initiative.operationDetails.discount.details.labels.idpayAmount" - )} - - - {formatNumberCentsToAmount(transaction.accruedCents, true)} - - - - -
- {I18n.t("idpay.initiative.operationDetails.transaction.infoTitle")} -
- - - - {I18n.t( - "idpay.initiative.operationDetails.discount.details.labels.business" - )} - - - - {businessName} - - - - - {I18n.t( - "idpay.initiative.operationDetails.discount.details.labels.status" - )} - - - {I18n.t( - `idpay.initiative.operationDetails.discount.labels.${transaction.status}` - )} - - - - - {I18n.t("idpay.initiative.operationDetails.transaction.date")} - - - {format(transaction.operationDate, "DD MMM YYYY, HH:mm")} - - + + + + + + + + + + { ); }; -const styles = StyleSheet.create({ - detailRow: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - paddingVertical: 8 - } -}); - export { TimelineDiscountTransactionDetailsComponent }; diff --git a/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx b/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx index 3f780ac2913..31298be9504 100644 --- a/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx +++ b/ts/features/idpay/timeline/components/TimelineRefundDetailsComponent.tsx @@ -1,8 +1,9 @@ import { useBottomSheet } from "@gorhom/bottom-sheet"; import { Alert, - Badge, - Body, + Divider, + ListItemHeader, + ListItemInfo, ListItemInfoCopy, VSpacer } from "@pagopa/io-app-design-system"; @@ -10,13 +11,11 @@ import { CommonActions } from "@react-navigation/native"; import { format } from "date-fns"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { StyleSheet, View } from "react-native"; import { RefundDetailDTO } from "../../../../../definitions/idpay/RefundDetailDTO"; import { OperationTypeEnum } from "../../../../../definitions/idpay/RefundOperationDTO"; import I18n from "../../../../i18n"; import NavigationService from "../../../../navigation/NavigationService"; import { useIOSelector } from "../../../../store/hooks"; -import themeVariables from "../../../../theme/variables"; import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard"; import { formatNumberCentsToAmount } from "../../../../utils/stringBuilder"; import { IdPayConfigurationRoutes } from "../../configuration/navigation/routes"; @@ -84,44 +83,41 @@ const TimelineRefundDetailsComponent = (props: Props) => { return ( <> {rejectedAlertComponent} - - {I18n.t("idpay.initiative.operationDetails.refund.iban")} - {refund.iban} - - - {I18n.t("idpay.initiative.operationDetails.refund.amount")} - {formattedAmount} - - - - {I18n.t("idpay.initiative.operationDetails.refund.resultLabel")} - - {refund.operationType === OperationTypeEnum.REJECTED_REFUND ? ( - - ) : ( - + + + + - )} - - - {I18n.t("idpay.initiative.operationDetails.refund.period")} - {getRefundPeriodDateString(refund)} - - - Data rimborso - - {format(refund.operationDate, "DD MMM YYYY, HH:mm")} - - + ) + } + }} + /> + + + + { ); }; -const styles = StyleSheet.create({ - detailRow: { - flexDirection: "row", - justifyContent: "space-between", - paddingTop: themeVariables.spacerSmallHeight, - paddingBottom: themeVariables.spacerSmallHeight - } -}); - export { TimelineRefundDetailsComponent }; diff --git a/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx b/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx index 6dc23e6b25a..d4d9df463d0 100644 --- a/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx +++ b/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx @@ -1,21 +1,20 @@ import { Alert, - Body, - H6, - HSpacer, + Divider, + IOLogoPaymentType, + ListItemHeader, + ListItemInfo, ListItemInfoCopy, VSpacer } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { StyleSheet, View } from "react-native"; +import { View } from "react-native"; import { TransactionDetailDTO, OperationTypeEnum as TransactionTypeEnum } from "../../../../../definitions/idpay/TransactionDetailDTO"; -import ItemSeparatorComponent from "../../../../components/ItemSeparatorComponent"; import { IOStyles } from "../../../../components/core/variables/IOStyles"; -import { LogoPaymentWithFallback } from "../../../../components/ui/utils/components/LogoPaymentWithFallback"; import I18n from "../../../../i18n"; import { clipboardSetStringWithFeedback } from "../../../../utils/clipboard"; import { format } from "../../../../utils/dates"; @@ -65,59 +64,47 @@ const TimelineTransactionDetailsComponent = (props: Props) => { return ( {reversalAlertComponent} - - - {I18n.t("idpay.initiative.operationDetails.transaction.instrument")} - - - - - - {I18n.t("idpay.initiative.operationDetails.transaction.maskedPan", { - lastDigits: transaction.maskedPan - })} - - - - - - {I18n.t("idpay.initiative.operationDetails.transaction.amountLabel")} - - {formattedAmount} - - - - {I18n.t( - "idpay.initiative.operationDetails.transaction.accruedAmountLabel" - )} - - {formattedAccrued} - - - -
- {I18n.t("idpay.initiative.operationDetails.transaction.infoTitle")} -
- - - - {I18n.t("idpay.initiative.operationDetails.transaction.date")} - - - {format(transaction.operationDate, "DD MMM YYYY, HH:mm")} - - - - - {I18n.t("idpay.initiative.operationDetails.transaction.circuit")} - - - {getLabelForCircuitType(transaction.circuitType)} - - + + + + + + + + + + { clipboardSetStringWithFeedback(idTrxAcquirer); }} /> + { ); }; -const styles = StyleSheet.create({ - detailRow: { - flexDirection: "row", - justifyContent: "space-between", - paddingVertical: 8 - } -}); - export { TimelineTransactionDetailsComponent }; diff --git a/ts/features/ingress/analytics/index.ts b/ts/features/ingress/analytics/index.ts index 8e8acd7efb1..a2cc1e94eec 100644 --- a/ts/features/ingress/analytics/index.ts +++ b/ts/features/ingress/analytics/index.ts @@ -1,13 +1,27 @@ import { mixpanelTrack } from "../../../mixpanel"; +import { buildEventProperties } from "../../../utils/analytics"; export function trackIngressServicesSlowDown() { - void mixpanelTrack("INGRESS_SERVICES_SLOW_DOWN"); + void mixpanelTrack( + "INGRESS_SERVICES_SLOW_DOWN", + buildEventProperties("KO", undefined) + ); } export function trackIngressTimeout() { - void mixpanelTrack("INGRESS_TIMEOUT"); + void mixpanelTrack("INGRESS_TIMEOUT", buildEventProperties("KO", undefined)); } export function trackIngressCdnSystemError() { - void mixpanelTrack("INGRESS_CDN_SYSTEM_ERROR"); + void mixpanelTrack( + "INGRESS_CDN_SYSTEM_ERROR", + buildEventProperties("KO", undefined) + ); +} + +export function trackIngressNoInternetConnection() { + void mixpanelTrack( + "INGRESS_NO_INTERNET_CONNECTION", + buildEventProperties("KO", undefined) + ); } diff --git a/ts/features/ingress/screens/IngressScreen.tsx b/ts/features/ingress/screens/IngressScreen.tsx index 63faa85a92f..e47521df076 100644 --- a/ts/features/ingress/screens/IngressScreen.tsx +++ b/ts/features/ingress/screens/IngressScreen.tsx @@ -21,6 +21,7 @@ import ModalSectionStatusComponent from "../../../components/SectionStatus/modal import { isMixpanelInitializedSelector } from "../../mixpanel/store/selectors"; import { trackIngressCdnSystemError, + trackIngressNoInternetConnection, trackIngressServicesSlowDown, trackIngressTimeout } from "../analytics"; @@ -84,14 +85,7 @@ export const IngressScreen = () => { }, [dispatch]); if (netInfo && !netInfo.isConnected) { - return ( - - ); + return ; } if (showBlockingScreen) { @@ -114,6 +108,26 @@ export const IngressScreen = () => { ); }; +const IngressScreenNoInternetConnection = memo(() => { + const isMixpanelEnabled = useIOSelector(isMixpanelEnabledSelector); + const isMixpanelInitialized = useIOSelector(isMixpanelInitializedSelector); + + useEffect(() => { + if (isMixpanelInitialized && isMixpanelEnabled !== false) { + void trackIngressNoInternetConnection(); + } + }, [isMixpanelEnabled, isMixpanelInitialized]); + + return ( + + ); +}); + const IngressScreenBlockingError = memo(() => { const operationRef = useRef(null); const isBackendStatusLoaded = useIOSelector(isBackendStatusLoadedSelector); diff --git a/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx index 31fce333352..cc1424cea53 100644 --- a/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx +++ b/ts/features/payments/checkout/screens/WalletPaymentPickPspScreen.tsx @@ -11,7 +11,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { useFocusEffect } from "@react-navigation/native"; import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; import I18n from "../../../../i18n"; import { useIONavigation } from "../../../../navigation/params/AppParamsList"; @@ -154,6 +154,7 @@ const WalletPaymentPickPspScreen = () => { () => ({ type: "buttonLink", componentProps: { + testID: "wallet-payment-pick-psp-sort-button", label: I18n.t("wallet.payment.psp.pspSortButton"), accessibilityLabel: I18n.t("wallet.payment.psp.pspSortButton"), onPress: present diff --git a/ts/features/payments/checkout/screens/__tests__/WalletPaymentPickPspScreen.test.tsx b/ts/features/payments/checkout/screens/__tests__/WalletPaymentPickPspScreen.test.tsx new file mode 100644 index 00000000000..7d573bdcd40 --- /dev/null +++ b/ts/features/payments/checkout/screens/__tests__/WalletPaymentPickPspScreen.test.tsx @@ -0,0 +1,153 @@ +import { fireEvent } from "@testing-library/react-native"; +import { View } from "react-native"; +import { createStore } from "redux"; +import configureMockStore from "redux-mock-store"; +import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; +import { PaymentMethodStatusEnum } from "../../../../../../definitions/pagopa/ecommerce/PaymentMethodStatus"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import I18n from "../../../../../i18n"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { useIOBottomSheetAutoresizableModal } from "../../../../../utils/hooks/bottomSheet"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { PaymentsCheckoutRoutes } from "../../navigation/routes"; +import { paymentsCalculatePaymentFeesAction } from "../../store/actions/networking"; +import { WalletPaymentPickPspScreen } from "../WalletPaymentPickPspScreen"; + +jest.mock("../../analytics"); +jest.mock("../../../../../utils/hooks/bottomSheet"); + +const mockNavigation = { + navigate: jest.fn(), + setOptions: jest.fn() +}; + +jest.mock("@react-navigation/native", () => ({ + ...jest.requireActual("@react-navigation/native"), + useNavigation: () => mockNavigation, + useRoute: () => ({ + params: { rptId: "1234567890" as RptId } + }) +})); + +const CHEAPER_VALUE = 123; +const MIDDLE_VALUE = 456; +const EXPENSIVE_VALUE = 789; +const MOCKED_PSP_LIST: ReadonlyArray = [ + { + idPsp: "1", + abi: "01010", + pspBusinessName: "BANCO di NAPOLI", + taxPayerFee: CHEAPER_VALUE, + primaryCiIncurredFee: CHEAPER_VALUE, + idBundle: "A" + }, + { + idPsp: "2", + abi: "01015", + pspBusinessName: "Banco di Sardegna", + taxPayerFee: MIDDLE_VALUE, + primaryCiIncurredFee: MIDDLE_VALUE, + idBundle: "B", + onUs: true + }, + { + idPsp: "3", + abi: "03015", + pspBusinessName: "FINECO", + taxPayerFee: EXPENSIVE_VALUE, + primaryCiIncurredFee: EXPENSIVE_VALUE, + idBundle: "C" + } +]; + +const globalState = appReducer(undefined, applicationChangeState("active")); +const mockStore = configureMockStore(); +const mockModal = { + present: jest.fn(), + dismiss: jest.fn(), + bottomSheet: +}; +const mockedUseIOBottomSheetAutoresizableModal = + useIOBottomSheetAutoresizableModal as jest.Mock; +mockedUseIOBottomSheetAutoresizableModal.mockReturnValue(mockModal); + +describe("WalletPaymentPickPspScreen", () => { + const renderComponent = () => { + const state = mockStore(globalState); + const store = createStore(appReducer, state as any); + return { + ...renderScreenWithNavigationStoreContext( + WalletPaymentPickPspScreen, + PaymentsCheckoutRoutes.PAYMENT_CHECKOUT_MAKE, + {}, + store + ), + store + }; + }; + + it("renders the main content with the list content if psp list is available", () => { + const { getAllByText, store } = renderComponent(); + + store.dispatch( + paymentsCalculatePaymentFeesAction.success({ + bundles: MOCKED_PSP_LIST, + asset: "MOCK", + paymentMethodDescription: "MOCK", + paymentMethodName: "MOCK", + paymentMethodStatus: PaymentMethodStatusEnum.ENABLED + }) + ); + + expect(getAllByText("BANCO di NAPOLI")).toBeTruthy(); + }); + + it("shows the featured reason if there is a psp with the onUs flag", () => { + const { getByText, store } = renderComponent(); + store.dispatch( + paymentsCalculatePaymentFeesAction.success({ + bundles: MOCKED_PSP_LIST, + asset: "MOCK", + paymentMethodDescription: "MOCK", + paymentMethodName: "MOCK", + paymentMethodStatus: PaymentMethodStatusEnum.ENABLED + }) + ); + expect(getByText(I18n.t("wallet.payment.psp.featuredReason"))).toBeTruthy(); + }); + + it("doesn't show the featured reason if there is not a psp with the onUs flag", () => { + const { queryByText, store } = renderComponent(); + store.dispatch( + paymentsCalculatePaymentFeesAction.success({ + bundles: MOCKED_PSP_LIST.map(psp => { + const { onUs, ...rest } = psp; + return rest; + }), + asset: "MOCK", + paymentMethodDescription: "MOCK", + paymentMethodName: "MOCK", + paymentMethodStatus: PaymentMethodStatusEnum.ENABLED + }) + ); + expect(queryByText(I18n.t("wallet.payment.psp.featuredReason"))).toBeNull(); + }); + + it("presents bottom sheet press the sort button", () => { + const { getByTestId, store } = renderComponent(); + store.dispatch( + paymentsCalculatePaymentFeesAction.success({ + bundles: MOCKED_PSP_LIST, + asset: "MOCK", + paymentMethodDescription: "MOCK", + paymentMethodName: "MOCK", + paymentMethodStatus: PaymentMethodStatusEnum.ENABLED + }) + ); + const sortButton = getByTestId("wallet-payment-pick-psp-sort-button"); + fireEvent.press(sortButton); + expect(mockModal.present).toHaveBeenCalled(); + }); +}); diff --git a/ts/features/payments/checkout/store/reducers/index.ts b/ts/features/payments/checkout/store/reducers/index.ts index 61f742e1daf..fc170b5303a 100644 --- a/ts/features/payments/checkout/store/reducers/index.ts +++ b/ts/features/payments/checkout/store/reducers/index.ts @@ -192,9 +192,9 @@ const reducer = ( // Bundles are stored sorted by default sort rule const sortedBundles = getSortedPspList(bundles, "default"); - // Automatically select PSP if only 1 received or with `onUs` property + // Automatically select PSP if only 1 received const preselectedPsp = - sortedBundles.length === 1 || sortedBundles[0]?.onUs + sortedBundles.length === 1 ? O.some(sortedBundles[0]) : state.selectedPsp; diff --git a/ts/features/payments/common/utils/__tests__/index.test.ts b/ts/features/payments/common/utils/__tests__/index.test.ts index 9dbc10b3273..f82822a9668 100644 --- a/ts/features/payments/common/utils/__tests__/index.test.ts +++ b/ts/features/payments/common/utils/__tests__/index.test.ts @@ -1,5 +1,6 @@ import { format } from "date-fns"; -import { isPaymentMethodExpired } from ".."; +import { getSortedPspList, isPaymentMethodExpired } from ".."; +import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; describe("isPaymentMethodExpired", () => { it("should return true if payment method is expired", () => { @@ -23,3 +24,64 @@ describe("isPaymentMethodExpired", () => { expect(result).toBeFalsy(); }); }); + +describe("getSortedPspList", () => { + const CHEAPER_VALUE = 123; + const MIDDLE_VALUE = 456; + const EXPENSIVE_VALUE = 789; + const MOCKED_PSP_LIST: ReadonlyArray = [ + { + idPsp: "1", + abi: "01010", + pspBusinessName: "BANCO di NAPOLI", + taxPayerFee: CHEAPER_VALUE, + primaryCiIncurredFee: CHEAPER_VALUE, + idBundle: "A" + }, + { + idPsp: "2", + abi: "01015", + pspBusinessName: "Banco di Sardegna", + taxPayerFee: MIDDLE_VALUE, + primaryCiIncurredFee: MIDDLE_VALUE, + idBundle: "B", + onUs: true + }, + { + idPsp: "3", + abi: "03015", + pspBusinessName: "FINECO", + taxPayerFee: EXPENSIVE_VALUE, + primaryCiIncurredFee: EXPENSIVE_VALUE, + idBundle: "C" + } + ]; + + it("should return as first element the element with onUs flag if default sorting", () => { + const result = getSortedPspList(MOCKED_PSP_LIST, "default"); + expect(result[0].onUs).toBe(true); + }); + + it("should return the list by amount from the cheaper to the more expensive if amount sorting", () => { + const result = getSortedPspList(MOCKED_PSP_LIST, "amount"); + expect(result[0].taxPayerFee).toBe(CHEAPER_VALUE); + expect(result[1].taxPayerFee).toBe(MIDDLE_VALUE); + expect(result[2].taxPayerFee).toBe(EXPENSIVE_VALUE); + }); + + it("should return the list sorted by name if name sorting", () => { + const result = getSortedPspList(MOCKED_PSP_LIST, "name"); + expect(result[0].pspBusinessName).toBe("BANCO di NAPOLI"); + expect(result[1].pspBusinessName).toBe("Banco di Sardegna"); + expect(result[2].pspBusinessName).toBe("FINECO"); + }); + + it("should return the exact same list if default sorting and not present the onUs flag", () => { + const MOCKED_PSP_LIST_WITHOUT_ONUS = MOCKED_PSP_LIST.map(psp => { + const { onUs, ...rest } = psp; + return rest; + }); + const result = getSortedPspList(MOCKED_PSP_LIST_WITHOUT_ONUS, "default"); + expect(result).toEqual(MOCKED_PSP_LIST_WITHOUT_ONUS); + }); +}); diff --git a/ts/features/payments/common/utils/index.ts b/ts/features/payments/common/utils/index.ts index a2b1a36fe6c..dfcb76cf360 100644 --- a/ts/features/payments/common/utils/index.ts +++ b/ts/features/payments/common/utils/index.ts @@ -153,7 +153,7 @@ export const getSortedPspList = ( return _.orderBy( pspList, ["onUs", "taxPayerFee", "pspBusinessName"], - ["desc", "asc", "asc"] + ["asc", "asc", "asc"] ); } }; diff --git a/ts/store/reducers/entities/__tests__/payments.test.ts b/ts/store/reducers/entities/__tests__/payments.test.ts new file mode 100644 index 00000000000..a55847a5a57 --- /dev/null +++ b/ts/store/reducers/entities/__tests__/payments.test.ts @@ -0,0 +1,94 @@ +import { Detail_v2Enum } from "../../../../../definitions/backend/PaymentProblemJson"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { + updatePaymentForMessage, + UpdatePaymentForMessageFailure +} from "../../../../features/messages/store/actions"; +import { UIMessageId } from "../../../../features/messages/types"; +import { isPaidPaymentFromDetailV2Enum } from "../../../../utils/payment"; +import { paymentByRptIdReducer, PaymentByRptIdState } from "../payments"; + +describe("payments", () => { + describe("paymentByRptIdReducer", () => { + const rptId1 = "01234567890012345678912345610"; + const rptId2 = "01234567890012345678912345620"; + const initialState: PaymentByRptIdState = { + [rptId1]: { + kind: "COMPLETED", + transactionId: 15 + }, + [rptId2]: { + kind: "DUPLICATED" + } + }; + const payload: UpdatePaymentForMessageFailure = { + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO, + messageId: "5a15aba4-7cd6-490b-b3fe-cf766f731a2f" as UIMessageId, + paymentId: "01234567890012345678912345630", + serviceId: "75c046cf-77a7-4d33-9c3f-578e00379b55" as ServiceId + }; + + Object.values(Detail_v2Enum) + .filter(detailV2Enum => !isPaidPaymentFromDetailV2Enum(detailV2Enum)) + .forEach(detailV2Enum => { + it(`should return the input state if the failure details are '${detailV2Enum}'`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: detailV2Enum + }) + ); + expect(outputState).toBe(initialState); + }); + }); + it(`should return the input state if failure details are 'PAA_PAGAMENTO_DUPLICATO' but there is already a record in the state for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + paymentId: rptId1, + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toBe(initialState); + }); + it(`should return the input state if failure details are 'PPT_PAGAMENTO_DUPLICATO' but there is already a record in the state for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + paymentId: rptId1, + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toBe(initialState); + }); + it(`should return the updated state if failure details are 'PAA_PAGAMENTO_DUPLICATO' and there is no record for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: Detail_v2Enum.PAA_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toEqual({ + ...initialState, + [payload.paymentId]: { kind: "DUPLICATED" } + }); + }); + it(`should return the updated state if failure details are 'PPT_PAGAMENTO_DUPLICATO' and there is no record for the input payment`, () => { + const outputState = paymentByRptIdReducer( + initialState, + updatePaymentForMessage.failure({ + ...payload, + details: Detail_v2Enum.PPT_PAGAMENTO_DUPLICATO + }) + ); + expect(outputState).toEqual({ + ...initialState, + [payload.paymentId]: { kind: "DUPLICATED" } + }); + }); + }); +}); diff --git a/ts/store/reducers/entities/payments.ts b/ts/store/reducers/entities/payments.ts index 9c7cbe52a99..da8e3eeb79f 100644 --- a/ts/store/reducers/entities/payments.ts +++ b/ts/store/reducers/entities/payments.ts @@ -8,6 +8,11 @@ import { Action } from "../../actions/types"; import { paymentCompletedSuccess } from "../../../features/payments/checkout/store/actions/orchestration"; import { GlobalState } from "../types"; import { differentProfileLoggedIn } from "../../actions/crossSessions"; +import { + updatePaymentForMessage, + UpdatePaymentForMessageFailure +} from "../../../features/messages/store/actions"; +import { isPaidPaymentFromDetailV2Enum } from "../../../utils/payment"; export type PaidReason = Readonly< | { @@ -44,6 +49,14 @@ export const paymentByRptIdReducer = ( transactionId: undefined } }; + // This action is dispatched by the payment status update saga that is triggered upon + // entering message details. Be aware that the status of a paid payment can never change, + // so there is no need to handle the removal of a no-more-paid payment from the state + case getType(updatePaymentForMessage.failure): + return paymentByRptIdStateFromUpdatePaymentForMessageFailure( + action.payload, + state + ); // clear state if the current profile is different from the previous one case getType(differentProfileLoggedIn): return INITIAL_STATE; @@ -53,6 +66,31 @@ export const paymentByRptIdReducer = ( } }; +const paymentByRptIdStateFromUpdatePaymentForMessageFailure = ( + payload: UpdatePaymentForMessageFailure, + state: PaymentByRptIdState +): PaymentByRptIdState => { + // Only paid payments are tracked from the reducer, ignore the others + const isPaidPayment = isPaidPaymentFromDetailV2Enum(payload.details); + if (!isPaidPayment) { + return state; + } + const rptId = payload.paymentId; + // Make sure not to overwrite any existing data (since it may + // have come from a payment flow, where data are more detailed) + const inMemoryPaymentData = state[rptId]; + if (inMemoryPaymentData != null) { + return state; + } + // Paid payment was not tracked, add it to the reducer's state + return { + ...state, + [rptId]: { + kind: "DUPLICATED" + } + }; +}; + // Selectors export const paymentsByRptIdSelector = (