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 = (