Skip to content

Commit

Permalink
chore: [IOCOM-1560] Fims history screen (#6021)
Browse files Browse the repository at this point in the history
## Short description
Addition of FIMS history screen, with email export coming soon

<img
src="https://github.com/user-attachments/assets/c3d804a1-96f6-489a-9d2e-5d5227731af3"
width=250/>


## List of changes proposed in this pull request
- required i18n keys
- new simplified utility pot fold function
- required selectors
- required screen and components
- full design-perfect screen coming soon due to problematic component
interactions
- new auto fetching/refreshing service data hook

## How to test
using the dev-server, while checked out in the fims testing branch,
navigate to profile>privacy>third party accesses and make sure
everything is working as intended

---------

Co-authored-by: Andrea <[email protected]>
  • Loading branch information
forrest57 and Vangaorth authored Jul 25, 2024
1 parent 3c159a4 commit be51a79
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 38 deletions.
9 changes: 9 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3956,6 +3956,15 @@ permissionRequest:
3: Seleziona “Foto” e consenti l’accesso
cta: Apri Impostazioni
FIMS:
history:
exportData:
CTA: "Richiedi una copia via email"
profileCTA:
title: Accessi a servizi di terze parti
subTitle: Rivedi gli accessi effettuati a servizi di terze parti tramite IO
historyScreen:
header: Accessi a servizi di terze parti
body: Qui puoi vedere tutti gli accessi a servizi esterni che hai fatto tramite IO e richiederne una copia via email
loadingScreen:
abort:
title: "Operazione annullata"
Expand Down
9 changes: 9 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3956,6 +3956,15 @@ permissionRequest:
3: Seleziona “Foto” e consenti l’accesso
cta: Apri Impostazioni
FIMS:
history:
exportData:
CTA: "Richiedi una copia via email"
profileCTA:
title: Accessi a servizi di terze parti
subTitle: Rivedi gli accessi effettuati a servizi di terze parti tramite IO
historyScreen:
header: Accessi a servizi di terze parti
body: Qui puoi vedere tutti gli accessi a servizi esterni che hai fatto tramite IO e richiederne una copia via email
loadingScreen:
abort:
title: "Operazione annullata"
Expand Down
22 changes: 22 additions & 0 deletions ts/features/fims/common/utils/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";
import { ServiceId } from "../../../../../definitions/backend/ServiceId";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { serviceByIdPotSelector } from "../../../services/details/store/reducers";
import { loadServiceDetail } from "../../../services/details/store/actions/details";
import { isStrictNone } from "../../../../utils/pot";

export const useAutoFetchingServiceByIdPot = (serviceId: ServiceId) => {
const dispatch = useIODispatch();
const serviceData = useIOSelector(state =>
serviceByIdPotSelector(state, serviceId)
);
const shouldFetchServiceData = isStrictNone(serviceData);

React.useEffect(() => {
if (shouldFetchServiceData) {
dispatch(loadServiceDetail.request(serviceId));
}
}, [dispatch, serviceId, shouldFetchServiceData]);

return { serviceData };
};
42 changes: 42 additions & 0 deletions ts/features/fims/history/components/FimsHistoryListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ListItemNav } from "@pagopa/io-app-design-system";
import { constNull } from "fp-ts/lib/function";
import * as React from "react";
import { ServiceId } from "../../../../../definitions/backend/ServiceId";
import { ServicePublic } from "../../../../../definitions/backend/ServicePublic";
import { Consent } from "../../../../../definitions/fims/Consent";
import { dateToAccessibilityReadableFormat } from "../../../../utils/accessibility";
import { potFoldWithDefault } from "../../../../utils/pot";
import { useAutoFetchingServiceByIdPot } from "../../common/utils/hooks";
import { LoadingFimsHistoryListItem } from "./FimsHistoryLoaders";

type SuccessListItemProps = {
serviceData: ServicePublic;
consent: Consent;
};
const SuccessListItem = ({ serviceData, consent }: SuccessListItemProps) => (
<ListItemNav
onPress={constNull}
value={serviceData.organization_name}
topElement={{
dateValue: dateToAccessibilityReadableFormat(consent.timestamp)
}}
description={consent.redirect?.display_name}
hideChevron
/>
);
type HistoryListItemProps = {
item: Consent;
};

export const FimsHistoryListItem = ({ item }: HistoryListItemProps) => {
const { serviceData } = useAutoFetchingServiceByIdPot(
item.service_id as ServiceId
);

return potFoldWithDefault(serviceData, {
default: LoadingFimsHistoryListItem,
some: data => <SuccessListItem serviceData={data} consent={item} />,
someError: data => <SuccessListItem serviceData={data} consent={item} />,
someLoading: data => <SuccessListItem serviceData={data} consent={item} />
});
};
25 changes: 25 additions & 0 deletions ts/features/fims/history/components/FimsHistoryLoaders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Divider } from "@pagopa/io-app-design-system";
import * as React from "react";
import { View } from "react-native";
import Placeholder from "rn-placeholder";

export const LoadingFimsHistoryItemsFooter = ({
showFirstDivider
}: {
showFirstDivider?: boolean;
}) => (
<>
{showFirstDivider && <Divider />}
<LoadingFimsHistoryListItem />
<Divider />
<LoadingFimsHistoryListItem />
<Divider />
<LoadingFimsHistoryListItem />
</>
);

export const LoadingFimsHistoryListItem = () => (
<View style={{ paddingVertical: 16 }}>
<Placeholder.Box height={16} width={178} radius={8} animate="fade" />
</View>
);
79 changes: 59 additions & 20 deletions ts/features/fims/history/screens/HistoryScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,76 @@
import { VSpacer } from "@pagopa/io-app-design-system";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { Body, Divider, IOStyles, VSpacer } from "@pagopa/io-app-design-system";
import { constNull } from "fp-ts/lib/function";
import * as React from "react";
import { SafeAreaView, Text, View } from "react-native";
import LoadingScreenContent from "../../../../components/screens/LoadingScreenContent";
import { FlatList, SafeAreaView, View } from "react-native";
import { FooterActions } from "../../../../components/ui/FooterActions";
import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
import I18n from "../../../../i18n";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { FimsHistoryListItem } from "../components/FimsHistoryListItem";
import { LoadingFimsHistoryItemsFooter } from "../components/FimsHistoryLoaders";
import { fimsHistoryGet } from "../store/actions";
import { fimsHistoryPotSelector } from "../store/selectors";
import {
fimsHistoryToUndefinedSelector,
isFimsHistoryLoadingSelector
} from "../store/selectors";

export const FimsHistoryScreen = () => {
const dispatch = useIODispatch();
const historyPot = useIOSelector(fimsHistoryPotSelector);
const isLoading = useIOSelector(isFimsHistoryLoadingSelector);
const consents = useIOSelector(fimsHistoryToUndefinedSelector);

React.useEffect(() => {
dispatch(fimsHistoryGet.request({}));
dispatch(fimsHistoryGet.request({ shouldReloadFromScratch: true }));
}, [dispatch]);

const fetchMore = React.useCallback(() => {
if (consents?.continuationToken) {
dispatch(
fimsHistoryGet.request({
continuationToken: consents.continuationToken
})
);
}
}, [consents?.continuationToken, dispatch]);

const renderLoadingFooter = () =>
isLoading ? (
<LoadingFimsHistoryItemsFooter
showFirstDivider={(consents?.items.length ?? 0) > 0}
/>
) : null;
useHeaderSecondLevel({
title: "History"
title: I18n.t("FIMS.history.historyScreen.header"),
supportRequest: true
});
if (pot.isLoading(historyPot)) {
return <LoadingScreenContent contentTitle="" />;
}
const history = pot.toUndefined(historyPot)?.items ?? [];

return (
<SafeAreaView style={{ flex: 1 }}>
{history.map((item, index) => (
<View key={index}>
<Text>{item.timestamp.toDateString()}</Text>
<VSpacer size={8} />
<>
<SafeAreaView>
<View style={IOStyles.horizontalContentPadding}>
<Body>{I18n.t("FIMS.history.historyScreen.body")}</Body>
</View>
))}
</SafeAreaView>

<VSpacer size={16} />

<FlatList
data={consents?.items}
contentContainerStyle={IOStyles.horizontalContentPadding}
ItemSeparatorComponent={Divider}
keyExtractor={item => item.id}
renderItem={item => <FimsHistoryListItem item={item.item} />}
onEndReached={fetchMore}
ListFooterComponent={renderLoadingFooter}
/>
</SafeAreaView>
<FooterActions
actions={{
type: "SingleButton",
primary: {
label: I18n.t("FIMS.history.exportData.CTA"),
onPress: constNull // full export functionality coming soon
}
}}
/>
</>
);
};
1 change: 1 addition & 0 deletions ts/features/fims/history/store/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ActionType, createAsyncAction } from "typesafe-actions";
import { ConsentsResponseDTO } from "../../../../../../definitions/fims/ConsentsResponseDTO";

export type FimsHistoryGetPayloadType = {
shouldReloadFromScratch?: boolean;
continuationToken?: string;
};

Expand Down
10 changes: 7 additions & 3 deletions ts/features/fims/history/store/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ const reducer = (
): FimsHistoryState => {
switch (action.type) {
case getType(fimsHistoryGet.request):
return {
consentsList: pot.toLoading(state.consentsList)
};
return action.payload.shouldReloadFromScratch
? {
consentsList: pot.noneLoading
}
: {
consentsList: pot.toLoading(state.consentsList)
};
case getType(fimsHistoryGet.success):
const currentHistoryItems =
pot.toUndefined(state.consentsList)?.items ?? [];
Expand Down
7 changes: 7 additions & 0 deletions ts/features/fims/history/store/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as pot from "@pagopa/ts-commons/lib/pot";
import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/lib/function";
import { backendStatusSelector } from "../../../../../store/reducers/backendStatus";
Expand All @@ -6,6 +7,12 @@ import { GlobalState } from "../../../../../store/reducers/types";
export const fimsHistoryPotSelector = (state: GlobalState) =>
state.features.fims.history.consentsList;

export const isFimsHistoryLoadingSelector = (state: GlobalState) =>
pot.isLoading(state.features.fims.history.consentsList);

export const fimsHistoryToUndefinedSelector = (state: GlobalState) =>
pot.toUndefined(state.features.fims.history.consentsList);

// the flag should be treated as enabled when either true or undefined,
// and is defined as an optional bool
export const fimsIsHistoryEnabledSelector = (state: GlobalState) =>
Expand Down
17 changes: 5 additions & 12 deletions ts/features/fims/singleSignOn/components/FimsSuccessBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ListItemHeader,
VSpacer
} from "@pagopa/io-app-design-system";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/Option";
import * as React from "react";
Expand All @@ -23,12 +24,11 @@ import { ServiceId } from "../../../../../definitions/backend/ServiceId";
import { Link } from "../../../../components/core/typography/Link";
import { LoadingSkeleton } from "../../../../components/ui/Markdown/LoadingSkeleton";
import I18n from "../../../../i18n";
import { useIODispatch, useIOSelector } from "../../../../store/hooks";
import { useIODispatch } from "../../../../store/hooks";
import { useIOBottomSheetModal } from "../../../../utils/hooks/bottomSheet";
import { openWebUrl } from "../../../../utils/url";
import { loadServiceDetail } from "../../../services/details/store/actions/details";
import { serviceByIdSelector } from "../../../services/details/store/reducers";
import { logoForService } from "../../../services/home/utils";
import { useAutoFetchingServiceByIdPot } from "../../common/utils/hooks";
import { fimsGetRedirectUrlAndOpenIABAction } from "../store/actions";
import { ConsentData, FimsClaimType } from "../types";

Expand All @@ -41,15 +41,8 @@ export const FimsFlowSuccessBody = ({
const dispatch = useIODispatch();
const serviceId = consents.service_id as ServiceId;

const serviceData = useIOSelector(state =>
serviceByIdSelector(state, serviceId)
);

React.useEffect(() => {
if (serviceData === undefined) {
dispatch(loadServiceDetail.request(serviceId));
}
}, [serviceData, serviceId, dispatch]);
const servicePot = useAutoFetchingServiceByIdPot(serviceId);
const serviceData = pot.toUndefined(servicePot.serviceData);

const serviceLogo = pipe(
serviceData,
Expand Down
5 changes: 2 additions & 3 deletions ts/screens/profile/PrivacyMainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,8 @@ const PrivacyMainScreen = ({ navigation }: Props) => {
const spreadableMaybeFimsHistoryListItem = isFimsHistoryEnabled
? [
{
// TODO: add correct I18n keys
value: "FIMS_HISTORY",
description: "HISTORY_DESC",
value: I18n.t("FIMS.history.profileCTA.title"),
description: I18n.t("FIMS.history.profileCTA.subTitle"),
onPress: () =>
navigation.navigate(FIMS_ROUTES.MAIN, {
screen: FIMS_ROUTES.HISTORY
Expand Down
38 changes: 38 additions & 0 deletions ts/utils/pot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,41 @@ export const isSomeOrSomeError = <A, E>(
pot.isSome(p) && !pot.isLoading(p) && !pot.isUpdating(p);
export const isLoadingOrUpdating = <A, E>(p: pot.Pot<A, E>): boolean =>
pot.isLoading(p) || pot.isUpdating(p);

type PotFoldWithDefaultHandlers<A, E, O> = {
none?: () => O;
noneLoading?: () => O;
noneUpdating?: (newValue: A) => O;
noneError?: (error: E) => O;
some?: (value: A) => O;
someLoading?: (value: A) => O;
someUpdating?: (value: A, newValue: A) => O;
someError?: (value: A, error: E) => O;
} & {
default: (value?: A | E, secondValue?: A | E) => O;
};

/**
* Fold a {@link pot.Pot} using a fallback default function
* @param value
* @param handlers {PotFoldWithDefaultHandlers}
*
* The default handler will be called if any of the args is not defined,
* and will have two optional parameters, which can be *some* or *error*
*
*/
export const potFoldWithDefault = <A, E, O>(
value: pot.Pot<A, E>,
handlers: PotFoldWithDefaultHandlers<A, E, O>
) =>
pot.fold(
value,
handlers.none ?? handlers.default,
handlers.noneLoading ?? handlers.default,
handlers.noneUpdating ?? handlers.default,
handlers.noneError ?? handlers.default,
handlers.some ?? handlers.default,
handlers.someLoading ?? handlers.default,
handlers.someUpdating ?? handlers.default,
handlers.someError ?? handlers.default
);

0 comments on commit be51a79

Please sign in to comment.