Skip to content

Commit

Permalink
[IOCOM-1338, IOCOM-1562, IOCOM-1563] FIMS history export feature (#6091)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
forrest57 and Vangaorth authored Aug 9, 2024
1 parent 3665c93 commit b0f8a2e
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 21 deletions.
7 changes: 7 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions ts/features/fims/history/hooks/useFimsHistoryResultToasts.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(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
};
};
47 changes: 47 additions & 0 deletions ts/features/fims/history/saga/handleExportFimsHistorySaga.ts
Original file line number Diff line number Diff line change
@@ -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<typeof fimsHistoryExport.request>
) {
const exportHistoryRequest = exportHistory({
Bearer: bearerToken
});

try {
const exportHistoryResult = (yield* call(
withRefreshApiCall,
exportHistoryRequest,
action
)) as SagaCallReturnType<typeof exportHistory>;

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());
}
}
9 changes: 8 additions & 1 deletion ts/features/fims/history/saga/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,4 +14,10 @@ export function* watchFimsHistorySaga(
client.getConsents,
bearerToken
);
yield* takeLatest(
fimsHistoryExport.request,
handleExportFimsHistorySaga,
client.exports,
bearerToken
);
}
50 changes: 33 additions & 17 deletions ts/features/fims/history/screens/HistoryScreen.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -40,16 +40,21 @@ export const FimsHistoryScreen = () => {
}
}, [consents?.continuationToken, dispatch]);

const renderLoadingFooter = () =>
isLoading ? (
<LoadingFimsHistoryItemsFooter
showFirstDivider={(consents?.items.length ?? 0) > 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 (
<OperationResultScreenContent
Expand All @@ -63,6 +68,16 @@ export const FimsHistoryScreen = () => {
/>
);
}

// ---------- RENDER

const renderLoadingFooter = () =>
isHistoryLoading ? (
<LoadingFimsHistoryItemsFooter
showFirstDivider={(consents?.items.length ?? 0) > 0}
/>
) : null;

return (
<>
<SafeAreaView>
Expand All @@ -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
}
}}
/>
Expand Down
21 changes: 19 additions & 2 deletions ts/features/fims/history/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,4 +17,16 @@ export const fimsHistoryGet = createAsyncAction(
"FIMS_GET_HISTORY_FAILURE"
)<FimsHistoryGetPayloadType, ConsentsResponseDTO, string>();

export type FimsHistoryActions = ActionType<typeof fimsHistoryGet>;
export const fimsHistoryExport = createAsyncAction(
"FIMS_HISTORY_EXPORT_REQUEST",
"FIMS_HISTORY_EXPORT_SUCCESS",
"FIMS_HISTORY_EXPORT_FAILURE"
)<void, FimsExportSuccessStates, void>();

export const resetFimsHistoryExportState =
createStandardAction("RESET_FIMS_HISTORY")<void>();

export type FimsHistoryActions =
| ActionType<typeof fimsHistoryGet>
| ActionType<typeof fimsHistoryExport>
| ActionType<typeof resetFimsHistoryExportState>;
41 changes: 40 additions & 1 deletion ts/features/fims/history/store/reducer/index.ts
Original file line number Diff line number Diff line change
@@ -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<FimsExportSuccessStates, null>;
consentsList: pot.Pot<ConsentsResponseDTO, string>;
};

const INITIAL_STATE: FimsHistoryState = {
historyExportState: remoteUndefined,
consentsList: pot.none
};

Expand All @@ -20,24 +35,48 @@ 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]
})
};
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;
};
Expand Down
Loading

0 comments on commit b0f8a2e

Please sign in to comment.