From 58557e7a6776f6a613ae12ee51a01746593cd8f0 Mon Sep 17 00:00:00 2001
From: Cristian Matteu <94987118+ChrisMattew@users.noreply.github.com>
Date: Fri, 17 Jan 2025 10:59:49 +0100
Subject: [PATCH 1/3] [IOPID-2598] Tracking offline event (#6606)
## Short description
This PR adds the tracking of the `INGRESS_NO_INTERNET_CONNECTION` event,
logged when the user opens the app with the internet connection
disabled.
## List of changes proposed in this pull request
- Added and implemented the `INGRESS_NO_INTERNET_CONNECTION` event
- Added the `event_category: KO` to the already existing
`INGRESS_SERVICES_SLOW_DOWN`, `INGRESS_TIMEOUT` and
`INGRESS_CDN_SYSTEM_ERROR` events
## Demo
|iOS|Android|
|---|--------|
|||
## How to test
Start the app ensuring that the internet connection is disabled (as
shown in the demo). Once the blocking screen appears, close the app,
re-enable the internet connection, and reopen the app.
>[!Note]
>In this demos, I'm using [Proxyman](https://proxyman.io/) to monitor
internet traffic, but any tool of your choice can be used.
---
ts/features/ingress/analytics/index.ts | 20 +++++++++++--
ts/features/ingress/screens/IngressScreen.tsx | 30 ++++++++++++++-----
2 files changed, 39 insertions(+), 11 deletions(-)
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 63b4d132e49..86fbd3b9b46 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);
From d76c8c9ce48c6f81ac36eb48cf30e7c71bea76aa Mon Sep 17 00:00:00 2001
From: Emanuele Dall'Ara <71103219+LeleDallas@users.noreply.github.com>
Date: Fri, 17 Jan 2025 11:44:31 +0100
Subject: [PATCH 2/3] refactor[IOBP-1119]: Adapt IDPay custom table and list to
design system components (#6597)
## Short description
> [!WARNING]
> This PR depends on https://github.com/pagopa/io-app/pull/6586
This pull request focuses on enhancing the integration of the design
system list components for IDPay screens currently utilizing custom
tables and lists
## List of changes proposed in this pull request
- Apply `ListItem` to IDPay timeline screens
- Apply `ListItem` to IDPay initiative screens
- Delete unused custom `Table` component moving its type in main file
## How to test
Ensure that all replaced components are properly aligned with design
standards
## Preview
https://github.com/user-attachments/assets/f5da985d-9c37-43ef-b80f-558645997f5c
---------
Co-authored-by: Alessandro
---
ts/features/idpay/common/components/Table.tsx | 50 -----
.../components/BeneficiaryDetailsContent.tsx | 192 ++++++++++--------
.../InitiativeDiscountSettingsComponent.tsx | 9 +-
.../InitiativeRefundSettingsComponent.tsx | 12 +-
.../InitiativeTimelineComponent.tsx | 19 +-
.../screens/IdPayInitiativeDetailsScreen.tsx | 3 -
...ineDiscountTransactionDetailsComponent.tsx | 117 ++++-------
.../TimelineRefundDetailsComponent.tsx | 87 ++++----
.../TimelineTransactionDetailsComponent.tsx | 114 +++++------
9 files changed, 257 insertions(+), 346 deletions(-)
delete mode 100644 ts/features/idpay/common/components/Table.tsx
diff --git a/ts/features/idpay/common/components/Table.tsx b/ts/features/idpay/common/components/Table.tsx
deleted file mode 100644
index ff37696e971..00000000000
--- a/ts/features/idpay/common/components/Table.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { Fragment } 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 | React.ReactNode;
-}>;
-
-type TableProps = {
- title: string;
- rows: ReadonlyArray;
-};
-
-const renderTable = (data: ReadonlyArray): React.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 71349c27c27..38350bca942 100644
--- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx
+++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx
@@ -1,11 +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 React from "react";
-import { StyleSheet, View } from "react-native";
+import { View } from "react-native";
import Placeholder from "rn-placeholder";
import {
InitiativeDTO,
@@ -17,7 +26,6 @@ import {
RewardValueDTO,
RewardValueTypeEnum
} from "../../../../../definitions/idpay/RewardValueDTO";
-import { IOStyles } from "../../../../components/core/variables/IOStyles";
import I18n from "../../../../i18n";
import {
AppParamsList,
@@ -25,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 {
@@ -33,6 +40,11 @@ import {
InitiativeRulesInfoBoxSkeleton
} from "./InitiativeRulesInfoBox";
+type TableRow = WithTestID<{
+ label: string;
+ value: string | React.ReactNode;
+}>;
+
export type BeneficiaryDetailsProps =
| {
isLoading?: false;
@@ -210,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
+ })}
+
>
);
@@ -297,22 +324,17 @@ const BeneficiaryDetailsContentSkeleton = () => (
- {Array.from({ length: 5 }).map((_, j) => (
+ {Array.from({ length: 2 }).map((_, j) => (
-
-
-
-
+
))}
@@ -321,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 583cae36552..47681b5fa31 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";
@@ -84,12 +84,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 ea40f1fbd0b..f72463ef26b 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";
@@ -171,12 +170,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 dfc3214da2a..b5106087d4c 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 cba8831c2e0..ff89835112b 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 9bea069faea..f6b19aae345 100644
--- a/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx
+++ b/ts/features/idpay/timeline/components/TimelineDiscountTransactionDetailsComponent.tsx
@@ -1,20 +1,19 @@
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 React from "react";
-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";
@@ -69,65 +68,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 d9a7f1bc60a..24bf7f7660f 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";
@@ -11,13 +12,11 @@ import { format } from "date-fns";
import * as O from "fp-ts/lib/Option";
import { pipe } from "fp-ts/lib/function";
import React from "react";
-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";
@@ -85,44 +84,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 ef9085d4130..694785c6d57 100644
--- a/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx
+++ b/ts/features/idpay/timeline/components/TimelineTransactionDetailsComponent.tsx
@@ -1,22 +1,21 @@
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 React from "react";
-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";
@@ -66,59 +65,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 };
From eddd677034d88c164279fc9a23cd7128a4145056 Mon Sep 17 00:00:00 2001
From: Andrea
Date: Fri, 17 Jan 2025 13:14:23 +0100
Subject: [PATCH 3/3] [IOCOM-1914] Paid payment badge from message details
(#6610)
## Short description
This PR adds the PAID badge to a message in the messages home if the
related check happens on the message details.
## List of changes proposed in this pull request
- Paid badge reducer listens for the action that is dispatched by the
payment update on the message details
- Status is updated only if the information was not available yet, in
order not to overwrite more detailed data that may have come from a
complete payment flow
Be aware that a paid payment cannot change its status (so there is no
case where data have to be removed from the paid badge reducer)
## How to test
Using the io-dev-api-server, check that a paid payment generates the
PAID label, if detected from the message details.
---------
Co-authored-by: Martino Cesari Tomba <60693085+forrest57@users.noreply.github.com>
---
.../entities/__tests__/payments.test.ts | 94 +++++++++++++++++++
ts/store/reducers/entities/payments.ts | 38 ++++++++
2 files changed, 132 insertions(+)
create mode 100644 ts/store/reducers/entities/__tests__/payments.test.ts
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 = (