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/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;