diff --git a/ts/components/LoadingSpinnerOverlay.tsx b/ts/components/LoadingSpinnerOverlay.tsx index 57e9bbd8217..22271727f72 100644 --- a/ts/components/LoadingSpinnerOverlay.tsx +++ b/ts/components/LoadingSpinnerOverlay.tsx @@ -1,22 +1,27 @@ -import { Text as NBButtonText } from "native-base"; import * as React from "react"; import { StyleSheet, View } from "react-native"; -import { IOColors, hexToRgba } from "@pagopa/io-app-design-system"; +import { + ButtonOutline, + IOColors, + hexToRgba +} from "@pagopa/io-app-design-system"; import I18n from "../i18n"; -import variables from "../theme/variables"; +import { useIOSelector } from "../store/hooks"; +import { isDesignSystemEnabledSelector } from "../store/reducers/persistedPreferences"; import ButtonDefaultOpacity from "./ButtonDefaultOpacity"; -import BoxedRefreshIndicator from "./ui/BoxedRefreshIndicator"; import { Overlay } from "./ui/Overlay"; import { IOStyles } from "./core/variables/IOStyles"; import { Body } from "./core/typography/Body"; +import BoxedRefreshIndicator from "./ui/BoxedRefreshIndicator"; const styles = StyleSheet.create({ textCaption: { - padding: variables.contentPadding + padding: 24 } }); type Props = Readonly<{ + children?: React.ReactNode; isLoading: boolean; loadingCaption?: string; loadingOpacity?: number; @@ -26,51 +31,56 @@ type Props = Readonly<{ /** * A Component to display and overlay spinner conditionally */ -class LoadingSpinnerOverlay extends React.Component { - public render() { - const { - children, - isLoading, - loadingCaption, - loadingOpacity = 0.7, - onCancel - } = this.props; - return ( - - - {loadingCaption || I18n.t("global.remoteStates.wait")} - - - } - action={ - onCancel && ( - +const LoadingSpinnerOverlay = ({ + children, + isLoading, + loadingCaption, + loadingOpacity = 0.7, + onCancel +}: Props) => { + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + return ( + + + {loadingCaption || I18n.t("global.remoteStates.wait")} + + + } + action={ + onCancel && ( + + {isDesignSystemEnabled ? ( + + ) : ( - - {I18n.t("global.buttons.cancel")} - + {I18n.t("global.buttons.cancel")} - - ) - } - /> - ) - } - > - {children} - - ); - } -} + )} + + ) + } + /> + ) + } + > + {children} + + ); +}; export default LoadingSpinnerOverlay; diff --git a/ts/components/__tests__/LoadingSpinnerOverlay.test.tsx b/ts/components/__tests__/LoadingSpinnerOverlay.test.tsx new file mode 100644 index 00000000000..98acfdc04b3 --- /dev/null +++ b/ts/components/__tests__/LoadingSpinnerOverlay.test.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import { Text } from "react-native"; +import { render } from "@testing-library/react-native"; +import configureMockStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { appReducer } from "../../store/reducers"; +import { applicationChangeState } from "../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../store/actions/persistedPreferences"; +import { GlobalState } from "../../store/reducers/types"; +import LoadingSpinnerOverlay from "../LoadingSpinnerOverlay"; + +describe("LoadingSpinnerOverlay", () => { + it("Should match base no-loading snapshot", () => { + const component = renderComponent(false); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match base loading snapshot", () => { + const component = renderComponent(true); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match all-properties and not-loading snapshot", () => { + const child = This is a child; + const loadingCaption = "This is the loading caption"; + const loadingOpacity = 0.65; + const onCancelCallback = () => undefined; + const component = renderComponent( + false, + child, + loadingCaption, + loadingOpacity, + onCancelCallback + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match all-properties and loading snapshot", () => { + const child = This is a child; + const loadingCaption = "This is the loading caption"; + const loadingOpacity = 0.65; + const onCancelCallback = () => undefined; + const component = renderComponent( + true, + child, + loadingCaption, + loadingOpacity, + onCancelCallback + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = ( + isLoading: boolean, + children?: React.ReactNode, + loadingCaption?: string, + loadingOpacity?: number, + onCancel?: () => void +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const dsState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(dsState); + return render( + + + {children} + + + ); +}; diff --git a/ts/components/__tests__/WebviewComponent.test.tsx b/ts/components/__tests__/WebviewComponent.test.tsx index 3b8b632bd24..9b0fc0c36b2 100644 --- a/ts/components/__tests__/WebviewComponent.test.tsx +++ b/ts/components/__tests__/WebviewComponent.test.tsx @@ -1,11 +1,28 @@ import { render } from "@testing-library/react-native"; +import { Provider } from "react-redux"; +import configureMockStore from "redux-mock-store"; import React from "react"; import WebviewComponent from "../WebviewComponent"; +import { appReducer } from "../../store/reducers"; +import { applicationChangeState } from "../../store/actions/application"; +import { GlobalState } from "../../store/reducers/types"; describe("WebviewComponent tests", () => { it("snapshot for component", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const enrichedState = { + ...globalState, + persistedPreferences: { + ...globalState.persistedPreferences, + isDesignSystemEnabled: false + } + }; + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(enrichedState); const component = render( - + + + ); expect(component).toMatchSnapshot(); }); diff --git a/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap b/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap new file mode 100644 index 00000000000..5f42ae771f3 --- /dev/null +++ b/ts/components/__tests__/__snapshots__/LoadingSpinnerOverlay.test.tsx.snap @@ -0,0 +1,654 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoadingSpinnerOverlay Should match all-properties and loading snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + This is the loading caption + + + + + + + Cancel + + + + + + + + + This is a child + + + +`; + +exports[`LoadingSpinnerOverlay Should match all-properties and not-loading snapshot 1`] = ` + + + + This is a child + + + +`; + +exports[`LoadingSpinnerOverlay Should match base loading snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + Wait a few seconds.. + + + + + + +`; + +exports[`LoadingSpinnerOverlay Should match base no-loading snapshot 1`] = ` + + + +`; diff --git a/ts/components/ui/BoxedRefreshIndicator.tsx b/ts/components/ui/BoxedRefreshIndicator.tsx index c0fb1ddf565..eb102cb687b 100644 --- a/ts/components/ui/BoxedRefreshIndicator.tsx +++ b/ts/components/ui/BoxedRefreshIndicator.tsx @@ -1,6 +1,8 @@ import * as React from "react"; import { StyleSheet, View } from "react-native"; -import { IOColors } from "@pagopa/io-app-design-system"; +import { IOColors, LoadingSpinner } from "@pagopa/io-app-design-system"; +import { useIOSelector } from "../../store/hooks"; +import { isDesignSystemEnabledSelector } from "../../store/reducers/persistedPreferences"; import { RefreshIndicator } from "./RefreshIndicator"; const styles = StyleSheet.create({ @@ -16,18 +18,21 @@ const styles = StyleSheet.create({ } }); -interface Props { +type Props = { + action?: React.ReactNode; caption?: React.ReactNode; white?: boolean; - action?: React.ReactNode; -} +}; -const BoxedRefreshIndicator: React.SFC = props => ( - - - {props.caption ? props.caption : null} - {props.action ? props.action : null} - -); +const BoxedRefreshIndicator = ({ action, caption, white }: Props) => { + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); + return ( + + {isDesignSystemEnabled ? : } + {caption ? caption : null} + {action ? action : null} + + ); +}; export default BoxedRefreshIndicator; diff --git a/ts/components/ui/Overlay.tsx b/ts/components/ui/Overlay.tsx index abe4e00cad0..6aa2ece0a73 100644 --- a/ts/components/ui/Overlay.tsx +++ b/ts/components/ui/Overlay.tsx @@ -1,9 +1,6 @@ -import { IOColors } from "@pagopa/io-app-design-system"; import * as React from "react"; import { StyleSheet, View } from "react-native"; - -const DEFAULT_OVERLAY_OPACITY = 1; -const DEFAULT_BACKGROUND_COLOR = IOColors.white; +import { IOColors } from "@pagopa/io-app-design-system"; const styles = StyleSheet.create({ container: { @@ -16,7 +13,7 @@ const styles = StyleSheet.create({ bottom: 0, left: 0, right: 0, - backgroundColor: DEFAULT_BACKGROUND_COLOR, + backgroundColor: IOColors.white, zIndex: 1, justifyContent: "center" }, @@ -26,9 +23,10 @@ const styles = StyleSheet.create({ }); type Props = Readonly<{ + backgroundColor?: string; + children?: React.ReactNode; foreground?: React.ReactNode; opacity?: number; - backgroundColor?: string; }>; /** @@ -36,28 +34,26 @@ type Props = Readonly<{ * * Used for loading spinners and error screens. */ -export const Overlay: React.SFC = props => { - const { - opacity = DEFAULT_OVERLAY_OPACITY, - backgroundColor = DEFAULT_BACKGROUND_COLOR - } = props; - return ( - - {props.foreground && ( - - {props.foreground} - - )} - - {props.children} - - ); -}; +export const Overlay = ({ + backgroundColor = IOColors.white, + children, + foreground, + opacity = 1 +}: Props) => ( + + {foreground && ( + + {foreground} + + )} + {children} + +); diff --git a/ts/components/ui/__test__/BoxedRefreshIndicator.test.tsx b/ts/components/ui/__test__/BoxedRefreshIndicator.test.tsx new file mode 100644 index 00000000000..3245dc4e74d --- /dev/null +++ b/ts/components/ui/__test__/BoxedRefreshIndicator.test.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { Text } from "react-native"; +import { render } from "@testing-library/react-native"; +import configureMockStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { appReducer } from "../../../store/reducers"; +import { applicationChangeState } from "../../../store/actions/application"; +import { preferencesDesignSystemSetEnabled } from "../../../store/actions/persistedPreferences"; +import { GlobalState } from "../../../store/reducers/types"; +import BoxedRefreshIndicator from "../BoxedRefreshIndicator"; + +describe("BoxedRefreshIndicator", () => { + it("Should match base snapshot", () => { + const component = renderComponent(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match all-properties snapshot", () => { + const action = This is the action; + const caption = This is the caption; + const white = true; + const component = renderComponent(action, caption, white); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); + +const renderComponent = ( + action?: React.ReactNode, + caption?: React.ReactNode, + white?: boolean +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const dsState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(dsState); + return render( + + + + ); +}; diff --git a/ts/components/ui/__test__/Overlay.test.tsx b/ts/components/ui/__test__/Overlay.test.tsx new file mode 100644 index 00000000000..930cbc12f71 --- /dev/null +++ b/ts/components/ui/__test__/Overlay.test.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { render } from "@testing-library/react-native"; +import { Text } from "react-native"; +import { Overlay } from "../Overlay"; + +describe("Overlay", () => { + it("Should match base snapshot", () => { + const component = render(); + expect(component.toJSON()).toMatchSnapshot(); + }); + it("Should match all-properties snapshot", () => { + const backgroundColor = "#FF0000"; + const opacity = 0.65; + const foreground = This is a foreground; + const children = This is a child; + const component = render( + + {children} + + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap b/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap new file mode 100644 index 00000000000..a0c0c9aebab --- /dev/null +++ b/ts/components/ui/__test__/__snapshots__/BoxedRefreshIndicator.test.tsx.snap @@ -0,0 +1,339 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BoxedRefreshIndicator Should match all-properties snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + This is the caption + + + This is the action + + +`; + +exports[`BoxedRefreshIndicator Should match base snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/components/ui/__test__/__snapshots__/Overlay.test.tsx.snap b/ts/components/ui/__test__/__snapshots__/Overlay.test.tsx.snap new file mode 100644 index 00000000000..8b954f15681 --- /dev/null +++ b/ts/components/ui/__test__/__snapshots__/Overlay.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Overlay Should match all-properties snapshot 1`] = ` + + + + This is a foreground + + + + + This is a child + + + +`; + +exports[`Overlay Should match base snapshot 1`] = ` + + + +`; diff --git a/ts/features/messages/analytics/index.ts b/ts/features/messages/analytics/index.ts index 96fde7ede6e..c8939737924 100644 --- a/ts/features/messages/analytics/index.ts +++ b/ts/features/messages/analytics/index.ts @@ -109,7 +109,7 @@ export function trackThirdPartyMessageAttachmentBadFormat( export function trackThirdPartyMessageAttachmentCorruptedFile( messageId: UIMessageId, - serviceId: ServiceId | undefined + serviceId?: ServiceId ) { void mixpanelTrack( "THIRD_PARTY_MESSAGE_ATTACHMENT_CORRUPTED_FILE", diff --git a/ts/features/messages/components/MessageAttachmentPreview.tsx b/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx similarity index 85% rename from ts/features/messages/components/MessageAttachmentPreview.tsx rename to ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx index bd635ea16ef..c20551a8d11 100644 --- a/ts/features/messages/components/MessageAttachmentPreview.tsx +++ b/ts/features/messages/components/MessageAttachment/LegacyMessageAttachmentPreview.tsx @@ -3,28 +3,28 @@ import { useNavigation } from "@react-navigation/native"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, SafeAreaView, StyleSheet } from "react-native"; import ReactNativeBlobUtil from "react-native-blob-util"; -import image from "../../../../img/servicesStatus/error-detail-icon.png"; -import { H2 } from "../../../components/core/typography/H2"; -import { renderInfoRasterImage } from "../../../components/infoScreen/imageRendering"; -import { InfoScreenComponent } from "../../../components/infoScreen/InfoScreenComponent"; -import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import I18n from "../../../i18n"; +import image from "../../../../../img/servicesStatus/error-detail-icon.png"; +import { H2 } from "../../../../components/core/typography/H2"; +import { renderInfoRasterImage } from "../../../../components/infoScreen/imageRendering"; +import { InfoScreenComponent } from "../../../../components/infoScreen/InfoScreenComponent"; +import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; +import FooterWithButtons from "../../../../components/ui/FooterWithButtons"; +import I18n from "../../../../i18n"; import { cancelPreviousAttachmentDownload, downloadAttachment -} from "../store/actions"; -import { useIODispatch, useIOSelector } from "../../../store/hooks"; -import { downloadPotForMessageAttachmentSelector } from "../store/reducers/downloads"; -import { UIAttachment, UIMessageId } from "../types"; -import variables from "../../../theme/variables"; -import { emptyContextualHelp } from "../../../utils/emptyContextualHelp"; -import { isIos } from "../../../utils/platform"; -import { isStrictNone } from "../../../utils/pot"; -import { share } from "../../../utils/share"; -import { showToast } from "../../../utils/showToast"; -import { confirmButtonProps } from "../../../components/buttons/ButtonConfigurations"; -import PdfViewer from "./MessageDetail/PdfViewer"; +} from "../../store/actions"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { downloadPotForMessageAttachmentSelector } from "../../store/reducers/downloads"; +import { UIAttachment, UIMessageId } from "../../types"; +import variables from "../../../../theme/variables"; +import { emptyContextualHelp } from "../../../../utils/emptyContextualHelp"; +import { isIos } from "../../../../utils/platform"; +import { isStrictNone } from "../../../../utils/pot"; +import { share } from "../../../../utils/share"; +import { showToast } from "../../../../utils/showToast"; +import { confirmButtonProps } from "../../../../components/buttons/ButtonConfigurations"; +import LegacyPdfViewer from "./LegacyPdfViewer"; type Props = { messageId: UIMessageId; @@ -86,7 +86,7 @@ const renderPDF = ( I18n.t("messagePDFPreview.errors.previewing.body") ) ) : ( - ); -export const MessageAttachmentPreview = ({ +export const LegacyMessageAttachmentPreview = ({ enableDownloadAttachment = true, ...props }: Props): React.ReactElement => { diff --git a/ts/features/messages/components/MessageAttachment/LegacyPdfViewer.tsx b/ts/features/messages/components/MessageAttachment/LegacyPdfViewer.tsx new file mode 100644 index 00000000000..d10f3fa1d80 --- /dev/null +++ b/ts/features/messages/components/MessageAttachment/LegacyPdfViewer.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import { StyleSheet } from "react-native"; +import Pdf from "react-native-pdf"; +import { IOColors } from "@pagopa/io-app-design-system"; +import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; +import I18n from "../../../../i18n"; + +const styles = StyleSheet.create({ + pdf: { + flex: 1, + backgroundColor: IOColors.bluegrey + } +}); + +type OwnProps = { + downloadPath: string; +}; + +type Props = OwnProps & Omit, "source">; + +const LegacyPdfViewer = ({ + style, + downloadPath, + onError, + onLoadComplete, + ...rest +}: Props) => { + const [isLoading, setIsLoading] = useState(true); + + return ( + + { + setIsLoading(false); + onLoadComplete?.(...args); + }} + onError={(...args) => { + setIsLoading(false); + onError?.(...args); + }} + /> + + ); +}; + +export default LegacyPdfViewer; diff --git a/ts/features/messages/components/MessageDetail/PdfViewer.tsx b/ts/features/messages/components/MessageAttachment/PdfViewer.tsx similarity index 94% rename from ts/features/messages/components/MessageDetail/PdfViewer.tsx rename to ts/features/messages/components/MessageAttachment/PdfViewer.tsx index 791b519fcda..b26f69052c2 100644 --- a/ts/features/messages/components/MessageDetail/PdfViewer.tsx +++ b/ts/features/messages/components/MessageAttachment/PdfViewer.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { StyleSheet } from "react-native"; import Pdf from "react-native-pdf"; import { IOColors } from "@pagopa/io-app-design-system"; -import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; import I18n from "../../../../i18n"; +import LoadingSpinnerOverlay from "../../../../components/LoadingSpinnerOverlay"; const styles = StyleSheet.create({ pdf: { @@ -18,7 +18,7 @@ type OwnProps = { type Props = OwnProps & Omit, "source">; -const PdfViewer = ({ +export const PdfViewer = ({ style, downloadPath, onError, @@ -26,11 +26,9 @@ const PdfViewer = ({ ...rest }: Props) => { const [isLoading, setIsLoading] = useState(true); - return ( ); }; - -export default PdfViewer; diff --git a/ts/features/messages/components/MessageAttachment/__test__/PdfViewer.test.tsx b/ts/features/messages/components/MessageAttachment/__test__/PdfViewer.test.tsx new file mode 100644 index 00000000000..72145e61fab --- /dev/null +++ b/ts/features/messages/components/MessageAttachment/__test__/PdfViewer.test.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { render } from "@testing-library/react-native"; +import configureMockStore from "redux-mock-store"; +import { Provider } from "react-redux"; +import { PdfViewer } from "../PdfViewer"; +import { appReducer } from "../../../../../store/reducers"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { GlobalState } from "../../../../../store/reducers/types"; + +describe("PdfViewer", () => { + it("should match the snapshot", () => { + const globalState = appReducer(undefined, applicationChangeState("active")); + const enrichedState = { + ...globalState, + persistedPreferences: { + ...globalState.persistedPreferences, + isDesignSystemEnabled: true + } + }; + const mockStore = configureMockStore(); + const store: ReturnType = mockStore(enrichedState); + const component = render( + + + + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap b/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap new file mode 100644 index 00000000000..07e6e5f8ead --- /dev/null +++ b/ts/features/messages/components/MessageAttachment/__test__/__snapshots__/PdfViewer.test.tsx.snap @@ -0,0 +1,250 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PdfViewer should match the snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + Loading document... + + + + + + +`; diff --git a/ts/features/messages/components/MessageDetail/index.tsx b/ts/features/messages/components/MessageDetail/index.tsx index 31a2bf26c1d..245dc7c0bad 100644 --- a/ts/features/messages/components/MessageDetail/index.tsx +++ b/ts/features/messages/components/MessageDetail/index.tsx @@ -155,17 +155,20 @@ const MessageDetailsComponent = ({ const messageTitle = useIOSelector(state => messageTitleSelector(state, messageId)) ?? title; + const serviceIdOpt = service?.id; const openAttachment = useCallback( (attachment: UIAttachment) => { navigation.navigate(MESSAGES_ROUTES.MESSAGES_NAVIGATOR, { screen: MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT, params: { messageId, - attachmentId: attachment.id + serviceId: serviceIdOpt, + attachmentId: attachment.id, + isPN: false } }); }, - [messageId, navigation] + [messageId, navigation, serviceIdOpt] ); const renderThirdPartyAttachments = useCallback( diff --git a/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx b/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx index 3820baec458..d91181746df 100644 --- a/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx +++ b/ts/features/messages/components/__test__/MessageAttachmentPreview.test.tsx @@ -9,12 +9,12 @@ import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWr import { Downloads } from "../../store/reducers/downloads"; import { mockPdfAttachment } from "../../__mocks__/attachment"; import I18n from "../../../../i18n"; -import { MessageAttachmentPreview } from "../MessageAttachmentPreview"; +import { LegacyMessageAttachmentPreview } from "../MessageAttachment/LegacyMessageAttachmentPreview"; const mockOpen = jest.fn(); const mockPdfViewer = ; -jest.mock("../MessageDetail/PdfViewer", () => () => mockPdfViewer); +jest.mock("../MessageAttachment/LegacyPdfViewer", () => () => mockPdfViewer); describe("MessageAttachmentPreview", () => { describe("when enableDownloadAttachment is false", () => { @@ -67,7 +67,7 @@ describe("MessageAttachmentPreview", () => { }); const renderComponent = ( - props: React.ComponentProps, + props: React.ComponentProps, downloads: Downloads = {} ) => { const globalState = appReducer(undefined, applicationChangeState("active")); @@ -84,7 +84,7 @@ const renderComponent = ( return { component: renderScreenWithNavigationStoreContext( - () => , + () => , "DUMMY", {}, store diff --git a/ts/features/messages/navigation/MessagesNavigator.tsx b/ts/features/messages/navigation/MessagesNavigator.tsx index 75527b7e274..56680906c65 100644 --- a/ts/features/messages/navigation/MessagesNavigator.tsx +++ b/ts/features/messages/navigation/MessagesNavigator.tsx @@ -9,43 +9,68 @@ import PN_ROUTES from "../../pn/navigation/routes"; import { useIOSelector } from "../../../store/hooks"; import { isGestureEnabled } from "../../../utils/navigation"; import { isPnEnabledSelector } from "../../../store/reducers/backendStatus"; -import { MessageDetailAttachment } from "../screens/MessageAttachment"; +import { LegacyMessageDetailAttachment } from "../screens/LegacyMessageAttachment"; +import { isDesignSystemEnabledSelector } from "../../../store/reducers/persistedPreferences"; +import { MessageAttachment } from "../screens/MessageAttachment"; import { MessagesParamsList } from "./params"; import { MESSAGES_ROUTES } from "./routes"; const Stack = createStackNavigator(); export const MessagesStackNavigator = () => { + const isDesignSystemEnabled = useIOSelector(isDesignSystemEnabledSelector); const isPnEnabled = useIOSelector(isPnEnabledSelector); return ( {isPnEnabled && ( - + )} ); diff --git a/ts/features/messages/navigation/params.ts b/ts/features/messages/navigation/params.ts index 162ed2e37a7..ae6f1f95751 100644 --- a/ts/features/messages/navigation/params.ts +++ b/ts/features/messages/navigation/params.ts @@ -3,15 +3,15 @@ import EUCOVIDCERT_ROUTES from "../../euCovidCert/navigation/routes"; import PN_ROUTES from "../../pn/navigation/routes"; import { MessageRouterScreenNavigationParams } from "../screens/MessageRouterScreen"; import { MessageDetailScreenNavigationParams } from "../screens/MessageDetailScreen"; -import { MessageDetailAttachmentNavigationParams } from "../screens/MessageAttachment"; import { EUCovidCertParamsList } from "../../euCovidCert/navigation/params"; import { PnParamsList } from "../../pn/navigation/params"; +import { MessageAttachmentNavigationParams } from "../screens/MessageAttachment"; import { MESSAGES_ROUTES } from "./routes"; export type MessagesParamsList = { [MESSAGES_ROUTES.MESSAGE_ROUTER]: MessageRouterScreenNavigationParams; [MESSAGES_ROUTES.MESSAGE_DETAIL]: MessageDetailScreenNavigationParams; - [MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT]: MessageDetailAttachmentNavigationParams; + [MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT]: MessageAttachmentNavigationParams; [EUCOVIDCERT_ROUTES.MAIN]: NavigatorScreenParams; [PN_ROUTES.MAIN]: NavigatorScreenParams; }; diff --git a/ts/features/messages/screens/LegacyMessageAttachment.tsx b/ts/features/messages/screens/LegacyMessageAttachment.tsx new file mode 100644 index 00000000000..d60ee13fd11 --- /dev/null +++ b/ts/features/messages/screens/LegacyMessageAttachment.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useRef } from "react"; +import { useNavigation } from "@react-navigation/native"; +import * as O from "fp-ts/lib/Option"; +import I18n from "../../../i18n"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import { LegacyMessageAttachmentPreview } from "../components/MessageAttachment/LegacyMessageAttachmentPreview"; +import { MessagesParamsList } from "../navigation/params"; +import { showToast } from "../../../utils/showToast"; +import { getServiceByMessageId } from "../store/reducers/paginatedById"; +import { useIOSelector } from "../../../store/hooks"; +import { thirdPartyMessageUIAttachment } from "../store/reducers/thirdPartyById"; +import { + trackThirdPartyMessageAttachmentCorruptedFile, + trackThirdPartyMessageAttachmentPreviewSuccess, + trackThirdPartyMessageAttachmentUserAction +} from "../analytics"; + +export const LegacyMessageDetailAttachment = ( + props: IOStackNavigationRouteProps< + MessagesParamsList, + "MESSAGE_DETAIL_ATTACHMENT" + > +): React.ReactElement => { + const navigation = useNavigation(); + const messageId = props.route.params.messageId; + const attachmentId = props.route.params.attachmentId; + // This ref is needed otherwise the auto back on the useEffect will fire multiple + // times, since its dependencies change during the back navigation + const autoBackOnErrorHandled = useRef(false); + const serviceId = useIOSelector(state => + getServiceByMessageId(state, messageId) + ); + + const maybeThirdPartyMessageUIAttachment = useIOSelector(state => + thirdPartyMessageUIAttachment(state)(messageId)(attachmentId) + ); + + useEffect(() => { + // This condition happens only if this screen is shown without having + // first retrieved the third party message (so it should never happen) + if ( + !autoBackOnErrorHandled.current && + O.isNone(maybeThirdPartyMessageUIAttachment) + ) { + // eslint-disable-next-line functional/immutable-data + autoBackOnErrorHandled.current = true; + showToast(I18n.t("messageDetails.attachments.downloadFailed")); + navigation.goBack(); + } + }, [navigation, maybeThirdPartyMessageUIAttachment]); + + return O.isSome(maybeThirdPartyMessageUIAttachment) ? ( + { + trackThirdPartyMessageAttachmentCorruptedFile(messageId, serviceId); + showToast(I18n.t("messageDetails.attachments.corruptedFile")); + }} + onLoadComplete={() => { + trackThirdPartyMessageAttachmentPreviewSuccess(); + }} + onDownload={() => { + trackThirdPartyMessageAttachmentUserAction("download"); + }} + onOpen={() => { + trackThirdPartyMessageAttachmentUserAction("open"); + }} + onShare={() => { + trackThirdPartyMessageAttachmentUserAction("share"); + }} + /> + ) : ( + <> + ); +}; diff --git a/ts/features/messages/screens/MessageAttachment.tsx b/ts/features/messages/screens/MessageAttachment.tsx index 03d545ed4c6..5945c03aed9 100644 --- a/ts/features/messages/screens/MessageAttachment.tsx +++ b/ts/features/messages/screens/MessageAttachment.tsx @@ -1,82 +1,236 @@ -import React, { useEffect, useRef } from "react"; -import { useNavigation } from "@react-navigation/native"; -import * as O from "fp-ts/lib/Option"; +import React, { useCallback, useState } from "react"; +import { pipe } from "fp-ts/lib/function"; +import * as B from "fp-ts/lib/boolean"; +import ReactNativeBlobUtil from "react-native-blob-util"; +import { FooterWithButtons } from "@pagopa/io-app-design-system"; import I18n from "../../../i18n"; -import { UIAttachmentId, UIMessageId } from "../types"; +import { useIOSelector } from "../../../store/hooks"; +import { downloadedMessageAttachmentSelector } from "../store/reducers/downloads"; +import { UIAttachment, UIAttachmentId, UIMessageId } from "../types"; +import { isIos } from "../../../utils/platform"; +import { share } from "../../../utils/share"; +import { IOToast } from "../../../components/Toast"; +import { useHeaderSecondLevel } from "../../../hooks/useHeaderSecondLevel"; +import { PdfViewer } from "../components/MessageAttachment/PdfViewer"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { MessageAttachmentPreview } from "../components/MessageAttachmentPreview"; import { MessagesParamsList } from "../navigation/params"; -import { showToast } from "../../../utils/showToast"; -import { getServiceByMessageId } from "../store/reducers/paginatedById"; -import { useIOSelector } from "../../../store/hooks"; -import { thirdPartyMessageUIAttachment } from "../store/reducers/thirdPartyById"; import { trackThirdPartyMessageAttachmentCorruptedFile, trackThirdPartyMessageAttachmentPreviewSuccess, trackThirdPartyMessageAttachmentUserAction } from "../analytics"; +import { ServiceId } from "../../../../definitions/backend/ServiceId"; +import { + trackPNAttachmentOpen, + trackPNAttachmentOpeningSuccess, + trackPNAttachmentSave, + trackPNAttachmentSaveShare, + trackPNAttachmentShare +} from "../../pn/analytics"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent"; -export type MessageDetailAttachmentNavigationParams = Readonly<{ +export type MessageAttachmentNavigationParams = Readonly<{ messageId: UIMessageId; attachmentId: UIAttachmentId; + isPN: boolean; + serviceId?: ServiceId; }>; -export const MessageDetailAttachment = ( +const renderFooter = ( + attachment: UIAttachment, + downloadPath: string, + isPN: boolean, + attachmentCategory?: string +) => + isIos ? ( + { + onShare(isPN, attachmentCategory); + ReactNativeBlobUtil.ios.presentOptionsMenu(downloadPath); + }, + label: I18n.t("messagePDFPreview.singleBtn") + } + }} + /> + ) : ( + { + onShare(isPN, attachmentCategory); + share(`file://${downloadPath}`, undefined, false)().catch(_ => { + IOToast.show(I18n.t("messagePDFPreview.errors.sharing")); + }); + }, + label: I18n.t("global.buttons.share") + } + }} + third={{ + type: "Outline", + buttonProps: { + accessibilityLabel: I18n.t("messagePDFPreview.save"), + onPress: () => { + onDownload(isPN, attachmentCategory); + ReactNativeBlobUtil.MediaCollection.copyToMediaStore( + { + name: attachment.displayName, + parentFolder: "", + mimeType: attachment.contentType + }, + "Download", + downloadPath + ) + .then(_ => { + IOToast.show( + I18n.t("messagePDFPreview.savedAtLocation", { + name: attachment.displayName + }) + ); + }) + .catch(_ => { + IOToast.error(I18n.t("messagePDFPreview.errors.saving")); + }); + }, + label: I18n.t("messagePDFPreview.save") + } + }} + secondary={{ + type: "Solid", + buttonProps: { + accessibilityLabel: I18n.t("messagePDFPreview.open"), + onPress: () => { + onOpen(isPN, attachmentCategory); + ReactNativeBlobUtil.android + .actionViewIntent(downloadPath, attachment.contentType) + .catch(_ => { + IOToast.error(I18n.t("messagePDFPreview.errors.opening")); + }); + }, + label: I18n.t("messagePDFPreview.open") + } + }} + /> + ); + +const onPDFError = ( + messageId: UIMessageId, + isPN: boolean, + serviceId?: ServiceId, + attachmentCategory?: string +) => + pipe( + isPN, + B.fold( + () => { + trackThirdPartyMessageAttachmentCorruptedFile(messageId, serviceId); + IOToast.error(I18n.t("messageDetails.attachments.corruptedFile")); + }, + () => trackPNAttachmentOpeningSuccess("error", attachmentCategory) + ) + ); + +const onLoadComplete = (isPN: boolean, attachmentCategory?: string) => + pipe( + isPN, + B.fold( + () => trackThirdPartyMessageAttachmentPreviewSuccess(), + () => trackPNAttachmentOpeningSuccess("displayer", attachmentCategory) + ) + ); + +const onShare = (isPN: boolean, attachmentCategory?: string) => + pipe( + isPN, + B.fold( + () => trackThirdPartyMessageAttachmentUserAction("share"), + () => + pipe( + isIos, + B.fold( + () => trackPNAttachmentShare(attachmentCategory), + () => trackPNAttachmentSaveShare(attachmentCategory) + ) + ) + ) + ); + +const onOpen = (isPN: boolean, attachmentCategory?: string) => + pipe( + isPN, + B.fold( + () => trackThirdPartyMessageAttachmentUserAction("open"), + () => trackPNAttachmentOpen(attachmentCategory) + ) + ); + +const onDownload = (isPN: boolean, attachmentCategory?: string) => + pipe( + isPN, + B.fold( + () => trackThirdPartyMessageAttachmentUserAction("download"), + () => trackPNAttachmentSave(attachmentCategory) + ) + ); + +export const MessageAttachment = ( props: IOStackNavigationRouteProps< MessagesParamsList, "MESSAGE_DETAIL_ATTACHMENT" > ): React.ReactElement => { - const navigation = useNavigation(); - const messageId = props.route.params.messageId; - const attachmentId = props.route.params.attachmentId; - // This ref is needed otherwise the auto back on the useEffect will fire multiple - // times, since its dependencies change during the back navigation - const autoBackOnErrorHandled = useRef(false); - const serviceId = useIOSelector(state => - getServiceByMessageId(state, messageId) - ); + const { messageId, attachmentId, isPN, serviceId } = props.route.params; + const [isPDFRenderingError, setIsPDFRenderingError] = useState(false); - const maybeThirdPartyMessageUIAttachment = useIOSelector(state => - thirdPartyMessageUIAttachment(state)(messageId)(attachmentId) + const downloadedAttachment = useIOSelector(state => + downloadedMessageAttachmentSelector(state, messageId, attachmentId) ); + const attachmentOpt = downloadedAttachment?.attachment; + const attachmentCategory = attachmentOpt?.category; + const downloadPathOpt = downloadedAttachment?.path; - useEffect(() => { - // This condition happens only if this screen is shown without having - // first retrieved the third party message (so it should never happen) - if ( - !autoBackOnErrorHandled.current && - O.isNone(maybeThirdPartyMessageUIAttachment) - ) { - // eslint-disable-next-line functional/immutable-data - autoBackOnErrorHandled.current = true; - showToast(I18n.t("messageDetails.attachments.downloadFailed")); - navigation.goBack(); - } - }, [navigation, maybeThirdPartyMessageUIAttachment]); + const onPDFRenderingError = useCallback(() => { + setIsPDFRenderingError(true); + onPDFError(messageId, isPN, serviceId, attachmentCategory); + }, [attachmentCategory, messageId, isPN, serviceId]); - return O.isSome(maybeThirdPartyMessageUIAttachment) ? ( - { - trackThirdPartyMessageAttachmentCorruptedFile(messageId, serviceId); - showToast(I18n.t("messageDetails.attachments.corruptedFile")); - }} - onLoadComplete={() => { - trackThirdPartyMessageAttachmentPreviewSuccess(); - }} - onDownload={() => { - trackThirdPartyMessageAttachmentUserAction("download"); - }} - onOpen={() => { - trackThirdPartyMessageAttachmentUserAction("open"); - }} - onShare={() => { - trackThirdPartyMessageAttachmentUserAction("share"); - }} - /> - ) : ( - <> + useHeaderSecondLevel({ + title: I18n.t("messagePDFPreview.title"), + supportRequest: true + }); + + if (!attachmentOpt || !downloadPathOpt) { + return ( + + ); + } + return ( + <> + {isPDFRenderingError ? ( + + ) : ( + onLoadComplete(isPN, attachmentCategory)} + /> + )} + {renderFooter(attachmentOpt, downloadPathOpt, isPN, attachmentCategory)} + ); }; diff --git a/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx b/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx new file mode 100644 index 00000000000..5531553a12f --- /dev/null +++ b/ts/features/messages/screens/__tests__/MessageAttachment.test.tsx @@ -0,0 +1,58 @@ +import { createStore } from "redux"; +import { UIAttachment, UIAttachmentId, UIMessageId } from "../../types"; +import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper"; +import { MESSAGES_ROUTES } from "../../navigation/routes"; +import { appReducer } from "../../../../store/reducers"; +import { applicationChangeState } from "../../../../store/actions/application"; +import { MessageAttachment } from "../MessageAttachment"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { downloadAttachment } from "../../store/actions"; +import { preferencesDesignSystemSetEnabled } from "../../../../store/actions/persistedPreferences"; + +describe("MessageAttachment", () => { + it("Should match the snapshot when there is an error", () => { + const messageId = "01HMZWRG7549N76017YR8YBSG2" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const serviceId = "01HMZXFS84T1Q1BN6GXRYT63VJ" as ServiceId; + const screen = renderScreen(messageId, attachmentId, serviceId, "failure"); + expect(screen.toJSON()).toMatchSnapshot(); + }); + it("Should match the snapshot when everything went fine", () => { + const messageId = "01HMZWRG7549N76017YR8YBSG2" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const serviceId = "01HMZXFS84T1Q1BN6GXRYT63VJ" as ServiceId; + const screen = renderScreen(messageId, attachmentId, serviceId, "success"); + expect(screen.toJSON()).toMatchSnapshot(); + }); +}); + +const renderScreen = ( + messageId: UIMessageId, + attachmentId: UIAttachmentId, + serviceId: ServiceId, + configuration: "failure" | "success" +) => { + const initialState = appReducer(undefined, applicationChangeState("active")); + const designSystemState = appReducer( + initialState, + preferencesDesignSystemSetEnabled({ isDesignSystemEnabled: true }) + ); + const withDownloadState = appReducer( + designSystemState, + downloadAttachment.success({ + attachment: { id: attachmentId, messageId } as UIAttachment, + path: "file:///fileName.pdf" + }) + ); + const store = createStore( + appReducer, + (configuration === "success" ? withDownloadState : designSystemState) as any + ); + + return renderScreenWithNavigationStoreContext( + MessageAttachment, + MESSAGES_ROUTES.MESSAGE_DETAIL_ATTACHMENT, + { messageId, attachmentId, isPN: false, serviceId }, + store + ); +}; diff --git a/ts/features/messages/screens/__tests__/MessageRouterScreen.test.tsx b/ts/features/messages/screens/__tests__/MessageRouterScreen.test.tsx index 678d072ba1d..dfe8164405d 100644 --- a/ts/features/messages/screens/__tests__/MessageRouterScreen.test.tsx +++ b/ts/features/messages/screens/__tests__/MessageRouterScreen.test.tsx @@ -6,7 +6,7 @@ import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWr import { MessageRouterScreen } from "../MessageRouterScreen"; import { getMessageDataAction } from "../../store/actions"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import * as ASD from "../../../../store/hooks"; +import * as IOHooks from "../../../../store/hooks"; import { MessageGetStatus } from "../../store/reducers/messageGetStatus"; import { MESSAGES_ROUTES } from "../../navigation/routes"; @@ -107,7 +107,7 @@ const renderScreen = ( const mockedDispatch = jest.fn(); jest - .spyOn(ASD, "useIODispatch") + .spyOn(IOHooks, "useIODispatch") .mockImplementation(() => mockedDispatch as Dispatch); const renderedMessageRouterScreen = renderScreenWithNavigationStoreContext( diff --git a/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap b/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap new file mode 100644 index 00000000000..18daafbbdfa --- /dev/null +++ b/ts/features/messages/screens/__tests__/__snapshots__/MessageAttachment.test.tsx.snap @@ -0,0 +1,1638 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MessageAttachment Should match the snapshot when there is an error 1`] = ` + + + + + + + + + + + + + + + + + PDF Preview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + There is a temporary problem, please try again. + + + + If the problem is not resolved, report it with the '?' icon at the top right, thank you! + + + + + + + + + + + + + + +`; + +exports[`MessageAttachment Should match the snapshot when everything went fine 1`] = ` + + + + + + + + + + + + + + + + + PDF Preview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading document... + + + + + + + + + + + + + + + Save or share + + + + + + + + + + + + + + + + + + +`; diff --git a/ts/features/messages/store/reducers/__tests__/downloads.test.ts b/ts/features/messages/store/reducers/__tests__/downloads.test.ts index 3191b856d39..49bee044cdd 100644 --- a/ts/features/messages/store/reducers/__tests__/downloads.test.ts +++ b/ts/features/messages/store/reducers/__tests__/downloads.test.ts @@ -1,10 +1,235 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { mockPdfAttachment } from "../../../__mocks__/attachment"; import { downloadAttachment, removeCachedAttachment } from "../../actions"; -import { Downloads, downloadsReducer } from "../downloads"; +import { + Download, + DownloadError, + Downloads, + INITIAL_STATE, + downloadedMessageAttachmentSelector, + downloadsReducer +} from "../downloads"; +import { + UIAttachment, + UIAttachmentId, + UIMessageId, + WithSkipMixpanelTrackingOnFailure +} from "../../../types"; +import { GlobalState } from "../../../../../store/reducers/types"; const path = "/path/attachment.pdf"; +describe("downloadedMessageAttachmentSelector", () => { + it("Should return undefined for an unmatching messageId", () => { + const attachmentId = "1" as UIAttachmentId; + const successDownload = { + attachment: { + messageId: "01HMXFQ803Q8JGQECKQF0EX6KX" as UIMessageId, + id: attachmentId + } as UIAttachment, + path: "randomPath" + } as Download; + const downloadSuccessAction = downloadAttachment.success(successDownload); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadSuccessAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return undefined for a matching messageId with an unmatching attachmentId", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const unrelatedAttachmentId = "2"; + const successDownload = { + attachment: { + messageId, + id: unrelatedAttachmentId + } as UIAttachment, + path: "randomPath" + } as Download; + const downloadSuccessAction = downloadAttachment.success(successDownload); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadSuccessAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const attachmentId = "1" as UIAttachmentId; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return undefined for an attachment that is loading", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const uiAttachmentRequest = { + messageId, + id: attachmentId, + skipMixpanelTrackingOnFailure: true + } as WithSkipMixpanelTrackingOnFailure; + const downloadRequestAction = + downloadAttachment.request(uiAttachmentRequest); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadRequestAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return undefined for an attachment that got an error", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const failedDownload = { + attachment: { + messageId, + id: attachmentId + } as UIAttachment, + error: new Error("An error") + } as DownloadError; + const downloadFailureAction = downloadAttachment.failure(failedDownload); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadFailureAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return undefined for an attachment that was cancelled before finishing the download", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const uiAttachmentCancelled = { + messageId, + id: attachmentId + } as UIAttachment; + const downloadCancelAction = downloadAttachment.cancel( + uiAttachmentCancelled + ); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadCancelAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return undefined for an attachment that was removed by a removeCachedAttachment action", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const successDownload = { + attachment: { + messageId, + id: attachmentId + } as UIAttachment, + path: "randomPath" + } as Download; + const removedCachedAttachmentAction = + removeCachedAttachment(successDownload); + const downloadsState = downloadsReducer( + INITIAL_STATE, + removedCachedAttachmentAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeUndefined(); + }); + it("Should return data for a matching downloaded attachment", () => { + const messageId = "01HMXFE7192J01KNK02BJAPMBR" as UIMessageId; + const attachmentId = "1" as UIAttachmentId; + const downloadPath = "randomPath"; + const successDownload = { + attachment: { + messageId, + id: attachmentId + } as UIAttachment, + path: downloadPath + } as Download; + const downloadSuccessAction = downloadAttachment.success(successDownload); + const downloadsState = downloadsReducer( + INITIAL_STATE, + downloadSuccessAction + ); + const globalState = { + entities: { + messages: { + downloads: downloadsState + } + } + } as GlobalState; + const downloadedAttachment = downloadedMessageAttachmentSelector( + globalState, + messageId, + attachmentId + ); + expect(downloadedAttachment).toBeDefined(); + expect(downloadedAttachment?.attachment).toBeDefined(); + expect(downloadedAttachment?.attachment.messageId).toBe(messageId); + expect(downloadedAttachment?.attachment.id).toBe(attachmentId); + expect(downloadedAttachment?.path).toBe(downloadPath); + }); +}); + describe("downloadsReducer", () => { describe("given no download", () => { const initialState = {}; diff --git a/ts/features/messages/store/reducers/downloads.ts b/ts/features/messages/store/reducers/downloads.ts index 5fd32447df3..3cf6630f74e 100644 --- a/ts/features/messages/store/reducers/downloads.ts +++ b/ts/features/messages/store/reducers/downloads.ts @@ -1,3 +1,5 @@ +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { createSelector } from "reselect"; import { getType } from "typesafe-actions"; @@ -107,3 +109,17 @@ export const downloadPotForMessageAttachmentSelector = createSelector( return pot.none; } ); + +export const downloadedMessageAttachmentSelector = ( + state: GlobalState, + messageId: UIMessageId, + attachmentId: UIAttachmentId +) => + pipe( + state.entities.messages.downloads[messageId], + O.fromNullable, + O.chainNullableK(messageDownloads => messageDownloads[attachmentId]), + O.map(pot.toOption), + O.flatten, + O.toUndefined + ); diff --git a/ts/features/pn/screens/AttachmentPreviewScreen.tsx b/ts/features/pn/screens/AttachmentPreviewScreen.tsx index 8f4acc12ef3..e516603718a 100644 --- a/ts/features/pn/screens/AttachmentPreviewScreen.tsx +++ b/ts/features/pn/screens/AttachmentPreviewScreen.tsx @@ -5,7 +5,6 @@ import * as O from "fp-ts/lib/Option"; import { PnParamsList } from "../navigation/params"; import { UIMessageId, UIAttachmentId } from "../../messages/types"; import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; -import { MessageAttachmentPreview } from "../../messages/components/MessageAttachmentPreview"; import { useIOSelector } from "../../../store/hooks"; import { pnMessageAttachmentSelector } from "../store/reducers"; import { @@ -16,6 +15,7 @@ import { trackPNAttachmentShare } from "../analytics"; import { isIos } from "../../../utils/platform"; +import { LegacyMessageAttachmentPreview } from "../../messages/components/MessageAttachment/LegacyMessageAttachmentPreview"; export type AttachmentPreviewScreenNavigationParams = Readonly<{ messageId: UIMessageId; @@ -51,7 +51,7 @@ export const AttachmentPreviewScreen = ({ }, [maybePnMessageAttachment, navigation]); return O.isSome(maybePnMessageAttachment) ? ( - "optInServiceId"; const navigateToServiceLink = () => @@ -188,7 +189,7 @@ const isPNOptInMessageTestInput: Array = [ CTAs: getMaybeCTAs(), service: { ...getMockService(), - id: "NotTheOptInOne" + id: "NotTheOptInOne" as ServiceId }, state: getMockState() }, diff --git a/ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx b/ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx new file mode 100644 index 00000000000..f4857eda2e4 --- /dev/null +++ b/ts/features/walletV3/payment/hooks/useOnTransactionActivationEffect.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { TransactionInfo } from "../../../../../definitions/pagopa/ecommerce/TransactionInfo"; +import { TransactionStatusEnum } from "../../../../../definitions/pagopa/ecommerce/TransactionStatus"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { getGenericError } from "../../../../utils/errors"; +import { walletPaymentGetTransactionInfo } from "../store/actions/networking"; +import { walletPaymentTransactionSelector } from "../store/selectors"; + +const INITIAL_DELAY = 250; +const MAX_TRIES = 3; + +type EffectCallback = ( + transaction: TransactionInfo +) => void | (() => void | undefined); + +/** + * This custom hook manages the transition of a transaction's status from ACTIVATION_REQUESTED to ACTIVATED. + * It employs a polling mechanism to continuously check the status, and once the status becomes ACTIVATED, + * the specified effect is triggered. + * @param effect Function to be executed upon transaction activation + */ +const useOnTransactionActivationEffect = (effect: EffectCallback) => { + const dispatch = useIODispatch(); + const transactionPot = useIOSelector(walletPaymentTransactionSelector); + + const delayRef = React.useRef(INITIAL_DELAY); + const countRef = React.useRef(0); + + /* eslint-disable functional/immutable-data */ + React.useEffect(() => { + if (transactionPot.kind === "PotSome") { + const { transactionId, status } = transactionPot.value; + + if (status === TransactionStatusEnum.ACTIVATED) { + // Execute the effect function when the transaction is activated + delayRef.current = INITIAL_DELAY; + countRef.current = 0; + return effect(transactionPot.value); + } else if (countRef.current > MAX_TRIES) { + // The transaction is not yet ACTIVATED, and we exceeded the max retries + dispatch( + walletPaymentGetTransactionInfo.failure( + getGenericError(new Error("Max try reached")) + ) + ); + return; + } else { + // The transaction is not yet ACTIVATED, continue polling for transaction status with a timeout + const timeout = setTimeout(() => { + delayRef.current *= 2; + countRef.current += 1; + dispatch(walletPaymentGetTransactionInfo.request({ transactionId })); + }, delayRef.current); + // Clean up the timeout to avoid memory leaks + return () => { + clearTimeout(timeout); + }; + } + } + + return undefined; + }, [dispatch, transactionPot, effect]); +}; + +export { useOnTransactionActivationEffect }; diff --git a/ts/features/walletV3/payment/saga/index.ts b/ts/features/walletV3/payment/saga/index.ts index 3f8bee87f6f..450a3a8a356 100644 --- a/ts/features/walletV3/payment/saga/index.ts +++ b/ts/features/walletV3/payment/saga/index.ts @@ -9,6 +9,7 @@ import { walletPaymentDeleteTransaction, walletPaymentGetAllMethods, walletPaymentGetDetails, + walletPaymentGetTransactionInfo, walletPaymentGetUserWallets, walletPaymentNewSessionToken } from "../store/actions/networking"; @@ -20,6 +21,7 @@ import { handleWalletPaymentGetUserWallets } from "./networking/handleWalletPaym import { handleWalletPaymentAuthorization } from "./networking/handleWalletPaymentAuthorization"; import { handleWalletPaymentDeleteTransaction } from "./networking/handleWalletPaymentDeleteTransaction"; import { handleWalletPaymentNewSessionToken } from "./networking/handleWalletPaymentNewSessionToken"; +import { handleWalletPaymentGetTransactionInfo } from "./networking/handleWalletPaymentGetTransactionInfo"; /** * Handle the pagoPA payments requests @@ -65,6 +67,12 @@ export function* watchWalletPaymentSaga( paymentClient.newTransaction ); + yield* takeLatest( + walletPaymentGetTransactionInfo.request, + handleWalletPaymentGetTransactionInfo, + paymentClient.getTransactionInfo + ); + yield* takeLatest( walletPaymentDeleteTransaction.request, handleWalletPaymentDeleteTransaction, diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts index 67de2845f2d..770d24100d1 100644 --- a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts +++ b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentCreateTransaction.ts @@ -26,20 +26,20 @@ export function* handleWalletPaymentCreateTransaction( return; } - const calculateFeesRequest = newTransaction({ + const newTransactionRequest = newTransaction({ body: action.payload, eCommerceSessionToken: sessionToken }); - const calculateFeesResult = (yield* call( + const newTransactionResult = (yield* call( withRefreshApiCall, - calculateFeesRequest, + newTransactionRequest, action )) as SagaCallReturnType; yield* put( pipe( - calculateFeesResult, + newTransactionResult, E.fold( error => walletPaymentCreateTransaction.failure({ @@ -47,7 +47,6 @@ export function* handleWalletPaymentCreateTransaction( }), ({ status, value }) => { if (status === 200) { - action.payload.onSucces?.(); return walletPaymentCreateTransaction.success(value); } else if (status === 400) { return walletPaymentCreateTransaction.failure({ diff --git a/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts new file mode 100644 index 00000000000..be09557b229 --- /dev/null +++ b/ts/features/walletV3/payment/saga/networking/handleWalletPaymentGetTransactionInfo.ts @@ -0,0 +1,65 @@ +import * as E from "fp-ts/lib/Either"; +import { pipe } from "fp-ts/lib/function"; +import { call, put } from "typed-redux-saga/macro"; +import { ActionType } from "typesafe-actions"; +import { SagaCallReturnType } from "../../../../../types/utils"; +import { getGenericError, getNetworkError } from "../../../../../utils/errors"; +import { readablePrivacyReport } from "../../../../../utils/reporters"; +import { withRefreshApiCall } from "../../../../fastLogin/saga/utils"; +import { PaymentClient } from "../../api/client"; +import { walletPaymentGetTransactionInfo } from "../../store/actions/networking"; +import { getOrFetchWalletSessionToken } from "./handleWalletPaymentNewSessionToken"; + +export function* handleWalletPaymentGetTransactionInfo( + getTransactionInfo: PaymentClient["getTransactionInfo"], + action: ActionType<(typeof walletPaymentGetTransactionInfo)["request"]> +) { + const sessionToken = yield* getOrFetchWalletSessionToken(); + + if (sessionToken === undefined) { + yield* put( + walletPaymentGetTransactionInfo.failure({ + ...getGenericError(new Error(`Missing session token`)) + }) + ); + return; + } + + const getTransactionInfoRequest = getTransactionInfo({ + eCommerceSessionToken: sessionToken, + transactionId: action.payload.transactionId + }); + + try { + const getTransactionInfoResult = (yield* call( + withRefreshApiCall, + getTransactionInfoRequest, + action + )) as SagaCallReturnType; + + yield* put( + pipe( + getTransactionInfoResult, + E.fold( + error => + walletPaymentGetTransactionInfo.failure({ + ...getGenericError(new Error(readablePrivacyReport(error))) + }), + ({ status, value }) => { + if (status === 200) { + return walletPaymentGetTransactionInfo.success(value); + } else { + return walletPaymentGetTransactionInfo.failure({ + ...getGenericError(new Error(JSON.stringify(value))) + }); + } + } + ) + ) + ); + } catch (e) { + yield* put( + walletPaymentGetTransactionInfo.failure({ ...getNetworkError(e) }) + ); + } +} diff --git a/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx index 9227127035e..7306b4f6750 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentConfirmScreen.tsx @@ -63,7 +63,7 @@ const WalletPaymentConfirmScreen = () => { startPaymentAuthorizaton({ paymentAmount: paymentDetail.amount as AmountEuroCents, paymentFees: (selectedPsp.taxPayerFee ?? 0) as AmountEuroCents, - pspId: selectedPsp.idBundle ?? "", + pspId: selectedPsp.idPsp ?? "", transactionId: transaction.transactionId, walletId: selectedMethod.walletId }) diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx index da133cd3e8a..74c73aab212 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentPickMethodScreen.tsx @@ -33,6 +33,7 @@ import { findFirstCaseInsensitive } from "../../../../utils/object"; import { UIWalletInfoDetails } from "../../details/types/UIWalletInfoDetails"; import { WalletPaymentMissingMethodsError } from "../components/WalletPaymentMissingMethodsError"; import { useWalletPaymentGoBackHandler } from "../hooks/useWalletPaymentGoBackHandler"; +import { useOnTransactionActivationEffect } from "../hooks/useOnTransactionActivationEffect"; import { WalletPaymentRoutes } from "../navigation/routes"; import { walletPaymentCreateTransaction, @@ -78,20 +79,35 @@ const WalletPaymentPickMethodScreen = () => { }); const paymentDetailsPot = useIOSelector(walletPaymentDetailsSelector); - const transactionPot = useIOSelector(walletPaymentTransactionSelector); const getSavedtMethodById = useIOSelector( walletPaymentSavedMethodByIdSelector ); const paymentAmountPot = useIOSelector(walletPaymentAmountSelector); const paymentMethodsPot = useIOSelector(walletPaymentAllMethodsSelector); const userWalletsPots = useIOSelector(walletPaymentUserWalletsSelector); + const transactionPot = useIOSelector(walletPaymentTransactionSelector); // const getGenericMethodById = useIOSelector(walletPaymentGenericMethodByIdSelector); + const [waitingTransactionActivation, setWaitingTransactionActivation] = + React.useState(false); + + // When a new transaction is created it comes with ACTIVATION_REQUESTED status, we can continue the payment flow + // only when the transaction status becomes ACTIVATED. + useOnTransactionActivationEffect( + React.useCallback(() => { + navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { + screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP + }); + setWaitingTransactionActivation(false); + }, [navigation]) + ); + const alertRef = React.useRef(null); const isLoading = pot.isLoading(paymentMethodsPot) || pot.isLoading(userWalletsPots); - const isLoadingTransaction = pot.isLoading(transactionPot); + const isLoadingTransaction = + pot.isLoading(transactionPot) || waitingTransactionActivation; const isError = pot.isError(transactionPot) || @@ -105,7 +121,8 @@ const WalletPaymentPickMethodScreen = () => { useFocusEffect( React.useCallback(() => { - // dispatch(walletPaymentGetAllMethods.request()); // currently we do not allow onboarding new methods in payment flow + // currently we do not allow onboarding new methods in payment flow + // dispatch(walletPaymentGetAllMethods.request()); dispatch(walletPaymentGetUserWallets.request()); }, [dispatch]) ); @@ -157,21 +174,6 @@ const WalletPaymentPickMethodScreen = () => { }); }; - /* Will be decommented once generic methods are implemented - const handleSelectNotSavedMethod = (methodId: string) => { - setSelectedMethod({ - kind: "generic", - methodId - }); - }; - */ - - const navigateToPspSelectionScreen = () => { - navigation.navigate(WalletPaymentRoutes.WALLET_PAYMENT_MAIN, { - screen: WalletPaymentRoutes.WALLET_PAYMENT_PICK_PSP - }); - }; - const handleContinue = () => { // todo:: should handle the case where the user // selects a non saved method @@ -187,10 +189,10 @@ const WalletPaymentPickMethodScreen = () => { walletPaymentCreateTransaction.request({ paymentNotices: [ { rptId: paymentDetails.rptId, amount: paymentDetails.amount } - ], - onSucces: navigateToPspSelectionScreen + ] }) ); + setWaitingTransactionActivation(true); }) ); } diff --git a/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx b/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx index 80ee6cac0e5..a6802e37265 100644 --- a/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx +++ b/ts/features/walletV3/payment/screens/WalletPaymentPickPspScreen.tsx @@ -14,6 +14,7 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { Bundle } from "../../../../../definitions/pagopa/ecommerce/Bundle"; +import { Transfer } from "../../../../../definitions/pagopa/ecommerce/Transfer"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { @@ -37,7 +38,7 @@ import { walletPaymentPickedPaymentMethodSelector, walletPaymentPickedPspSelector, walletPaymentPspListSelector, - walletPaymentTransactionTransferListSelector + walletPaymentTransactionSelector } from "../store/selectors"; import { WalletPaymentPspSortType } from "../types"; import { WalletPaymentOutcomeEnum } from "../types/PaymentOutcomeEnum"; @@ -53,9 +54,7 @@ const WalletPaymentPickPspScreen = () => { const [sortType, setSortType] = React.useState("default"); - const transactionTransferListPot = useIOSelector( - walletPaymentTransactionTransferListSelector - ); + const transactionPot = useIOSelector(walletPaymentTransactionSelector); const pspListPot = useIOSelector(walletPaymentPspListSelector); const selectedPspOption = useIOSelector(walletPaymentPickedPspSelector); @@ -93,12 +92,19 @@ const WalletPaymentPickPspScreen = () => { pipe( sequenceT(O.Monad)( pot.toOption(paymentAmountPot), - pot.toOption(transactionTransferListPot), + pot.toOption(transactionPot), selectedWalletOption ), - O.map(([paymentAmountInCents, transferList, selectedWallet]) => { + O.map(([paymentAmountInCents, transaction, selectedWallet]) => { + const transferList = transaction.payments.reduce( + (a, p) => [...a, ...(p.transferList ?? [])], + [] as ReadonlyArray + ); + const paymentToken = transaction.payments[0]?.paymentToken; + dispatch( walletPaymentCalculateFees.request({ + paymentToken, paymentMethodId: selectedWallet.paymentMethodId, walletId: selectedWallet.walletId, paymentAmount: paymentAmountInCents, @@ -107,12 +113,7 @@ const WalletPaymentPickPspScreen = () => { ); }) ); - }, [ - dispatch, - paymentAmountPot, - selectedWalletOption, - transactionTransferListPot - ]) + }, [dispatch, paymentAmountPot, selectedWalletOption, transactionPot]) ); React.useEffect( diff --git a/ts/features/walletV3/payment/store/actions/networking.ts b/ts/features/walletV3/payment/store/actions/networking.ts index 1a776e15d95..9065a94feac 100644 --- a/ts/features/walletV3/payment/store/actions/networking.ts +++ b/ts/features/walletV3/payment/store/actions/networking.ts @@ -11,6 +11,7 @@ import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; import { NetworkError } from "../../../../../utils/errors"; import { WalletPaymentFailure } from "../../types/failure"; import { NewSessionTokenResponse } from "../../../../../../definitions/pagopa/ecommerce/NewSessionTokenResponse"; +import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; import { CalculateFeeRequest } from "../../../../../../definitions/pagopa/ecommerce/CalculateFeeRequest"; export const walletPaymentNewSessionToken = createAsyncAction( @@ -52,11 +53,17 @@ export const walletPaymentCreateTransaction = createAsyncAction( "WALLET_PAYMENT_CREATE_TRANSACTION_SUCCESS", "WALLET_PAYMENT_CREATE_TRANSACTION_FAILURE" )< - NewTransactionRequest & { onSucces?: () => void }, + NewTransactionRequest, NewTransactionResponse, NetworkError | WalletPaymentFailure >(); +export const walletPaymentGetTransactionInfo = createAsyncAction( + "WALLET_PAYMENT_GET_TRANSACTION_INFO_REQUEST", + "WALLET_PAYMENT_GET_TRANSACTION_INFO_SUCCESS", + "WALLET_PAYMENT_GET_TRANSACTION_INFO_FAILURE" +)<{ transactionId: string }, TransactionInfo, NetworkError>(); + export const walletPaymentDeleteTransaction = createAsyncAction( "WALLET_PAYMENT_DELETE_TRANSACTION_REQUEST", "WALLET_PAYMENT_DELETE_TRANSACTION_SUCCESS", @@ -90,5 +97,6 @@ export type WalletPaymentNetworkingActions = | ActionType | ActionType | ActionType + | ActionType | ActionType | ActionType; diff --git a/ts/features/walletV3/payment/store/reducers/index.ts b/ts/features/walletV3/payment/store/reducers/index.ts index 691745a98ff..0e9302a0ebd 100644 --- a/ts/features/walletV3/payment/store/reducers/index.ts +++ b/ts/features/walletV3/payment/store/reducers/index.ts @@ -1,17 +1,21 @@ -import _ from "lodash"; import * as pot from "@pagopa/ts-commons/lib/pot"; -import * as O from "fp-ts/lib/Option"; import { NavigatorScreenParams } from "@react-navigation/native"; -import { getType } from "typesafe-actions"; -import { pipe } from "fp-ts/lib/function"; import { sequenceS } from "fp-ts/lib/Apply"; +import * as O from "fp-ts/lib/Option"; +import { pipe } from "fp-ts/lib/function"; +import { getType } from "typesafe-actions"; import { Bundle } from "../../../../../../definitions/pagopa/ecommerce/Bundle"; -import { NewTransactionResponse } from "../../../../../../definitions/pagopa/ecommerce/NewTransactionResponse"; import { PaymentRequestsGetResponse } from "../../../../../../definitions/pagopa/ecommerce/PaymentRequestsGetResponse"; +import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; +import { TransactionInfo } from "../../../../../../definitions/pagopa/ecommerce/TransactionInfo"; import { PaymentMethodsResponse } from "../../../../../../definitions/pagopa/walletv3/PaymentMethodsResponse"; +import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; import { Wallets } from "../../../../../../definitions/pagopa/walletv3/Wallets"; +import NavigationService from "../../../../../navigation/NavigationService"; +import { AppParamsList } from "../../../../../navigation/params/AppParamsList"; import { Action } from "../../../../../store/actions/types"; import { NetworkError } from "../../../../../utils/errors"; +import { WalletPaymentFailure } from "../../types/failure"; import { walletPaymentAuthorization, walletPaymentCalculateFees, @@ -19,20 +23,16 @@ import { walletPaymentDeleteTransaction, walletPaymentGetAllMethods, walletPaymentGetDetails, - walletPaymentNewSessionToken, - walletPaymentGetUserWallets + walletPaymentGetTransactionInfo, + walletPaymentGetUserWallets, + walletPaymentNewSessionToken } from "../actions/networking"; import { + walletPaymentInitState, walletPaymentPickPaymentMethod, walletPaymentPickPsp, - walletPaymentInitState, walletPaymentResetPickedPsp } from "../actions/orchestration"; -import { WalletInfo } from "../../../../../../definitions/pagopa/walletv3/WalletInfo"; -import { WalletPaymentFailure } from "../../types/failure"; -import { RptId } from "../../../../../../definitions/pagopa/ecommerce/RptId"; -import NavigationService from "../../../../../navigation/NavigationService"; -import { AppParamsList } from "../../../../../navigation/params/AppParamsList"; export type WalletPaymentState = { rptId?: RptId; @@ -46,10 +46,7 @@ export type WalletPaymentState = { pspList: pot.Pot, NetworkError>; chosenPaymentMethod: O.Option; chosenPsp: O.Option; - transaction: pot.Pot< - NewTransactionResponse, - NetworkError | WalletPaymentFailure - >; + transaction: pot.Pot; authorizationUrl: pot.Pot; startRoute?: { routeName: keyof AppParamsList; @@ -196,14 +193,16 @@ const reducer = ( chosenPsp: O.none }; - // Create/delete transaction + // Transaction case getType(walletPaymentCreateTransaction.request): + case getType(walletPaymentGetTransactionInfo.request): case getType(walletPaymentDeleteTransaction.request): return { ...state, transaction: pot.toLoading(state.transaction) }; case getType(walletPaymentCreateTransaction.success): + case getType(walletPaymentGetTransactionInfo.success): return { ...state, transaction: pot.some(action.payload) @@ -214,6 +213,7 @@ const reducer = ( transaction: pot.none }; case getType(walletPaymentCreateTransaction.failure): + case getType(walletPaymentGetTransactionInfo.failure): case getType(walletPaymentDeleteTransaction.failure): return { ...state, diff --git a/ts/features/walletV3/payment/store/selectors/index.ts b/ts/features/walletV3/payment/store/selectors/index.ts index fcaf6e62e3c..bd05aa66f9d 100644 --- a/ts/features/walletV3/payment/store/selectors/index.ts +++ b/ts/features/walletV3/payment/store/selectors/index.ts @@ -3,7 +3,6 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import { createSelector } from "reselect"; import { GlobalState } from "../../../../../store/reducers/types"; -import { Transfer } from "../../../../../../definitions/pagopa/ecommerce/Transfer"; const selectWalletPayment = (state: GlobalState) => state.features.wallet.payment; @@ -79,17 +78,6 @@ export const walletPaymentTransactionSelector = createSelector( state => state.transaction ); -export const walletPaymentTransactionTransferListSelector = createSelector( - walletPaymentTransactionSelector, - transaction => - pot.map(transaction, t => - t.payments.reduce( - (a, p) => [...a, ...(p.transferList ?? [])], - [] as ReadonlyArray - ) - ) -); - export const walletPaymentAuthorizationUrlSelector = createSelector( selectWalletPayment, state => state.authorizationUrl diff --git a/ts/store/reducers/entities/services/types.ts b/ts/store/reducers/entities/services/types.ts index 2e1d6ee3cdd..e1bdac128a4 100644 --- a/ts/store/reducers/entities/services/types.ts +++ b/ts/store/reducers/entities/services/types.ts @@ -1,11 +1,12 @@ import { ImageURISource } from "react-native"; import { ServicePublic } from "../../../../../definitions/backend/ServicePublic"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; /** * Domain-specific representation of a Service with aggregated data. */ export type UIService = { - id: string; + id: ServiceId; name: string; organizationName: string; organizationFiscalCode: string;