diff --git a/locales/it/index.json b/locales/it/index.json index 244cc8d4ce7..cdce7773d1f 100644 --- a/locales/it/index.json +++ b/locales/it/index.json @@ -664,7 +664,38 @@ "email": "Indirizzo email", "need_validate": "Da validare", "nameSurname": "Nome e cognome", - "fiscalCode": "Codice Fiscale" + "fiscalCode": "Codice Fiscale", + "birthDate" : "Data di nascita" + } + }, + "toy" : { + "main" : { + "email": "Indirizzo email", + "nameSurname": "Nome e cognome", + "fiscalCode": "Codice Fiscale", + "birthDate" : "Data di nascita", + "error_none" : "Nessun Profilo Caricato", + "deleteState" : "Stato cancellazione profilo" + }, + "warn" : { + "header" : "Sei avvisato", + "headerTitle" : "Sicuro di voler procedere?", + "message" : "Sicuro **sicuro** Sicuro?", + "buttons" : { + "continue" : "Continua", + "cancel" : "Annulla" + } + }, + "confirm_delete" : { + "header" : "Ecco cosa stai per rimuovere...", + "header_deleted" : "Ecco cosa stai per rimuovere...", + "deleted" : "Hai richiesto correttamente la cancellazione", + "buttons" : { + "continue" : "Sono Sicuro", + "cancel" : "Annulla", + "retry" : "Riprova", + "close" : "Chiudi" + } } }, "security": { @@ -3772,6 +3803,9 @@ } }, "features": { + "profile" : { + "title" : "Profilo" + }, "messages": { "pushNotifications": { "banner": { diff --git a/ts/features/toyProfile/components/ProfileFields.tsx b/ts/features/toyProfile/components/ProfileFields.tsx new file mode 100644 index 00000000000..41515f84406 --- /dev/null +++ b/ts/features/toyProfile/components/ProfileFields.tsx @@ -0,0 +1,75 @@ +import { + Divider, + ListItemInfo, + LoadingSpinner +} from "@pagopa/io-app-design-system"; +import I18n from "i18next"; +import { IOIcons } from "@pagopa/io-app-design-system/src/components/icons/Icon.tsx"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { useIOSelector } from "../../../store/hooks.ts"; +import { toyProfileSelector } from "../store/selectors"; +import { OperationResultScreenContent } from "../../../components/screens/OperationResultScreenContent.tsx"; +import { getNetworkErrorMessage } from "../../../utils/errors.ts"; + +const ProfileFieldItem = ({ + label, + icon, + value +}: { + label: string; + icon: IOIcons; + value?: string; +}) => + value && ( + <> + + + + ); + +export const ProfileFields = () => { + const toyProfilePot = useIOSelector(toyProfileSelector); + + return pot.fold( + toyProfilePot, + () => ( + + ), + () => , + () => , + e => , + profileData => ( + <> + + + + {profileData.date_of_birth && ( + + )} + + ), + () => , + () => , + (_profileData, e) => ( + + ) + ); +}; diff --git a/ts/features/toyProfile/navigation/params.ts b/ts/features/toyProfile/navigation/params.ts new file mode 100644 index 00000000000..3946af0b153 --- /dev/null +++ b/ts/features/toyProfile/navigation/params.ts @@ -0,0 +1,7 @@ +import { TOY_PROFILE_ROUTES } from "./routes.ts"; + +export type ToyProfileParamsList = { + [TOY_PROFILE_ROUTES.PROFILE_MAIN]: undefined; + [TOY_PROFILE_ROUTES.PROFILE_WARN]: undefined; + [TOY_PROFILE_ROUTES.PROFILE_CONFIRM_DELETE]: undefined; +}; diff --git a/ts/features/toyProfile/navigation/routes.ts b/ts/features/toyProfile/navigation/routes.ts new file mode 100644 index 00000000000..f1e74faa164 --- /dev/null +++ b/ts/features/toyProfile/navigation/routes.ts @@ -0,0 +1,5 @@ +export const TOY_PROFILE_ROUTES = { + PROFILE_MAIN: "PROFILE_MAIN", + PROFILE_WARN: "PROFILE_WARN", + PROFILE_CONFIRM_DELETE: "PROFILE_CONFIRM_DELETE" +} as const; diff --git a/ts/features/toyProfile/navigation/toyProfileNavigator.tsx b/ts/features/toyProfile/navigation/toyProfileNavigator.tsx new file mode 100644 index 00000000000..81be2de6b85 --- /dev/null +++ b/ts/features/toyProfile/navigation/toyProfileNavigator.tsx @@ -0,0 +1,34 @@ +import { createStackNavigator } from "@react-navigation/stack"; +import { ProfileHomeScreen } from "../screens/ProfileHomeScreen.tsx"; +import { isGestureEnabled } from "../../../utils/navigation.ts"; +import { ProfileWarnScreen } from "../screens/ProfileWarnScreen.tsx"; +import { ProfileConfirmDeleteScreen } from "../screens/ProfileConfirmDeleteScreen.tsx"; +import { TOY_PROFILE_ROUTES } from "./routes"; +import { ToyProfileParamsList } from "./params.ts"; + +const Stack = createStackNavigator(); + +/** + * A navigator for all the screens of the Settings section + */ +const ToyProfileNavigator = () => ( + + + + + +); + +export default ToyProfileNavigator; diff --git a/ts/features/toyProfile/saga/handleLoadToyProfileSaga.ts b/ts/features/toyProfile/saga/handleLoadToyProfileSaga.ts new file mode 100644 index 00000000000..c397b355c23 --- /dev/null +++ b/ts/features/toyProfile/saga/handleLoadToyProfileSaga.ts @@ -0,0 +1,44 @@ +import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; +import { call, put } from "typed-redux-saga/macro"; +import { BackendClient } from "../../../api/backend.ts"; +import { ReduxSagaEffect, SagaCallReturnType } from "../../../types/utils.ts"; +import { withRefreshApiCall } from "../../authentication/fastLogin/saga/utils"; +import { getToyProfileDetailsAction } from "../store/actions"; +import { getNetworkError } from "../../../utils/errors.ts"; +import { ToyProfileResponse } from "../types"; + +export function* handleLoadToyProfileSaga( + getProfile: ReturnType["getProfile"] +): Generator< + ReduxSagaEffect, + O.Option, + SagaCallReturnType +> { + try { + const resp = (yield* call( + withRefreshApiCall, + getProfile({}) + )) as unknown as SagaCallReturnType; + + if (E.isRight(resp)) { + const { status, value } = resp.right; + + if (status === 200) { + const payload = value as ToyProfileResponse; + yield* put(getToyProfileDetailsAction.success(payload)); + return O.some(payload); + } + + throw new Error(`response status ${status}`); + } + + const messages = resp.left.map(e => e.message).join(", "); + + throw new Error(`error: ${messages}`); + } catch (err) { + // FAILED ACTION + yield* put(getToyProfileDetailsAction.failure(getNetworkError(err))); + } + return O.none; +} diff --git a/ts/features/toyProfile/saga/index.ts b/ts/features/toyProfile/saga/index.ts new file mode 100644 index 00000000000..4322740bc5e --- /dev/null +++ b/ts/features/toyProfile/saga/index.ts @@ -0,0 +1,16 @@ +import { takeLatest } from "typed-redux-saga/macro"; +import { getType } from "typesafe-actions"; +import { BackendClient } from "../../../api/backend.ts"; +import { ReduxSagaEffect } from "../../../types/utils.ts"; +import { getToyProfileDetailsAction } from "../store/actions"; +import { handleLoadToyProfileSaga } from "./handleLoadToyProfileSaga.ts"; + +export function* watchToyProfileSaga( + getProfile: ReturnType["getProfile"] +): Iterator { + yield* takeLatest( + getType(getToyProfileDetailsAction.request), + handleLoadToyProfileSaga, + getProfile + ); +} diff --git a/ts/features/toyProfile/screens/ProfileConfirmDeleteScreen.tsx b/ts/features/toyProfile/screens/ProfileConfirmDeleteScreen.tsx new file mode 100644 index 00000000000..5a067bef520 --- /dev/null +++ b/ts/features/toyProfile/screens/ProfileConfirmDeleteScreen.tsx @@ -0,0 +1,97 @@ +import { ComponentProps } from "react"; +import I18n from "i18next"; +import { useDispatch } from "react-redux"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { ListItemInfo, LoadingSpinner } from "@pagopa/io-app-design-system"; +import { IOScrollView } from "../../../components/ui/IOScrollView.tsx"; +import { useIONavigation } from "../../../navigation/params/AppParamsList.ts"; +import { IOScrollViewWithLargeHeader } from "../../../components/ui/IOScrollViewWithLargeHeader.tsx"; +import { ProfileFields } from "../components/ProfileFields.tsx"; +import { useIOSelector } from "../../../store/hooks.ts"; +import { userDataProcessingSelector } from "../../settings/common/store/selectors/userDataProcessing.ts"; +import { UserDataProcessingChoiceEnum } from "../../../../definitions/backend/UserDataProcessingChoice.ts"; +import { upsertUserDataProcessing } from "../../settings/common/store/actions/userDataProcessing.ts"; + +export const ProfileConfirmDeleteScreen = () => { + const navigation = useIONavigation(); + + const dispatch = useDispatch(); + + const deletePot = useIOSelector( + s => userDataProcessingSelector(s)[UserDataProcessingChoiceEnum.DELETE] + ); + + const delHasValue = pot.isSome(deletePot) && deletePot.value?.status; + + const renderActionProps = (): ComponentProps< + typeof IOScrollView + >["actions"] => { + const onDeletePress = () => { + dispatch( + upsertUserDataProcessing.request(UserDataProcessingChoiceEnum.DELETE) + ); + }; + + if (pot.isError(deletePot)) { + return { + type: "TwoButtons", + primary: { + label: I18n.t("profile.toy.confirm_delete.buttons.cancel"), + onPress: navigation.popToTop + }, + secondary: { + label: I18n.t("profile.toy.confirm_delete.buttons.retry"), + onPress: onDeletePress + } + }; + } + + if (delHasValue) { + return { + type: "SingleButton", + primary: { + label: I18n.t("profile.toy.confirm_delete.buttons.close"), + onPress: navigation.popToTop + } + }; + } + + return { + type: "TwoButtons", + primary: { + label: I18n.t("profile.toy.confirm_delete.buttons.cancel"), + onPress: navigation.popToTop + }, + secondary: { + label: I18n.t("profile.toy.confirm_delete.buttons.continue"), + onPress: onDeletePress + } + }; + }; + + return ( + + {pot.isError(deletePot) && ( + + )} + {pot.isLoading(deletePot) && } + {!delHasValue && } + {delHasValue && ( + + )} + + ); +}; diff --git a/ts/features/toyProfile/screens/ProfileHomeScreen.tsx b/ts/features/toyProfile/screens/ProfileHomeScreen.tsx new file mode 100644 index 00000000000..2d699208f13 --- /dev/null +++ b/ts/features/toyProfile/screens/ProfileHomeScreen.tsx @@ -0,0 +1,82 @@ +import { ContentWrapper, ListItemSwitch } from "@pagopa/io-app-design-system"; +import I18n from "i18next"; +import { useDispatch } from "react-redux"; +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { useEffect } from "react"; +import { emptyContextualHelp } from "../../../utils/emptyContextualHelp.tsx"; +import { IOScrollViewWithLargeHeader } from "../../../components/ui/IOScrollViewWithLargeHeader.tsx"; +import { useIOSelector } from "../../../store/hooks.ts"; +import { loadUserDataProcessing } from "../../settings/common/store/actions/userDataProcessing.ts"; +import { UserDataProcessingChoiceEnum } from "../../../../definitions/backend/UserDataProcessingChoice.ts"; +import { userDataProcessingSelector } from "../../settings/common/store/selectors/userDataProcessing.ts"; +import { UserDataProcessingStatusEnum } from "../../../../definitions/backend/UserDataProcessingStatus.ts"; +import { useIONavigation } from "../../../navigation/params/AppParamsList.ts"; +import { ProfileFields } from "../components/ProfileFields.tsx"; +import { getToyProfileDetailsAction } from "../store/actions"; +import { TOY_PROFILE_ROUTES } from "../navigation/routes.ts"; + +const DELETE_ACTIVE_STATUSES = [ + UserDataProcessingStatusEnum.PENDING, + UserDataProcessingStatusEnum.WIP, + UserDataProcessingStatusEnum.CLOSED +]; + +const ProfileHomeScreen = () => { + const navigation = useIONavigation(); + const dispatch = useDispatch(); + + const deleteUserDataPot = useIOSelector( + s => userDataProcessingSelector(s)[UserDataProcessingChoiceEnum.DELETE] + ); + + useEffect(() => { + dispatch(getToyProfileDetailsAction.request()); + dispatch( + loadUserDataProcessing.request(UserDataProcessingChoiceEnum.DELETE) + ); + }, [dispatch]); + + const deleteRequestStatus = pot.isSome(deleteUserDataPot) + ? deleteUserDataPot.value?.status + : undefined; + + const isDeleteSwitchDisabled = + pot.isLoading(deleteUserDataPot) || + pot.isNone(deleteUserDataPot) || + !!deleteRequestStatus; + + const isDeleteInProgress = + !!deleteRequestStatus && + DELETE_ACTIVE_STATUSES.includes(deleteRequestStatus); + + const handleDeleteSwitchToggle = (checked: boolean) => { + if (checked) { + navigation.navigate(TOY_PROFILE_ROUTES.PROFILE_WARN); + } + }; + + return ( + + + + + + + ); +}; + +export { ProfileHomeScreen }; diff --git a/ts/features/toyProfile/screens/ProfileWarnScreen.tsx b/ts/features/toyProfile/screens/ProfileWarnScreen.tsx new file mode 100644 index 00000000000..15b4301b173 --- /dev/null +++ b/ts/features/toyProfile/screens/ProfileWarnScreen.tsx @@ -0,0 +1,55 @@ +import { FeatureInfo } from "@pagopa/io-app-design-system"; +import { ComponentProps } from "react"; +import I18n from "i18next"; +import { IOScrollViewWithLargeHeader } from "../../../components/ui/IOScrollViewWithLargeHeader.tsx"; +import { IOScrollView } from "../../../components/ui/IOScrollView.tsx"; +import IOMarkdown from "../../../components/IOMarkdown"; +import { useIONavigation } from "../../../navigation/params/AppParamsList.ts"; +import { TOY_PROFILE_ROUTES } from "../navigation/routes.ts"; + +export const ProfileWarnScreen = () => { + const navigation = useIONavigation(); + + const onCancelPress = () => { + navigation.navigate(TOY_PROFILE_ROUTES.PROFILE_MAIN); + }; + + const renderActionProps = (): ComponentProps< + typeof IOScrollView + >["actions"] => ({ + type: "TwoButtons", + primary: { + label: I18n.t("profile.toy.warn.buttons.cancel"), + onPress: onCancelPress, + testID: "addIbanButtonTestID" + }, + secondary: { + label: I18n.t("profile.toy.warn.buttons.continue"), + onPress: () => { + navigation.navigate(TOY_PROFILE_ROUTES.PROFILE_CONFIRM_DELETE); + }, + testID: "continueButtonTestID" + } + }); + + return ( + + } + variant={"neutral"} + /> + + ); +}; diff --git a/ts/features/toyProfile/store/actions/index.ts b/ts/features/toyProfile/store/actions/index.ts new file mode 100644 index 00000000000..8e7ce3bc3bd --- /dev/null +++ b/ts/features/toyProfile/store/actions/index.ts @@ -0,0 +1,12 @@ +import { ActionType, createAsyncAction } from "typesafe-actions"; +import { NetworkError } from "../../../../utils/errors.ts"; +import { ToyProfileResponse } from "../../types"; + +export const getToyProfileDetailsAction = createAsyncAction( + "TOY_PROFILE_DETAILS_REQUEST", + "TOY_PROFILE_DETAILS_SUCCESS", + "TOY_PROFILE_DETAILS_FAILURE", + "TOY_PROFILE_DETAILS_CANCEL" +)(); + +export type ToyProfileActions = ActionType; diff --git a/ts/features/toyProfile/store/reducers/index.ts b/ts/features/toyProfile/store/reducers/index.ts new file mode 100644 index 00000000000..1b7535e625c --- /dev/null +++ b/ts/features/toyProfile/store/reducers/index.ts @@ -0,0 +1,25 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { getType } from "typesafe-actions"; + +import { ToyProfileResponse } from "../../types"; +import { getToyProfileDetailsAction } from "../actions"; +import { Action } from "../../../../store/actions/types.ts"; +import { NetworkError } from "../../../../utils/errors.ts"; + +export type ToyProfileState = pot.Pot; + +export const toyProfileReducer = ( + state: ToyProfileState = pot.none, + action: Action +): ToyProfileState => { + switch (action.type) { + case getType(getToyProfileDetailsAction.request): + return pot.toLoading(state); + case getType(getToyProfileDetailsAction.success): + return pot.some(action.payload); + case getType(getToyProfileDetailsAction.failure): + return pot.toError(state, action.payload); + default: + return state; + } +}; diff --git a/ts/features/toyProfile/store/selectors/index.ts b/ts/features/toyProfile/store/selectors/index.ts new file mode 100644 index 00000000000..b94daeaaa48 --- /dev/null +++ b/ts/features/toyProfile/store/selectors/index.ts @@ -0,0 +1,3 @@ +import { GlobalState } from "../../../../store/reducers/types.ts"; + +export const toyProfileSelector = (state: GlobalState) => state.toyProfile; diff --git a/ts/features/toyProfile/types/index.ts b/ts/features/toyProfile/types/index.ts new file mode 100644 index 00000000000..72c76553193 --- /dev/null +++ b/ts/features/toyProfile/types/index.ts @@ -0,0 +1,7 @@ +export type ToyProfileResponse = { + name: string; + family_name: string; + fiscal_code: string; + email: string; + date_of_birth?: Date; +}; diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index 53384007264..9416340974f 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -71,6 +71,7 @@ import { } from "../store/reducers/backendStatus/remoteConfig"; import { isGestureEnabled } from "../utils/navigation"; import OnboardingNavigator from "../features/onboarding/navigation/OnboardingNavigator.tsx"; +import ToyProfileNavigator from "../features/toyProfile/navigation/toyProfileNavigator.tsx"; import { AppParamsList } from "./params/AppParamsList"; import ROUTES from "./routes"; import { MainTabNavigator } from "./TabNavigator"; @@ -177,7 +178,7 @@ const AuthenticatedStackNavigator = () => { ...TransitionPresets.SlideFromRightIOS, gestureEnabled: isGestureEnabled }} - component={SettingsStackNavigator} + component={ToyProfileNavigator} /> ; [ITW_REMOTE_ROUTES.MAIN]: NavigatorScreenParams; [SERVICES_ROUTES.SERVICES_HOME]: undefined; + + [TOY_PROFILE_ROUTES.PROFILE_MAIN]: undefined; + [TOY_PROFILE_ROUTES.PROFILE_WARN]: undefined; + [TOY_PROFILE_ROUTES.PROFILE_CONFIRM_DELETE]: undefined; }; /** diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index f88367e567b..8ba9d2a10b0 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -139,6 +139,7 @@ import { waitForNavigatorServiceInitialization } from "../navigation/saga/navigation"; import { checkShouldDisplaySendEngagementScreen } from "../features/pn/loginEngagement/sagas/checkShouldDisplaySendEngagementScreen"; +import { watchToyProfileSaga } from "../features/toyProfile/saga"; import { previousInstallationDataDeleteSaga } from "./installation"; import { askMixpanelOptIn, @@ -465,6 +466,7 @@ export function* initializeApplicationSaga( // Start watching for requests of refresh the profile yield* fork(watchProfileRefreshRequestsSaga, backendClient.getProfile); + yield* fork(watchToyProfileSaga, backendClient.getProfile); // Start watching for requests about session and support token yield* fork( diff --git a/ts/store/actions/types.ts b/ts/store/actions/types.ts index 6f9309aad84..37977b717fa 100644 --- a/ts/store/actions/types.ts +++ b/ts/store/actions/types.ts @@ -53,6 +53,7 @@ import { WhatsNewActions } from "../../features/whatsnew/store/actions"; import { ZendeskSupportActions } from "../../features/zendesk/store/actions"; import { GlobalState } from "../reducers/types"; import { SENDLoginEngagementActions } from "../../features/pn/loginEngagement/store/actions"; +import { ToyProfileActions } from "../../features/toyProfile/store/actions"; import { AnalyticsActions } from "./analytics"; import { ApplicationActions } from "./application"; import { BackendStatusActions } from "./backendStatus"; @@ -125,7 +126,8 @@ export type Action = | LoginPreferencesActions | AARFlowStateActions | BackgroundLinkingActions - | SENDLoginEngagementActions; + | SENDLoginEngagementActions + | ToyProfileActions; export type Dispatch = DispatchAPI; diff --git a/ts/store/reducers/index.ts b/ts/store/reducers/index.ts index d400a70c6bb..cb8bd601215 100644 --- a/ts/store/reducers/index.ts +++ b/ts/store/reducers/index.ts @@ -51,6 +51,7 @@ import { isDevEnv } from "../../utils/environment"; import { Action } from "../actions/types"; import createSecureStorage from "../storages/keychain"; import { DateISO8601Transform } from "../transforms/dateISO8601Tranform"; +import { toyProfileReducer } from "../../features/toyProfile/store/reducers"; import appStateReducer from "./appState"; import assistanceToolsReducer from "./assistanceTools"; import { backendInfoReducer } from "./backendStatus/backendInfo"; @@ -125,6 +126,7 @@ export const appReducer: Reducer = combineReducers< // // ephemeral state // + toyProfile: toyProfileReducer, appState: appStateReducer, navigation: navigationReducer, versionInfo: versionInfoReducer, @@ -198,6 +200,7 @@ export function createRootReducer( // eslint-disable-next-line no-param-reassign state = state ? ({ + toyProfile: state.toyProfile, authentication: { ...authenticationInitialState, diff --git a/ts/store/reducers/types.ts b/ts/store/reducers/types.ts index c27fee41b4a..c8dff4f9548 100644 --- a/ts/store/reducers/types.ts +++ b/ts/store/reducers/types.ts @@ -13,6 +13,7 @@ import { PersistedNotificationsState } from "../../features/pushNotifications/st import { ProfileState } from "../../features/settings/common/store/reducers"; import { UserDataProcessingState } from "../../features/settings/common/store/reducers/userDataProcessing"; import { TrialSystemState } from "../../features/trialSystem/store/reducers"; +import { ToyProfileState } from "../../features/toyProfile/store/reducers"; import { AppState } from "./appState"; import { AssistanceToolsState } from "./assistanceTools"; import { BackedInfoState } from "./backendStatus/backendInfo"; @@ -60,6 +61,7 @@ export type GlobalState = Readonly<{ startup: StartupState; lollipop: PersistedLollipopState; trialSystem: TrialSystemState; + toyProfile: ToyProfileState; }>; export type PersistedGlobalState = GlobalState & PersistPartial;