From b0f8a2e2b361f1e4fbfa83cf04e48dfb6a1a382e Mon Sep 17 00:00:00 2001 From: Martino Cesari Tomba <60693085+forrest57@users.noreply.github.com> Date: Fri, 9 Aug 2024 15:09:17 +0200 Subject: [PATCH] [IOCOM-1338, IOCOM-1562, IOCOM-1563] FIMS history export feature (#6091) ## Short description Addition of Fims history export feature https://github.com/user-attachments/assets/50fd997b-8035-48b3-8715-d13f3337ecc3 ## List of changes proposed in this pull request - history export saga - error/confirm alerts - required i18n entries - system alerts for both export request confirmation and already exporting notification ## How to test using the dev-server, navigate to profile>privacy>third party accesses and play around with the export functionality. Make sure everything works as expected. --------- Co-authored-by: Andrea --- locales/en/index.yml | 7 ++ locales/it/index.yml | 7 ++ .../hooks/useFimsHistoryResultToasts.tsx | 84 +++++++++++++++++++ .../saga/handleExportFimsHistorySaga.ts | 47 +++++++++++ ts/features/fims/history/saga/index.ts | 9 +- .../fims/history/screens/HistoryScreen.tsx | 50 +++++++---- .../fims/history/store/actions/index.ts | 21 ++++- .../fims/history/store/reducer/index.ts | 41 ++++++++- .../fims/history/store/selectors/index.ts | 6 ++ 9 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 ts/features/fims/history/hooks/useFimsHistoryResultToasts.tsx create mode 100644 ts/features/fims/history/saga/handleExportFimsHistorySaga.ts diff --git a/locales/en/index.yml b/locales/en/index.yml index 1da745e6fe6..88158213582 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -3974,7 +3974,14 @@ permissionRequest: FIMS: history: exportData: + alerts: + areYouSure: "Vuoi davvero esportare una copia di tutti gli accessi?" + alreadyExporting: + title: "Abbiamo già preso in carico una richiesta di esportazione." + body: "Al termine dell’elaborazione, riceverai una email con tutte le informazioni dello storico accessi." CTA: "Richiedi una copia via email" + successToast: "Fatto! Controlla la tua casella di posta." + errorToast: "C’è stato un problema nell’invio della richiesta. Riprova" profileCTA: title: Accessi a servizi di terze parti subTitle: Rivedi gli accessi effettuati a servizi di terze parti tramite IO diff --git a/locales/it/index.yml b/locales/it/index.yml index 8f156665f14..11fd4ba73d4 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -3974,7 +3974,14 @@ permissionRequest: FIMS: history: exportData: + alerts: + areYouSure: "Vuoi davvero esportare una copia di tutti gli accessi?" + alreadyExporting: + title: "Abbiamo già preso in carico una richiesta di esportazione." + body: "Al termine dell’elaborazione, riceverai una email con tutte le informazioni dello storico accessi." CTA: "Richiedi una copia via email" + successToast: "Fatto! Controlla la tua casella di posta." + errorToast: "C’è stato un problema nell’invio della richiesta. Riprova" profileCTA: title: Accessi a servizi di terze parti subTitle: Rivedi gli accessi effettuati a servizi di terze parti tramite IO diff --git a/ts/features/fims/history/hooks/useFimsHistoryResultToasts.tsx b/ts/features/fims/history/hooks/useFimsHistoryResultToasts.tsx new file mode 100644 index 00000000000..97b3f9b2f36 --- /dev/null +++ b/ts/features/fims/history/hooks/useFimsHistoryResultToasts.tsx @@ -0,0 +1,84 @@ +/* eslint-disable functional/immutable-data */ +import { IOToast } from "@pagopa/io-app-design-system"; +import { constVoid } from "fp-ts/lib/function"; +import * as React from "react"; +import { Alert } from "react-native"; +import * as RemoteValue from "../../../../common/model/RemoteValue"; +import I18n from "../../../../i18n"; +import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { + fimsHistoryExport, + resetFimsHistoryExportState +} from "../store/actions"; +import { fimsHistoryExportStateSelector } from "../store/selectors"; + +const showFimsExportError = () => + IOToast.error(I18n.t("FIMS.history.exportData.errorToast")); + +const showFimsExportSuccess = () => + IOToast.success(I18n.t("FIMS.history.exportData.successToast")); + +const showFimsAlreadyExportingAlert = (onPress: () => void) => + Alert.alert( + I18n.t("FIMS.history.exportData.alerts.alreadyExporting.title"), + I18n.t("FIMS.history.exportData.alerts.alreadyExporting.body"), + [{ text: I18n.t("global.buttons.ok"), onPress }] + ); + +export const useFimsHistoryExport = () => { + const historyExportState = useIOSelector(fimsHistoryExportStateSelector); + const dispatch = useIODispatch(); + const isProcessing = React.useRef(false); + + React.useEffect(() => { + RemoteValue.fold( + historyExportState, + constVoid, + constVoid, + value => { + if (value === "SUCCESS") { + showFimsExportSuccess(); + isProcessing.current = false; + } else { + showFimsAlreadyExportingAlert(() => (isProcessing.current = false)); + } + }, + () => { + showFimsExportError(); + isProcessing.current = false; + } + ); + }, [historyExportState, dispatch]); + + // cleanup + React.useEffect( + () => () => { + dispatch(resetFimsHistoryExportState()); + }, + [dispatch] + ); + + const handleExportOnPress = () => { + if (!isProcessing.current) { + Alert.alert( + I18n.t("FIMS.history.exportData.alerts.areYouSure"), + undefined, + [ + { text: I18n.t("global.buttons.cancel"), style: "cancel" }, + { + text: I18n.t("global.buttons.confirm"), + isPreferred: true, + onPress: () => { + isProcessing.current = true; + dispatch(fimsHistoryExport.request()); + } + } + ] + ); + } + }; + + return { + handleExportOnPress + }; +}; diff --git a/ts/features/fims/history/saga/handleExportFimsHistorySaga.ts b/ts/features/fims/history/saga/handleExportFimsHistorySaga.ts new file mode 100644 index 00000000000..e0ca40c646a --- /dev/null +++ b/ts/features/fims/history/saga/handleExportFimsHistorySaga.ts @@ -0,0 +1,47 @@ +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 { withRefreshApiCall } from "../../../fastLogin/saga/utils"; +import { FimsHistoryClient } from "../api/client"; +import { fimsHistoryExport } from "../store/actions"; + +export function* handleExportFimsHistorySaga( + exportHistory: FimsHistoryClient["exports"], + bearerToken: string, + action: ActionType +) { + const exportHistoryRequest = exportHistory({ + Bearer: bearerToken + }); + + try { + const exportHistoryResult = (yield* call( + withRefreshApiCall, + exportHistoryRequest, + action + )) as SagaCallReturnType; + + const resultAction = pipe( + exportHistoryResult, + E.foldW( + _failure => fimsHistoryExport.failure(), + success => { + switch (success.status) { + case 202: + return fimsHistoryExport.success("SUCCESS"); + case 409: + return fimsHistoryExport.success("ALREADY_EXPORTING"); + default: + return fimsHistoryExport.failure(); + } + } + ) + ); + + yield* put(resultAction); + } catch (e: any) { + yield* put(fimsHistoryExport.failure()); + } +} diff --git a/ts/features/fims/history/saga/index.ts b/ts/features/fims/history/saga/index.ts index 7dd5b6fdfe6..fb65e8bc8d0 100644 --- a/ts/features/fims/history/saga/index.ts +++ b/ts/features/fims/history/saga/index.ts @@ -1,7 +1,8 @@ import { takeLatest } from "typed-redux-saga/macro"; import { FimsHistoryClient } from "../api/client"; -import { fimsHistoryGet } from "../store/actions"; +import { fimsHistoryExport, fimsHistoryGet } from "../store/actions"; import { handleGetFimsHistorySaga } from "./handleGetFimsHistorySaga"; +import { handleExportFimsHistorySaga } from "./handleExportFimsHistorySaga"; export function* watchFimsHistorySaga( client: FimsHistoryClient, @@ -13,4 +14,10 @@ export function* watchFimsHistorySaga( client.getConsents, bearerToken ); + yield* takeLatest( + fimsHistoryExport.request, + handleExportFimsHistorySaga, + client.exports, + bearerToken + ); } diff --git a/ts/features/fims/history/screens/HistoryScreen.tsx b/ts/features/fims/history/screens/HistoryScreen.tsx index 067ecef9a31..0945842ecc2 100644 --- a/ts/features/fims/history/screens/HistoryScreen.tsx +++ b/ts/features/fims/history/screens/HistoryScreen.tsx @@ -1,34 +1,34 @@ import { Body, Divider, IOStyles, VSpacer } from "@pagopa/io-app-design-system"; -import { constNull } from "fp-ts/lib/function"; import * as React from "react"; import { FlatList, SafeAreaView, View } from "react-native"; +import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; import { FooterActions } from "../../../../components/ui/FooterActions"; import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel"; import I18n from "../../../../i18n"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; +import { fimsRequiresAppUpdateSelector } from "../../../../store/reducers/backendStatus"; +import { openAppStoreUrl } from "../../../../utils/url"; import { FimsHistoryListItem } from "../components/FimsHistoryListItem"; import { LoadingFimsHistoryItemsFooter } from "../components/FimsHistoryLoaders"; import { fimsHistoryGet } from "../store/actions"; import { + fimsHistoryExportStateSelector, fimsHistoryToUndefinedSelector, isFimsHistoryLoadingSelector } from "../store/selectors"; -import { fimsRequiresAppUpdateSelector } from "../../../../store/reducers/backendStatus"; -import { OperationResultScreenContent } from "../../../../components/screens/OperationResultScreenContent"; -import { openAppStoreUrl } from "../../../../utils/url"; +import { useFimsHistoryExport } from "../hooks/useFimsHistoryResultToasts"; +import * as RemoteValue from "../../../../common/model/RemoteValue"; export const FimsHistoryScreen = () => { const dispatch = useIODispatch(); const requiresAppUpdate = useIOSelector(fimsRequiresAppUpdateSelector); - const isLoading = useIOSelector(isFimsHistoryLoadingSelector); + const isHistoryLoading = useIOSelector(isFimsHistoryLoadingSelector); const consents = useIOSelector(fimsHistoryToUndefinedSelector); + const historyExportState = useIOSelector(fimsHistoryExportStateSelector); + const isHistoryExporting = RemoteValue.isLoading(historyExportState); - React.useEffect(() => { - if (!requiresAppUpdate) { - dispatch(fimsHistoryGet.request({ shouldReloadFromScratch: true })); - } - }, [dispatch, requiresAppUpdate]); + // ---------- HOOKS const fetchMore = React.useCallback(() => { if (consents?.continuationToken) { @@ -40,16 +40,21 @@ export const FimsHistoryScreen = () => { } }, [consents?.continuationToken, dispatch]); - const renderLoadingFooter = () => - isLoading ? ( - 0} - /> - ) : null; useHeaderSecondLevel({ title: I18n.t("FIMS.history.historyScreen.header"), supportRequest: true }); + + React.useEffect(() => { + if (!requiresAppUpdate) { + dispatch(fimsHistoryGet.request({ shouldReloadFromScratch: true })); + } + }, [dispatch, requiresAppUpdate]); + + const { handleExportOnPress } = useFimsHistoryExport(); + + // ---------- APP UPDATE + if (requiresAppUpdate) { return ( { /> ); } + + // ---------- RENDER + + const renderLoadingFooter = () => + isHistoryLoading ? ( + 0} + /> + ) : null; + return ( <> @@ -86,8 +101,9 @@ export const FimsHistoryScreen = () => { actions={{ type: "SingleButton", primary: { + loading: isHistoryExporting, label: I18n.t("FIMS.history.exportData.CTA"), - onPress: constNull // full export functionality coming soon + onPress: handleExportOnPress } }} /> diff --git a/ts/features/fims/history/store/actions/index.ts b/ts/features/fims/history/store/actions/index.ts index 8ff672bc2b5..b9b1064a567 100644 --- a/ts/features/fims/history/store/actions/index.ts +++ b/ts/features/fims/history/store/actions/index.ts @@ -1,5 +1,10 @@ -import { ActionType, createAsyncAction } from "typesafe-actions"; +import { + ActionType, + createAsyncAction, + createStandardAction +} from "typesafe-actions"; import { ConsentsResponseDTO } from "../../../../../../definitions/fims/ConsentsResponseDTO"; +import { FimsExportSuccessStates } from "../reducer"; export type FimsHistoryGetPayloadType = { shouldReloadFromScratch?: boolean; @@ -12,4 +17,16 @@ export const fimsHistoryGet = createAsyncAction( "FIMS_GET_HISTORY_FAILURE" )(); -export type FimsHistoryActions = ActionType; +export const fimsHistoryExport = createAsyncAction( + "FIMS_HISTORY_EXPORT_REQUEST", + "FIMS_HISTORY_EXPORT_SUCCESS", + "FIMS_HISTORY_EXPORT_FAILURE" +)(); + +export const resetFimsHistoryExportState = + createStandardAction("RESET_FIMS_HISTORY")(); + +export type FimsHistoryActions = + | ActionType + | ActionType + | ActionType; diff --git a/ts/features/fims/history/store/reducer/index.ts b/ts/features/fims/history/store/reducer/index.ts index d88e8281e0c..19a45d7f0ae 100644 --- a/ts/features/fims/history/store/reducer/index.ts +++ b/ts/features/fims/history/store/reducer/index.ts @@ -1,14 +1,29 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; import { ConsentsResponseDTO } from "../../../../../../definitions/fims/ConsentsResponseDTO"; +import { + remoteError, + remoteLoading, + remoteReady, + remoteUndefined, + RemoteValue +} from "../../../../../common/model/RemoteValue"; import { Action } from "../../../../../store/actions/types"; -import { fimsHistoryGet } from "../actions"; +import { + fimsHistoryExport, + fimsHistoryGet, + resetFimsHistoryExportState +} from "../actions"; + +export type FimsExportSuccessStates = "SUCCESS" | "ALREADY_EXPORTING"; export type FimsHistoryState = { + historyExportState: RemoteValue; consentsList: pot.Pot; }; const INITIAL_STATE: FimsHistoryState = { + historyExportState: remoteUndefined, consentsList: pot.none }; @@ -20,15 +35,18 @@ const reducer = ( case getType(fimsHistoryGet.request): return action.payload.shouldReloadFromScratch ? { + ...state, consentsList: pot.noneLoading } : { + ...state, consentsList: pot.toLoading(state.consentsList) }; case getType(fimsHistoryGet.success): const currentHistoryItems = pot.toUndefined(state.consentsList)?.items ?? []; return { + ...state, consentsList: pot.some({ ...action.payload, items: [...currentHistoryItems, ...action.payload.items] @@ -36,8 +54,29 @@ const reducer = ( }; case getType(fimsHistoryGet.failure): return { + ...state, consentsList: pot.toError(state.consentsList, action.payload) }; + case getType(fimsHistoryExport.request): + return { + ...state, + historyExportState: remoteLoading + }; + case getType(fimsHistoryExport.success): + return { + ...state, + historyExportState: remoteReady(action.payload) + }; + case getType(fimsHistoryExport.failure): + return { + ...state, + historyExportState: remoteError(null) + }; + case getType(resetFimsHistoryExportState): + return { + ...state, + historyExportState: remoteUndefined + }; } return state; }; diff --git a/ts/features/fims/history/store/selectors/index.ts b/ts/features/fims/history/store/selectors/index.ts index d074b23df3d..90cf153767e 100644 --- a/ts/features/fims/history/store/selectors/index.ts +++ b/ts/features/fims/history/store/selectors/index.ts @@ -22,3 +22,9 @@ export const fimsIsHistoryEnabledSelector = (state: GlobalState) => O.map(backendStatus => backendStatus.config.fims.historyEnabled !== false), O.getOrElse(() => false) ); + +export const fimsHistoryExportStateSelector = (state: GlobalState) => + state.features.fims.history.historyExportState; + +export const isFimsHistoryExportingSelector = (state: GlobalState) => + state.features.fims.history.historyExportState.kind === "loading";