From e382d2663aefd8d43fab7719536c4c2236e951d1 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:41:47 +0100 Subject: [PATCH 01/35] add new BE values and add business logic to integrate the email verification flow --- locales/en/index.yml | 3 + locales/it/index.yml | 3 + ts/sagas/profile.ts | 14 ++- .../NewOnboardingEmailInsertScreen.tsx | 113 +++++++++--------- ts/screens/profile/EmailInsertScreen.tsx | 16 ++- ts/store/actions/profile.ts | 3 +- ts/store/reducers/profile.ts | 12 +- ts/store/reducers/profileErrorType.ts | 28 +++++ 8 files changed, 130 insertions(+), 62 deletions(-) create mode 100644 ts/store/reducers/profileErrorType.ts diff --git a/locales/en/index.yml b/locales/en/index.yml index 8abb92754e6..4954d8d369e 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -771,6 +771,9 @@ email: help: title: Email content: !include email_insert_help.md + alertTitle: Questa email è già in uso + alertDescription: Può succedere se condividi lo stesso indirizzo con un familiare + alertButton: Usa un’altra email newinsert: header: "Configure IO" title: "What is your email address?" diff --git a/locales/it/index.yml b/locales/it/index.yml index 6ce052d6766..597f5f2ad0a 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -771,6 +771,9 @@ email: help: title: Email content: !include email_insert_help.md + alertTitle: Questa email è già in uso + alertDescription: Può succedere se condividi lo stesso indirizzo con un familiare + alertButton: Usa un’altra email newinsert: header: Configura IO title: Qual è la tua email? diff --git a/ts/sagas/profile.ts b/ts/sagas/profile.ts index 6dab2f309e3..1546f517a38 100644 --- a/ts/sagas/profile.ts +++ b/ts/sagas/profile.ts @@ -52,6 +52,7 @@ import { } from "../utils/locale"; import { readablePrivacyReport } from "../utils/reporters"; import { withRefreshApiCall } from "../features/fastLogin/saga/utils"; +import { ProfileError } from "../store/reducers/profileErrorType"; // A saga to load the Profile. export function* loadProfile( @@ -135,6 +136,7 @@ function* createOrUpdateProfileSaga( is_inbox_enabled: currentProfile.is_inbox_enabled, is_webhook_enabled: currentProfile.is_webhook_enabled, is_email_validated: currentProfile.is_email_validated || false, + is_email_already_taken: currentProfile.is_email_already_taken, is_email_enabled: currentProfile.is_email_enabled, version: currentProfile.version, email: currentProfile.email, @@ -157,6 +159,7 @@ function* createOrUpdateProfileSaga( is_email_validated: action.payload.is_email_validated || false, is_email_enabled: action.payload.is_email_enabled || false, last_app_version: currentProfile.last_app_version ?? appVersion, + is_email_already_taken: currentProfile.is_email_already_taken, ...action.payload, accepted_tos_version: tosVersion, version: 0 @@ -180,7 +183,16 @@ function* createOrUpdateProfileSaga( // app has a different version of profile compared to that one owned by the backend // so we force profile reloading (see https://www.pivotaltracker.com/n/projects/2048617/stories/171994417) yield* put(profileLoadRequest()); - throw new Error(response.right.value.title); + throw new ProfileError( + response.right.value.title, + "PROFILE_EMAIL_VALIDATION_ERROR" + ); + } + if (response.right.status === 412) { + throw new ProfileError( + response.right.value.title, + "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR" + ); } if (response.right.status !== 200) { diff --git a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx index ada425e4c56..b73ebf7829d 100644 --- a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx +++ b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx @@ -8,8 +8,13 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { Content } from "native-base"; -import * as React from "react"; -import { useCallback, useMemo, useState } from "react"; +import React, { + useCallback, + useMemo, + useState, + useEffect, + useContext +} from "react"; import { View, Keyboard, SafeAreaView, StyleSheet, Alert } from "react-native"; import validator from "validator"; import { @@ -33,6 +38,7 @@ import { OnboardingParamsList } from "../../navigation/params/OnboardingParamsLi import { profileLoadRequest, profileUpsert } from "../../store/actions/profile"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { + isProfileEmailAlreadyTakenSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; @@ -42,6 +48,9 @@ import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; import ROUTES from "../../navigation/routes"; import { emailInsert } from "../../store/actions/onboarding"; +import { usePrevious } from "../../utils/hooks/usePrevious"; +import { LightModalContext } from "../../components/ui/LightModal"; +import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; type Props = IOStackNavigationRouteProps< OnboardingParamsList, @@ -65,21 +74,20 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * A screen to allow user to insert an email address. */ const NewOnboardingEmailInsertScreen = (props: Props) => { + const { showModal } = useContext(LightModalContext); const dispatch = useIODispatch(); - // FIXME - < https://pagopa.atlassian.net/browse/IOPID-690> change this state logic and name (this value will be retrive by the backend) - const [isCduEmail] = useState(false); - const viewRef = React.createRef(); - const profile = useIOSelector(profileSelector); + const prevUserProfile = usePrevious(profile); const optionEmail = useIOSelector(profileEmailSelector); - + const isProfileEmailAlreadyTaken = useIOSelector( + isProfileEmailAlreadyTakenSelector + ); const isLoading = useMemo( () => pot.isUpdating(profile) || pot.isLoading(profile), [profile] ); - const reloadProfile = useCallback( () => dispatch(profileLoadRequest()), [dispatch] @@ -98,8 +106,10 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { ), [dispatch] ); - - const [email, setEmail] = useState(isCduEmail ? optionEmail : O.none); + const [areSameEmails, setAreSameEmails] = useState(false); + const [email, setEmail] = useState( + isProfileEmailAlreadyTaken ? optionEmail : O.none + ); /** validate email returning three possible values: * - _true_, if email is valid. @@ -115,7 +125,7 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { if ( EMPTY_EMAIL === value || !validator.isEmail(value) || - isSameEmailToChange() + areSameEmails ) { return undefined; } @@ -124,36 +134,6 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { O.toUndefined ); - /** - * This function control if the email is the same that the user need to change - * @returns boolean - */ - const isSameEmailToChange = () => - !isCduEmail ? areStringsEqual(email, optionEmail, true) : false; - - /** - * This function control if the email is already used - * @returns boolean - * - * FIXME - < https://pagopa.atlassian.net/browse/IOPID-690> this function need to be integrated with API that control if the email already exists - */ - const isExistingEmail = () => { - const showAlertExistsEmail: boolean = false; - if (showAlertExistsEmail) { - Alert.alert( - I18n.t("email.newinsert.alert.modaltitle"), - I18n.t("email.newinsert.alert.modaldescription"), - [ - { - text: I18n.t("email.newinsert.alert.modalbutton"), - style: "cancel" - } - ] - ); - } - return showAlertExistsEmail; - }; - const navigateToEmailReadScreen = useCallback(() => { props.navigation.dispatch(StackActions.popToTop()); props.navigation.navigate(ROUTES.ONBOARDING, { @@ -161,19 +141,37 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { }); }, [props.navigation]); + useEffect(() => { + if (prevUserProfile && prevUserProfile !== profile) { + if (pot.isError(profile)) { + if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { + Alert.alert( + I18n.t("email.newinsert.alert.modaltitle"), + I18n.t("email.newinsert.alert.modaldescription"), + [ + { + text: I18n.t("email.newinsert.alert.modalbutton"), + style: "cancel" + } + ] + ); + } + } else { + showModal(); + } + } + }, [profile, prevUserProfile, navigateToEmailReadScreen, showModal]); + const continueOnPress = () => { Keyboard.dismiss(); - if (!isExistingEmail()) { - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - acknowledgeEmailInsert(); - reloadProfile(); - navigateToEmailReadScreen(); - } + pipe( + email, + O.map(e => { + updateEmail(e as EmailString); + }) + ); + acknowledgeEmailInsert(); + reloadProfile(); }; const renderFooterButtons = () => { @@ -194,6 +192,7 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { }; const handleOnChangeEmailText = (value: string) => { + setAreSameEmails(areStringsEqual(O.some(value), optionEmail, true)); setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); }; @@ -216,7 +215,7 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { {I18n.t("email.newinsert.subtitle")} - {!isCduEmail && ( + {isProfileEmailAlreadyTaken && ( <> { label={I18n.t("email.newinsert.label")} icon="email" isValid={isValidEmail()} - overrideBorderColor={ - isSameEmailToChange() ? IOColors.red : undefined - } + overrideBorderColor={areSameEmails ? IOColors.red : undefined} inputProps={{ returnKeyType: "done", onSubmitEditing: continueOnPress, autoCapitalize: "none", keyboardType: "email-address", - defaultValue: isCduEmail + defaultValue: !isProfileEmailAlreadyTaken ? pipe( email, O.getOrElse(() => EMPTY_EMAIL) @@ -257,7 +254,7 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { }} testID="TextField" /> - {isSameEmailToChange() && ( + {areSameEmails && ( { useEffect(() => { if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { + // the user is trying to enter an email already in use + if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { + Alert.alert( + I18n.t("email.insert.alertTitle"), + I18n.t("email.insert.alertDescription"), + [ + { + text: I18n.t("email.insert.alertButton"), + style: "cancel" + } + ] + ); + } else { + showToast(I18n.t("email.edit.upsert_ko"), "danger"); + } // display a toast with error - showToast(I18n.t("email.edit.upsert_ko"), "danger"); } else if (pot.isSome(profile)) { // user is inserting his email from onboarding phase // he comes from checkAcknowledgedEmailSaga if onboarding is not finished yet diff --git a/ts/store/actions/profile.ts b/ts/store/actions/profile.ts index 1a08e922046..ce76d72863c 100644 --- a/ts/store/actions/profile.ts +++ b/ts/store/actions/profile.ts @@ -11,6 +11,7 @@ import { createStandardAction } from "typesafe-actions"; import { InitializedProfile } from "../../../definitions/backend/InitializedProfile"; +import { ProfileError } from "../reducers/profileErrorType"; export const resetProfileState = createStandardAction("RESET_PROFILE_STATE")(); @@ -37,7 +38,7 @@ export const profileUpsert = createAsyncAction( "PROFILE_UPSERT_REQUEST", "PROFILE_UPSERT_SUCCESS", "PROFILE_UPSERT_FAILURE" -)(); +)(); export const startEmailValidation = createAsyncAction( "START_EMAIL_VALIDATION_REQUEST", diff --git a/ts/store/reducers/profile.ts b/ts/store/reducers/profile.ts index b8a97e85d3a..16141bcb8df 100644 --- a/ts/store/reducers/profile.ts +++ b/ts/store/reducers/profile.ts @@ -22,8 +22,9 @@ import { ServicesPreferencesModeEnum } from "../../../definitions/backend/Servic import { ReminderStatusEnum } from "../../../definitions/backend/ReminderStatus"; import { PushNotificationsContentTypeEnum } from "../../../definitions/backend/PushNotificationsContentType"; import { GlobalState } from "./types"; +import { ProfileError } from "./profileErrorType"; -export type ProfileState = pot.Pot; +export type ProfileState = pot.Pot; const INITIAL_STATE: ProfileState = pot.none; @@ -122,6 +123,15 @@ export const profileServicePreferencesModeSelector = createSelector( undefined ) ); +// return if the profile email user is already taken +export const isProfileEmailAlreadyTakenSelector = createSelector( + profileSelector, + (profile: ProfileState): boolean | undefined => + pot.getOrElse( + pot.map(profile, p => p.is_email_already_taken), + undefined + ) +); // return true if the profile services preference mode is set (mode is set only when AUTO or MANUAL is the current mode) export const isServicesPreferenceModeSet = ( diff --git a/ts/store/reducers/profileErrorType.ts b/ts/store/reducers/profileErrorType.ts new file mode 100644 index 00000000000..d50abb60e79 --- /dev/null +++ b/ts/store/reducers/profileErrorType.ts @@ -0,0 +1,28 @@ +type ProfileErrorType = + | "PROFILE_EMAIL_VALIDATION_ERROR" + | "PROFILE_GENRIC_ERROR" + | "PROFILE_LOAD_ERROR" + | "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR"; +export class ProfileError extends Error { + public readonly type?: ProfileErrorType; + + constructor( + message: string | undefined, + type: ProfileErrorType = "PROFILE_GENRIC_ERROR" + ) { + // Pass parent constructor parameters + super(message); + + // Maintains stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ProfileError); + } + + this.name = "ProfileError"; + if (message) { + this.message = message; + } + // Set custom information + this.type = type; + } +} From 12a30eb2002713283557b1e326481bbb46946617 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:33:41 +0100 Subject: [PATCH 02/35] fix a part of business logic --- .../NewOnboardingEmailInsertScreen.tsx | 108 +++++++----------- ts/screens/profile/EmailInsertScreen.tsx | 2 + 2 files changed, 44 insertions(+), 66 deletions(-) diff --git a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx index b73ebf7829d..9cc1d50c4ea 100644 --- a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx +++ b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx @@ -8,13 +8,7 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { Content } from "native-base"; -import React, { - useCallback, - useMemo, - useState, - useEffect, - useContext -} from "react"; +import React, { useCallback, useMemo, useState, useEffect } from "react"; import { View, Keyboard, SafeAreaView, StyleSheet, Alert } from "react-native"; import validator from "validator"; import { @@ -24,7 +18,6 @@ import { VSpacer, Alert as AlertComponent } from "@pagopa/io-app-design-system"; -import { StackActions } from "@react-navigation/native"; import { H1 } from "../../components/core/typography/H1"; import { LabelledItem } from "../../components/LabelledItem"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; @@ -33,9 +26,7 @@ import BaseScreenComponent, { } from "../../components/screens/BaseScreenComponent"; import FooterWithButtons from "../../components/ui/FooterWithButtons"; import I18n from "../../i18n"; -import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; -import { OnboardingParamsList } from "../../navigation/params/OnboardingParamsList"; -import { profileLoadRequest, profileUpsert } from "../../store/actions/profile"; +import { profileUpsert } from "../../store/actions/profile"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { isProfileEmailAlreadyTakenSelector, @@ -46,16 +37,8 @@ import { withKeyboard } from "../../utils/keyboard"; import { areStringsEqual } from "../../utils/options"; import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import ROUTES from "../../navigation/routes"; -import { emailInsert } from "../../store/actions/onboarding"; -import { usePrevious } from "../../utils/hooks/usePrevious"; -import { LightModalContext } from "../../components/ui/LightModal"; -import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; - -type Props = IOStackNavigationRouteProps< - OnboardingParamsList, - "ONBOARDING_INSERT_EMAIL_SCREEN" ->; +import { emailAcknowledged, emailInsert } from "../../store/actions/onboarding"; +import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; const styles = StyleSheet.create({ flex: { @@ -73,13 +56,12 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { /** * A screen to allow user to insert an email address. */ -const NewOnboardingEmailInsertScreen = (props: Props) => { - const { showModal } = useContext(LightModalContext); +const NewOnboardingEmailInsertScreen = () => { + useValidatedEmailModal(true); const dispatch = useIODispatch(); const viewRef = React.createRef(); const profile = useIOSelector(profileSelector); - const prevUserProfile = usePrevious(profile); const optionEmail = useIOSelector(profileEmailSelector); const isProfileEmailAlreadyTaken = useIOSelector( isProfileEmailAlreadyTakenSelector @@ -88,8 +70,9 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { () => pot.isUpdating(profile) || pot.isLoading(profile), [profile] ); - const reloadProfile = useCallback( - () => dispatch(profileLoadRequest()), + + const acknowledgeEmail = useCallback( + () => dispatch(emailAcknowledged()), [dispatch] ); @@ -108,59 +91,54 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { ); const [areSameEmails, setAreSameEmails] = useState(false); const [email, setEmail] = useState( - isProfileEmailAlreadyTaken ? optionEmail : O.none + isProfileEmailAlreadyTaken ? optionEmail : O.some(EMPTY_EMAIL) + ); + const isValidEmail = useCallback( + () => + pipe( + email, + O.map(value => { + if ( + EMPTY_EMAIL === value || + !validator.isEmail(value) || + areSameEmails + ) { + return undefined; + } + return E.isRight(EmailString.decode(value)); + }), + O.toUndefined + ), + [areSameEmails, email] ); - - /** validate email returning three possible values: - * - _true_, if email is valid. - * - _false_, if email has been already changed from the user and it is not - * valid. - * - _undefined_, if email field is empty. This state is consumed by - * LabelledItem Component and it used for style pourposes ONLY. - */ - const isValidEmail = () => - pipe( - email, - O.map(value => { - if ( - EMPTY_EMAIL === value || - !validator.isEmail(value) || - areSameEmails - ) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ); - - const navigateToEmailReadScreen = useCallback(() => { - props.navigation.dispatch(StackActions.popToTop()); - props.navigation.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_READ_EMAIL_SCREEN - }); - }, [props.navigation]); useEffect(() => { - if (prevUserProfile && prevUserProfile !== profile) { + if (!pot.isLoading(profile)) { if (pot.isError(profile)) { + // the user is trying to enter an email already in use if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { Alert.alert( - I18n.t("email.newinsert.alert.modaltitle"), - I18n.t("email.newinsert.alert.modaldescription"), + I18n.t("email.insert.alertTitle"), + I18n.t("email.insert.alertDescription"), [ { - text: I18n.t("email.newinsert.alert.modalbutton"), + text: I18n.t("email.insert.alertButton"), style: "cancel" } ] ); } - } else { - showModal(); + } else if (pot.isSome(profile)) { + acknowledgeEmailInsert(); + return; } } - }, [profile, prevUserProfile, navigateToEmailReadScreen, showModal]); + + // FIXME -> this acknowledgeEmail need to be called after the email verification. Need to test if in a real case the validation modal works. + return () => { + acknowledgeEmail(); + }; + }, [acknowledgeEmailInsert, acknowledgeEmail, profile]); const continueOnPress = () => { Keyboard.dismiss(); @@ -170,8 +148,6 @@ const NewOnboardingEmailInsertScreen = (props: Props) => { updateEmail(e as EmailString); }) ); - acknowledgeEmailInsert(); - reloadProfile(); }; const renderFooterButtons = () => { diff --git a/ts/screens/profile/EmailInsertScreen.tsx b/ts/screens/profile/EmailInsertScreen.tsx index 2ec9f365c27..f39bb541b57 100644 --- a/ts/screens/profile/EmailInsertScreen.tsx +++ b/ts/screens/profile/EmailInsertScreen.tsx @@ -36,6 +36,7 @@ import { areStringsEqual } from "../../utils/options"; import { showToast } from "../../utils/showToast"; import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; +import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; type Props = IOStackNavigationRouteProps< ProfileParamsList, @@ -60,6 +61,7 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * A screen to allow user to insert an email address. */ const EmailInsertScreen = (props: Props) => { + useValidatedEmailModal(); const dispatch = useIODispatch(); const profile = useIOSelector(profileSelector); From 2debae4fc2b750545c24de461d6b6c6affe81817 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:54:28 +0100 Subject: [PATCH 03/35] fix error on useeffect --- .../NewOnboardingEmailInsertScreen.tsx | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx index 9cc1d50c4ea..fdeabf4cdba 100644 --- a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx +++ b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx @@ -8,7 +8,13 @@ import * as E from "fp-ts/lib/Either"; import { pipe } from "fp-ts/lib/function"; import * as O from "fp-ts/lib/Option"; import { Content } from "native-base"; -import React, { useCallback, useMemo, useState, useEffect } from "react"; +import React, { + useCallback, + useMemo, + useState, + useEffect, + createRef +} from "react"; import { View, Keyboard, SafeAreaView, StyleSheet, Alert } from "react-native"; import validator from "validator"; import { @@ -39,6 +45,7 @@ import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { emailAcknowledged, emailInsert } from "../../store/actions/onboarding"; import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; +import { usePrevious } from "../../utils/hooks/usePrevious"; const styles = StyleSheet.create({ flex: { @@ -60,12 +67,15 @@ const NewOnboardingEmailInsertScreen = () => { useValidatedEmailModal(true); const dispatch = useIODispatch(); - const viewRef = React.createRef(); + const viewRef = createRef(); + const profile = useIOSelector(profileSelector); const optionEmail = useIOSelector(profileEmailSelector); const isProfileEmailAlreadyTaken = useIOSelector( isProfileEmailAlreadyTakenSelector ); + const prevUserProfile = usePrevious(profile); + const isLoading = useMemo( () => pot.isUpdating(profile) || pot.isLoading(profile), [profile] @@ -80,6 +90,7 @@ const NewOnboardingEmailInsertScreen = () => { () => dispatch(emailInsert()), [dispatch] ); + const updateEmail = useCallback( (email: EmailString) => dispatch( @@ -113,7 +124,7 @@ const NewOnboardingEmailInsertScreen = () => { ); useEffect(() => { - if (!pot.isLoading(profile)) { + if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { // the user is trying to enter an email already in use if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { @@ -128,17 +139,13 @@ const NewOnboardingEmailInsertScreen = () => { ] ); } - } else if (pot.isSome(profile)) { + } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { acknowledgeEmailInsert(); + acknowledgeEmail(); return; } } - - // FIXME -> this acknowledgeEmail need to be called after the email verification. Need to test if in a real case the validation modal works. - return () => { - acknowledgeEmail(); - }; - }, [acknowledgeEmailInsert, acknowledgeEmail, profile]); + }, [acknowledgeEmailInsert, acknowledgeEmail, profile, prevUserProfile]); const continueOnPress = () => { Keyboard.dismiss(); From e1994494b414484119dc3138835ab9b86b257379 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:49:03 +0100 Subject: [PATCH 04/35] finish to implement the logic --- package.json | 2 +- .../NewRemindEmailValidationOverlay.tsx | 14 +++-- ts/sagas/profile.ts | 9 ++- .../NewOnboardingEmailInsertScreen.tsx | 57 +++++++++++++++---- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index f5f59fe9e49..40d855be496 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "italia-app", "version": "2.44.0-rc.4", - "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.18.0-RELEASE/api_backend.yaml", + "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/IOPID-1045-1050/api_backend.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.18.0-RELEASE/api_public.yaml", "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", "io_bonus_vacanze_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.18.0-RELEASE/api_bonus.yaml", diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 7e45f34d3a3..7eda48ee660 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -30,6 +30,7 @@ import { useIODispatch, useIOSelector } from "../store/hooks"; import { emailValidationSelector } from "../store/reducers/emailValidation"; import NavigationService from "../navigation/NavigationService"; import ROUTES from "../navigation/routes"; +import { emailAcknowledged } from "../store/actions/onboarding"; import { IOStyles } from "./core/variables/IOStyles"; import FooterWithButtons from "./ui/FooterWithButtons"; import { IOToast } from "./Toast"; @@ -70,7 +71,10 @@ const NewRemindEmailValidationOverlay = (props: Props) => { () => dispatch(startEmailValidation.request()), [dispatch] ); - + const acknowledgeEmail = useCallback( + () => dispatch(emailAcknowledged()), + [dispatch] + ); const reloadProfile = useCallback( () => dispatch(profileLoadRequest()), [dispatch] @@ -100,6 +104,11 @@ const NewRemindEmailValidationOverlay = (props: Props) => { const handleSendEmailValidationButton = () => { if (isEmailValidated) { + if (isOnboarding) { + // if the user is in the onboarding flow and the email il correctly validated, + // the email validation flow is finished + acknowledgeEmail(); + } hideModal(); } else { // send email validation only if it exists @@ -115,9 +124,6 @@ const NewRemindEmailValidationOverlay = (props: Props) => { const navigateToInsertEmail = () => { if (isOnboarding) { hideModal(); - NavigationService.navigate(ROUTES.ONBOARDING, { - screen: ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN - }); } else { hideModal(); NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { diff --git a/ts/sagas/profile.ts b/ts/sagas/profile.ts index 1546f517a38..e3b614cdaae 100644 --- a/ts/sagas/profile.ts +++ b/ts/sagas/profile.ts @@ -53,6 +53,7 @@ import { import { readablePrivacyReport } from "../utils/reporters"; import { withRefreshApiCall } from "../features/fastLogin/saga/utils"; import { ProfileError } from "../store/reducers/profileErrorType"; +import { UpdateProfile412ErrorTypesEnum } from "../../definitions/backend/UpdateProfile412ErrorTypes"; // A saga to load the Profile. export function* loadProfile( @@ -188,7 +189,13 @@ function* createOrUpdateProfileSaga( "PROFILE_EMAIL_VALIDATION_ERROR" ); } - if (response.right.status === 412) { + if ( + response.right.status === 412 && + response.right.value.type === + UpdateProfile412ErrorTypesEnum[ + "https://ioapp.it/problems/email-already-taken" + ] + ) { throw new ProfileError( response.right.value.title, "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR" diff --git a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx index fdeabf4cdba..3875a1feace 100644 --- a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx +++ b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx @@ -13,7 +13,8 @@ import React, { useMemo, useState, useEffect, - createRef + createRef, + useContext } from "react"; import { View, Keyboard, SafeAreaView, StyleSheet, Alert } from "react-native"; import validator from "validator"; @@ -36,6 +37,7 @@ import { profileUpsert } from "../../store/actions/profile"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { isProfileEmailAlreadyTakenSelector, + isProfileEmailValidatedSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; @@ -43,9 +45,11 @@ import { withKeyboard } from "../../utils/keyboard"; import { areStringsEqual } from "../../utils/options"; import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import { emailAcknowledged, emailInsert } from "../../store/actions/onboarding"; -import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; +import { emailInsert } from "../../store/actions/onboarding"; import { usePrevious } from "../../utils/hooks/usePrevious"; +import { LightModalContext } from "../../components/ui/LightModal"; +import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; +import { emailValidationSelector } from "../../store/reducers/emailValidation"; const styles = StyleSheet.create({ flex: { @@ -64,8 +68,8 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * A screen to allow user to insert an email address. */ const NewOnboardingEmailInsertScreen = () => { - useValidatedEmailModal(true); const dispatch = useIODispatch(); + const { showModal } = useContext(LightModalContext); const viewRef = createRef(); @@ -74,6 +78,13 @@ const NewOnboardingEmailInsertScreen = () => { const isProfileEmailAlreadyTaken = useIOSelector( isProfileEmailAlreadyTakenSelector ); + const isEmailValidatedSelector = useIOSelector( + isProfileEmailValidatedSelector + ); + const { acknowledgeOnEmailValidated } = useIOSelector( + emailValidationSelector + ); + const prevUserProfile = usePrevious(profile); const isLoading = useMemo( @@ -81,11 +92,6 @@ const NewOnboardingEmailInsertScreen = () => { [profile] ); - const acknowledgeEmail = useCallback( - () => dispatch(emailAcknowledged()), - [dispatch] - ); - const acknowledgeEmailInsert = useCallback( () => dispatch(emailInsert()), [dispatch] @@ -100,10 +106,22 @@ const NewOnboardingEmailInsertScreen = () => { ), [dispatch] ); + + const isEmailValidated = useMemo( + () => + isEmailValidatedSelector && + pipe( + acknowledgeOnEmailValidated, + O.getOrElse(() => true) + ), + [isEmailValidatedSelector, acknowledgeOnEmailValidated] + ); const [areSameEmails, setAreSameEmails] = useState(false); const [email, setEmail] = useState( isProfileEmailAlreadyTaken ? optionEmail : O.some(EMPTY_EMAIL) ); + + // this function return a boolean const isValidEmail = useCallback( () => pipe( @@ -124,6 +142,8 @@ const NewOnboardingEmailInsertScreen = () => { ); useEffect(() => { + // this control is true only if the user try to insert a email, + // only if the continueOnPress function should be call we exeute this code if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { // the user is trying to enter an email already in use @@ -140,13 +160,28 @@ const NewOnboardingEmailInsertScreen = () => { ); } } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { + // the email is correctly inserted acknowledgeEmailInsert(); - acknowledgeEmail(); return; } } - }, [acknowledgeEmailInsert, acknowledgeEmail, profile, prevUserProfile]); + }, [acknowledgeEmailInsert, profile, prevUserProfile]); + + useEffect(() => { + // if the email is correct, the user can validate it. + // in fact, if the email il correct the validation modal is open + if ( + prevUserProfile && + pot.isUpdating(prevUserProfile) && + pot.isSome(profile) && + !pot.isUpdating(profile) && + !isEmailValidated + ) { + showModal(); + } + }, [isEmailValidated, prevUserProfile, profile, showModal]); + // the user try to update the email const continueOnPress = () => { Keyboard.dismiss(); pipe( From 9285cbadcf987c886d6930627a0e69603466a265 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:56:42 +0100 Subject: [PATCH 05/35] fix optin screen accessibility label --- ts/screens/authentication/NewOptInScreen.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ts/screens/authentication/NewOptInScreen.tsx b/ts/screens/authentication/NewOptInScreen.tsx index 59041aa48ca..e7c61b2f1d1 100644 --- a/ts/screens/authentication/NewOptInScreen.tsx +++ b/ts/screens/authentication/NewOptInScreen.tsx @@ -117,7 +117,9 @@ const NewOptInScreen = (props: Props) => { navigateToIdpPage(true)} testID="accept-button-test" /> @@ -125,7 +127,9 @@ const NewOptInScreen = (props: Props) => { secondaryAction={ navigateToIdpPage(false)} testID="decline-button-test" /> From 85af66f19b7c3b557c8e10ef064c1b3c3e6e1363 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:47:16 +0100 Subject: [PATCH 06/35] fix tsc error --- ts/__mocks__/initializedProfile.ts | 1 + ts/components/messages/__tests__/PaymentButton.test.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/ts/__mocks__/initializedProfile.ts b/ts/__mocks__/initializedProfile.ts index 137d76d2e2c..9010c531720 100644 --- a/ts/__mocks__/initializedProfile.ts +++ b/ts/__mocks__/initializedProfile.ts @@ -15,6 +15,7 @@ const mockedProfile: InitializedProfile = { is_email_validated: true, is_inbox_enabled: true, is_webhook_enabled: true, + is_email_already_taken: false, name: "John", spid_email: "test@example.com" as EmailString, version: 1 as Version diff --git a/ts/components/messages/__tests__/PaymentButton.test.tsx b/ts/components/messages/__tests__/PaymentButton.test.tsx index 595ff93517a..09c4d367588 100644 --- a/ts/components/messages/__tests__/PaymentButton.test.tsx +++ b/ts/components/messages/__tests__/PaymentButton.test.tsx @@ -62,6 +62,7 @@ const testPaymentButton = ( is_email_enabled: true, is_webhook_enabled: true, is_email_validated: true, + is_email_already_taken: false, family_name: "Red", fiscal_code: "FiscalCode" as FiscalCode, has_profile: true, From 25bfcb3acb5a28717eab751660d3094e6bc54dde Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:26:22 +0100 Subject: [PATCH 07/35] Update locales/en/index.yml Co-authored-by: Alice Azzolini <93777761+AliceAzzolini@users.noreply.github.com> --- locales/en/index.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index e72ad1156c5..d0660047d6d 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -772,7 +772,7 @@ email: help: title: Email content: !include email_insert_help.md - alertTitle: Questa email è già in uso + alertTitle: This email address is already in use alertDescription: Può succedere se condividi lo stesso indirizzo con un familiare alertButton: Usa un’altra email newinsert: From 7a2b37f22f4d3151f329f0540c30ac855c3827ac Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:26:34 +0100 Subject: [PATCH 08/35] Update locales/en/index.yml Co-authored-by: Alice Azzolini <93777761+AliceAzzolini@users.noreply.github.com> --- locales/en/index.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index d0660047d6d..93b8069899c 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -773,7 +773,7 @@ email: title: Email content: !include email_insert_help.md alertTitle: This email address is already in use - alertDescription: Può succedere se condividi lo stesso indirizzo con un familiare + alertDescription: This may happen if you share the same email address with a family member alertButton: Usa un’altra email newinsert: header: "Configure IO" From d6a49746a077d31166130c8358b91069e357be78 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:26:42 +0100 Subject: [PATCH 09/35] Update locales/en/index.yml Co-authored-by: Alice Azzolini <93777761+AliceAzzolini@users.noreply.github.com> --- locales/en/index.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index 93b8069899c..9fe94d77b2a 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -774,7 +774,7 @@ email: content: !include email_insert_help.md alertTitle: This email address is already in use alertDescription: This may happen if you share the same email address with a family member - alertButton: Usa un’altra email + alertButton: Use another email address newinsert: header: "Configure IO" title: "What is your email address?" From 72a815681952e3f5819cb793b5bafc7a32b9a823 Mon Sep 17 00:00:00 2001 From: Sabino <114305019+sabontech@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:19:34 +0100 Subject: [PATCH 10/35] chore: [IOPID-1015] Biometric refinement (#5141) ## Short description This pr adds the logic that allows the app not to show the biometric activation request if it has already been activated in a previous login and adds, in the case of FACE_ID, the prompt requesting permissions upon activation. ## List of changes proposed in this pull request - ts/sagas/startup/onboarding/biometric/checkAcknowledgedFingerprintSaga.ts: added check to see if it had been activated in the last login. - [ts/screens/onboarding/biometric&securityChecks/FingerprintScreen.tsx , ts/screens/profile/SecurityScreen.tsx] : added permission request prompt in case of faceid. - ts/store/reducers/index.ts: biometric preference persisted to get it also after the end of a session. - ts/store/reducers/onboarding.ts: added reset of biometric control also after session expired. - ts/store/reducers/persistedPreferences.ts: added cross session control. - ts/utils/biometrics.ts: added function that handles the biometric activation. ## How to test With a new installation, try activating biometrics during onboarding: - If the biometric is not FaceID, it should activate (or not) the feature without prompting. - If the biometric is FaceID, when the active button is pressed, it should present the request for permissions: - If you give permissions and the authentication fails, it should not proceed. - If you give permissions and the authentication is successful, it should proceed and enable the feature. - If you don't give permissions, it should proceed and not activate the feature. After logout or session expired, if the biometric has been activated during last session, it should not ask you for activation again after login, unless this is done with a different account than the previous one. --------- Co-authored-by: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Co-authored-by: Cristiano Tofani --- .../checkAcknowledgedFingerprintSaga.ts | 11 +- .../FingerprintScreen.tsx | 24 ++- ts/screens/profile/SecurityScreen.tsx | 8 +- ts/store/reducers/index.ts | 7 +- ts/store/reducers/onboarding.ts | 3 +- ts/store/reducers/persistedPreferences.ts | 3 +- ts/utils/__tests__/biometrics.test.ts | 154 +++++++++++++++++- ts/utils/biometrics.ts | 51 +++++- 8 files changed, 247 insertions(+), 14 deletions(-) diff --git a/ts/sagas/startup/onboarding/biometric/checkAcknowledgedFingerprintSaga.ts b/ts/sagas/startup/onboarding/biometric/checkAcknowledgedFingerprintSaga.ts index 27fd642406b..af02676b419 100644 --- a/ts/sagas/startup/onboarding/biometric/checkAcknowledgedFingerprintSaga.ts +++ b/ts/sagas/startup/onboarding/biometric/checkAcknowledgedFingerprintSaga.ts @@ -3,6 +3,7 @@ import { fingerprintAcknowledged } from "../../../../store/actions/onboarding"; import { isFingerprintAcknowledgedSelector } from "../../../../store/reducers/onboarding"; import { ReduxSagaEffect } from "../../../../types/utils"; import { getBometricState, isDevicePinSet } from "../../../../utils/biometrics"; +import { isFingerprintEnabledSelector } from "../../../../store/reducers/persistedPreferences"; import { handleBiometricAvailable, hanldeMissingDevicePin, @@ -53,8 +54,14 @@ export function* checkAcknowledgedFingerprintSaga(): Generator< isFingerprintAcknowledgedSelector ); + const isFingerprintEnabled = yield* select(isFingerprintEnabledSelector); + if (!isFingerprintAcknowledged) { - // Navigate to the FingerprintScreen and wait for acknowledgment - yield* call(onboardFingerprintIfAvailableSaga); + if (isFingerprintEnabled) { + yield* put(fingerprintAcknowledged()); + } else { + // Navigate to the FingerprintScreen and wait for acknowledgment + yield* call(onboardFingerprintIfAvailableSaga); + } } } diff --git a/ts/screens/onboarding/biometric&securityChecks/FingerprintScreen.tsx b/ts/screens/onboarding/biometric&securityChecks/FingerprintScreen.tsx index 49532578839..d2d635cc8bc 100644 --- a/ts/screens/onboarding/biometric&securityChecks/FingerprintScreen.tsx +++ b/ts/screens/onboarding/biometric&securityChecks/FingerprintScreen.tsx @@ -13,6 +13,10 @@ import { abortOnboarding } from "../../../store/actions/onboarding"; import { InfoBox } from "../../../components/box/InfoBox"; import { H5 } from "../../../components/core/typography/H5"; import { preferenceFingerprintIsEnabledSaveSuccess } from "../../../store/actions/persistedPreferences"; +import { + BiometriActivationUserType, + mayUserActivateBiometric +} from "../../../utils/biometrics"; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "onboarding.contextualHelpTitle", @@ -105,11 +109,23 @@ const FingerprintScreen = () => { rightButton={{ title: I18n.t("global.buttons.activate2"), onPress: () => - dispatch( - preferenceFingerprintIsEnabledSaveSuccess({ - isFingerprintEnabled: true + mayUserActivateBiometric() + .then(_ => + dispatch( + preferenceFingerprintIsEnabledSaveSuccess({ + isFingerprintEnabled: true + }) + ) + ) + .catch((err: BiometriActivationUserType) => { + if (err === "PERMISSION_DENIED") { + dispatch( + preferenceFingerprintIsEnabledSaveSuccess({ + isFingerprintEnabled: false + }) + ); + } }) - ) }} /> diff --git a/ts/screens/profile/SecurityScreen.tsx b/ts/screens/profile/SecurityScreen.tsx index dcc5e74575c..be5722d0e41 100644 --- a/ts/screens/profile/SecurityScreen.tsx +++ b/ts/screens/profile/SecurityScreen.tsx @@ -21,7 +21,8 @@ import { useScreenReaderEnabled } from "../../utils/accessibility"; import { biometricAuthenticationRequest, getBiometricsType, - isBiometricsValidType + isBiometricsValidType, + mayUserActivateBiometric } from "../../utils/biometrics"; import { showToast } from "../../utils/showToast"; @@ -91,7 +92,10 @@ const SecurityScreen = (): React.ReactElement => { const setBiometricPreference = (biometricPreference: boolean): void => { if (biometricPreference) { // if user asks to enable biometric then call enable action directly - setFingerprintPreference(biometricPreference); + void mayUserActivateBiometric() + .then(_ => setFingerprintPreference(biometricPreference)) + .catch(_ => undefined); + return; } // if user asks to disable biometric recnognition is required to proceed diff --git a/ts/store/reducers/index.ts b/ts/store/reducers/index.ts index 9622027097d..b01bbee4b2d 100644 --- a/ts/store/reducers/index.ts +++ b/ts/store/reducers/index.ts @@ -222,9 +222,14 @@ export function createRootReducer( ...state.payments }, // isMixpanelEnabled must be kept + // isFingerprintEnabled must be kept only if true persistedPreferences: { ...initialPreferencesState, - isMixpanelEnabled: state.persistedPreferences.isMixpanelEnabled + isMixpanelEnabled: state.persistedPreferences.isMixpanelEnabled, + isFingerprintEnabled: state.persistedPreferences + .isFingerprintEnabled + ? true + : undefined }, wallet: { wallets: { diff --git a/ts/store/reducers/onboarding.ts b/ts/store/reducers/onboarding.ts index 014c3b12c43..c56f7fa68b8 100644 --- a/ts/store/reducers/onboarding.ts +++ b/ts/store/reducers/onboarding.ts @@ -8,6 +8,7 @@ import { fingerprintAcknowledged } from "../actions/onboarding"; import { Action } from "../actions/types"; +import { sessionExpired } from "../actions/authentication"; import { GlobalState } from "./types"; export type OnboardingState = Readonly<{ @@ -28,7 +29,7 @@ const reducer = ( ...state, isFingerprintAcknowledged: true }; - + case getType(sessionExpired): case getType(clearOnboarding): return INITIAL_STATE; diff --git a/ts/store/reducers/persistedPreferences.ts b/ts/store/reducers/persistedPreferences.ts index 2d0a0614615..106df12a97b 100644 --- a/ts/store/reducers/persistedPreferences.ts +++ b/ts/store/reducers/persistedPreferences.ts @@ -136,7 +136,8 @@ export default function preferencesReducer( if (isActionOf(differentProfileLoggedIn, action)) { return { ...state, - isMixpanelEnabled: null + isMixpanelEnabled: null, + isFingerprintEnabled: undefined }; } diff --git a/ts/utils/__tests__/biometrics.test.ts b/ts/utils/__tests__/biometrics.test.ts index 7776958335c..7169fc2528f 100644 --- a/ts/utils/__tests__/biometrics.test.ts +++ b/ts/utils/__tests__/biometrics.test.ts @@ -1,11 +1,13 @@ -import FingerprintScanner from "react-native-fingerprint-scanner"; +import FingerprintScanner, { Errors } from "react-native-fingerprint-scanner"; import * as mixpanel from "../../mixpanel"; import { biometricAuthenticationRequest, getBiometricsType, - isBiometricsValidType + isBiometricsValidType, + mayUserActivateBiometric } from "../biometrics"; +import * as Biometric from "../biometrics"; describe("getBiometricsType function", () => { it.each` @@ -86,3 +88,151 @@ describe("biometricAuthenticationRequest function", () => { }); }); }); + +describe("mayUserActivateBiometric function", () => { + it.each` + input | expected + ${"Touch ID"} | ${"ACTIVATED"} + ${"Biometrics"} | ${"ACTIVATED"} + ${"Unknown"} | ${"ACTIVATED"} + `("returns $expected when $input is given", async ({ input, expected }) => { + const spy = jest.spyOn(FingerprintScanner, "isSensorAvailable"); + spy.mockResolvedValue(input); + const result = await mayUserActivateBiometric(); + expect(result).toMatch(expected); + }); + + it("returns SENSOR_ERROR when getBiometricsType promise cannot be resolved ", async () => { + const getBiometricsTypeRejectedMock = Promise.reject(); + + try { + await Biometric.biometricFunctionForTests.mayUserActivateBiometricWithDependency( + getBiometricsTypeRejectedMock + ); + } catch (error) { + expect(error).toBe("SENSOR_ERROR"); + } + }); + + it("returns ACTIVATED when getBiometricsType promise resolves FACE_ID and the authentication is successful", async () => { + const getBiometricsTypeFaceIDMock = Promise.resolve( + "FACE_ID" as Biometric.BiometricsType + ); + const spy = jest.spyOn(FingerprintScanner, "authenticate"); + + spy.mockResolvedValue(Promise.resolve()); + const result = + await Biometric.biometricFunctionForTests.mayUserActivateBiometricWithDependency( + getBiometricsTypeFaceIDMock + ); + + expect(result).toMatch("ACTIVATED"); + }); + + it("returns PERMISSION_DENIED when getBiometricsType promise resolves FACE_ID and the user refuses permission to use the biometric", async () => { + const getBiometricsTypeFaceIDMock = Promise.resolve( + "FACE_ID" as Biometric.BiometricsType + ); + const spy = jest.spyOn(FingerprintScanner, "authenticate"); + + const error: Errors = { + name: "FingerprintScannerNotAvailable", + message: + "\tAuthentication could not start because Fingerprint Scanner is not available on the device" + }; + + spy.mockResolvedValue(Promise.reject(error)); + try { + await Biometric.biometricFunctionForTests.mayUserActivateBiometricWithDependency( + getBiometricsTypeFaceIDMock + ); + } catch (error) { + expect(error).toBe("PERMISSION_DENIED"); + } + }); + + it("returns AUTH_FAILED when getBiometricsType promise resolves FACE_ID and the authentication fails", async () => { + const getBiometricsTypeFaceIDMock = Promise.resolve( + "FACE_ID" as Biometric.BiometricsType + ); + const spy = jest.spyOn(FingerprintScanner, "authenticate"); + + const errorsArray: Array = [ + { + name: "AuthenticationFailed", + message: + "Authentication was not successful because the user failed to provide valid credentials" + }, + { name: "AuthenticationNotMatch", message: "No match" }, + { + name: "AuthenticationProcessFailed", + message: "Sensor was unable to process the image. Please try again." + }, + { + name: "AuthenticationTimeout", + message: + "Authentication was not successful because the operation timed out." + }, + { + name: "DeviceLocked", + message: + "Authentication was not successful, the device currently in a lockout of 30 seconds" + }, + { + name: "DeviceLockedPermanent", + message: + "Authentication was not successful, device must be unlocked via password." + }, + { + name: "DeviceOutOfMemory", + message: + "Authentication could not proceed because there is not enough free memory on the device." + }, + { + name: "FingerprintScannerNotEnrolled", + message: + "\tAuthentication could not start because Fingerprint Scanner has no enrolled fingers" + }, + { + name: "FingerprintScannerNotSupported", + message: "Device does not support Fingerprint Scanner" + }, + { + name: "FingerprintScannerUnknownError", + message: "Could not authenticate for an unknown reason" + }, + { name: "HardwareError", message: "A hardware error occurred." }, + { + name: "PasscodeNotSet", + message: + "Authentication could not start because the passcode is not set on the device" + }, + { + name: "SystemCancel", + message: + "Authentication was canceled by system - e.g. if another application came to foreground while the authentication dialog was up" + }, + { + name: "UserCancel", + message: + "Authentication was canceled by the user - e.g. the user tapped Cancel in the dialog" + }, + { + name: "UserFallback", + message: + "Authentication was canceled because the user tapped the fallback button (Enter Password)" + } + ]; + + for (const error of errorsArray) { + spy.mockResolvedValue(Promise.reject(error)); + try { + await Biometric.biometricFunctionForTests.mayUserActivateBiometricWithDependency( + getBiometricsTypeFaceIDMock + ); + } catch (error) { + expect(error).toBe("AUTH_FAILED"); + } + } + }); +}); diff --git a/ts/utils/biometrics.ts b/ts/utils/biometrics.ts index be52f4a9357..c82551056dc 100644 --- a/ts/utils/biometrics.ts +++ b/ts/utils/biometrics.ts @@ -3,7 +3,8 @@ import FingerprintScanner, { AuthenticateAndroid, AuthenticateIOS, Biometrics, - FingerprintScannerError + FingerprintScannerError, + Errors } from "react-native-fingerprint-scanner"; import { isPinOrFingerprintSet } from "react-native-device-info"; import { isDebugBiometricIdentificationEnabled } from "../config"; @@ -121,3 +122,51 @@ export const isDevicePinSet = (): Promise => }) .catch(_ => resolve(false)); }); + +export type BiometriActivationUserType = + | "ACTIVATED" + | "AUTH_FAILED" + | "PERMISSION_DENIED" + | "SENSOR_ERROR"; + +const mayUserActivateBiometricWithDependency = ( + getBiometricsType: Promise +): Promise => + new Promise((resolve, reject) => { + getBiometricsType + .then(value => { + if (value === "FACE_ID") { + FingerprintScanner.authenticate({ + description: I18n.t( + "identification.biometric.popup.sensorDescription" + ), + fallbackEnabled: false + } as AuthenticateIOS) + .then(_ => resolve("ACTIVATED")) + .catch((err: Errors) => { + reject(handleErrorDuringBiometricActivation(err)); + }); + } else { + resolve("ACTIVATED"); + } + }) + .catch(_ => { + reject("SENSOR_ERROR"); + }); + }); + +export const mayUserActivateBiometric = () => + mayUserActivateBiometricWithDependency(getBiometricsType()); + +export const biometricFunctionForTests = { + mayUserActivateBiometricWithDependency +}; + +function handleErrorDuringBiometricActivation( + err: Errors +): BiometriActivationUserType { + if (err.name === "FingerprintScannerNotAvailable") { + return "PERMISSION_DENIED"; + } + return "AUTH_FAILED"; +} From c096997f4fe1fab10214a6a7cdef85f54a653d5a Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Tue, 14 Nov 2023 17:03:20 +0100 Subject: [PATCH 11/35] chore(release): 2.46.0-rc.3 --- CHANGELOG.md | 12 ++++++++++++ android/app/build.gradle | 4 ++-- ios/ItaliaApp.xcodeproj/project.pbxproj | 4 ++-- ios/ItaliaApp/Info.plist | 2 +- ios/ItaliaAppTests/Info.plist | 2 +- package.json | 2 +- publiccode.yml | 4 ++-- 7 files changed, 21 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d086bb72e79..6a9ca86c653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.46.0-rc.3](https://github.com/pagopa/io-app/compare/2.46.0-rc.2...2.46.0-rc.3) (2023-11-14) + + +### Features + +* [[IOCOM-667](https://pagopa.atlassian.net/browse/IOCOM-667)] Failure logging for messages' sagas ([#5223](https://github.com/pagopa/io-app/issues/5223)) ([5b92210](https://github.com/pagopa/io-app/commit/5b9221013f6b63e998b6f20e752f2dbe07993055)) + + +### Chores + +* [[IOPID-1015](https://pagopa.atlassian.net/browse/IOPID-1015)] Biometric refinement ([#5141](https://github.com/pagopa/io-app/issues/5141)) ([af94c2a](https://github.com/pagopa/io-app/commit/af94c2a7b48601a13de59022325c8ede3e85cfe5)) + ## [2.46.0-rc.2](https://github.com/pagopa/io-app/compare/2.46.0-rc.0...2.46.0-rc.2) (2023-11-08) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8d5b8ee92cb..6a6e05ff659 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -144,8 +144,8 @@ android { applicationId "it.pagopa.io.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 100154767 - versionName "2.46.0.2" + versionCode 100154768 + versionName "2.46.0.3" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { // We configure the NDK build only if you decide to opt-in for the New Architecture. diff --git a/ios/ItaliaApp.xcodeproj/project.pbxproj b/ios/ItaliaApp.xcodeproj/project.pbxproj index 2813ec7ec16..97a93fd28e1 100644 --- a/ios/ItaliaApp.xcodeproj/project.pbxproj +++ b/ios/ItaliaApp.xcodeproj/project.pbxproj @@ -795,7 +795,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; @@ -836,7 +836,7 @@ CODE_SIGN_ENTITLEMENTS = ItaliaApp/ItaliaApp.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; diff --git a/ios/ItaliaApp/Info.plist b/ios/ItaliaApp/Info.plist index 1662afd677e..e3ce31131ea 100644 --- a/ios/ItaliaApp/Info.plist +++ b/ios/ItaliaApp/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 2 + 3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ItaliaAppTests/Info.plist b/ios/ItaliaAppTests/Info.plist index 174e0531b6c..d7c0a63af02 100644 --- a/ios/ItaliaAppTests/Info.plist +++ b/ios/ItaliaAppTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 2 + 3 \ No newline at end of file diff --git a/package.json b/package.json index 36aef50e82b..b9cf7057409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "italia-app", - "version": "2.46.0-rc.2", + "version": "2.46.0-rc.3", "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/IOPID-1045-1050/api_backend.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_public.yaml", "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", diff --git a/publiccode.yml b/publiccode.yml index 9b9262b28f6..e397a7612ed 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -5,11 +5,11 @@ publiccodeYmlVersion: '0.2' name: IO logo: "img/app-logo.svg" -releaseDate: '2023-11-08' +releaseDate: '2023-11-14' url: 'https://github.com/pagopa/io-app' applicationSuite: IO landingURL: 'https://io.italia.it/' -softwareVersion: 2.46.0-rc.2 +softwareVersion: 2.46.0-rc.3 developmentStatus: beta softwareType: standalone/mobile roadmap: 'https://io.italia.it/' From c7866d2d365ed41d56cdb99dbc8bc428c4c6ca5f Mon Sep 17 00:00:00 2001 From: Federico Mastrini Date: Tue, 14 Nov 2023 20:36:54 +0100 Subject: [PATCH 12/35] fix: [IOBP-361] Fix ID Pay rules info bottom sheet in initiative details screen (#5209) ## Short description This PR fixes the ID Pay initiative rules info bottom sheet height for shorter contents. https://github.com/pagopa/io-app/assets/6160324/7ffc63cf-82d8-4349-a55a-651157537887 ## List of changes proposed in this pull request - [ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx](https://github.com/pagopa/io-app/compare/IOBP-361-fix-idpay-rules-bottom-sheet?expand=1#diff-aec7df12aad70c4276336c3813019b4655fac37dc09eb456b93208f7c6b8b302): increased bottom padding to `170` for the rules bottom sheet - [ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx](https://github.com/pagopa/io-app/compare/IOBP-361-fix-idpay-rules-bottom-sheet?expand=1#diff-763f13f0b9f72e4763aa758664939937121a62cc637c86ff29bfe6f41ab7d56c): component props refactoring ## How to test Navigate to the ID Pay initiative details screen and check that the rules bottom sheet is correctly displayed. Co-authored-by: Alessandro Izzo <34343582+Hantex9@users.noreply.github.com> --- .../components/BeneficiaryDetailsContent.tsx | 28 ++++++++++++++----- .../components/InitiativeRulesInfoBox.tsx | 2 +- .../screens/BeneficiaryDetailsScreen.tsx | 9 ++---- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx index 0e73f784ff7..63a98432e36 100644 --- a/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx +++ b/ts/features/idpay/details/components/BeneficiaryDetailsContent.tsx @@ -34,17 +34,31 @@ import { InitiativeRulesInfoBoxSkeleton } from "./InitiativeRulesInfoBox"; -export type BeneficiaryDetailsProps = { - initiativeDetails: InitiativeDTO; - beneficiaryDetails: InitiativeDetailDTO; - onboardingStatus: OnboardingStatusDTO; -}; +export type BeneficiaryDetailsProps = + | { + isLoading?: false; + initiativeDetails: InitiativeDTO; + beneficiaryDetails: InitiativeDetailDTO; + onboardingStatus: OnboardingStatusDTO; + } + | { + isLoading: true; + initiativeDetails?: never; + beneficiaryDetails?: never; + onboardingStatus?: never; + }; const formatDate = (fmt: string) => (date: Date) => format(date, fmt); const BeneficiaryDetailsContent = (props: BeneficiaryDetailsProps) => { const navigation = useNavigation>(); - const { initiativeDetails, beneficiaryDetails, onboardingStatus } = props; + const { initiativeDetails, beneficiaryDetails, onboardingStatus, isLoading } = + props; + + if (isLoading) { + return ; + } + const { initiativeId, initiativeName, @@ -293,4 +307,4 @@ const styles = StyleSheet.create({ } }); -export { BeneficiaryDetailsContent, BeneficiaryDetailsContentSkeleton }; +export { BeneficiaryDetailsContent }; diff --git a/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx b/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx index f9d025f15c2..de39263b47f 100644 --- a/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx +++ b/ts/features/idpay/details/components/InitiativeRulesInfoBox.tsx @@ -45,7 +45,7 @@ const InitiativeRulesInfoBox = (props: Props) => { ) }, - 130 + 170 ); return ( diff --git a/ts/features/idpay/details/screens/BeneficiaryDetailsScreen.tsx b/ts/features/idpay/details/screens/BeneficiaryDetailsScreen.tsx index 1a481ef29d1..a627a52ab35 100644 --- a/ts/features/idpay/details/screens/BeneficiaryDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/BeneficiaryDetailsScreen.tsx @@ -1,3 +1,4 @@ +import { ContentWrapper } from "@pagopa/io-app-design-system"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { RouteProp, useRoute } from "@react-navigation/native"; import { sequenceS } from "fp-ts/lib/Apply"; @@ -5,14 +6,10 @@ import * as O from "fp-ts/lib/Option"; import { pipe } from "fp-ts/lib/function"; import React from "react"; import { ScrollView } from "react-native"; -import { ContentWrapper } from "@pagopa/io-app-design-system"; import BaseScreenComponent from "../../../../components/screens/BaseScreenComponent"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; -import { - BeneficiaryDetailsContent, - BeneficiaryDetailsContentSkeleton -} from "../components/BeneficiaryDetailsContent"; +import { BeneficiaryDetailsContent } from "../components/BeneficiaryDetailsContent"; import { IDPayDetailsParamsList } from "../navigation"; import { idPayBeneficiaryDetailsSelector, @@ -57,7 +54,7 @@ const BeneficiaryDetailsScreen = () => { onboardingStatus: pipe(idPayOnboardingStatusPot, pot.toOption) }), O.fold( - () => , + () => , props => ) ); From 42b2ffb49ace11d1f7b5cdeb5de185d5637264c8 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:49:03 +0100 Subject: [PATCH 13/35] finish to implement the logic --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9cf7057409..c751094203b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.46.0-rc.3", "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/IOPID-1045-1050/api_backend.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_public.yaml", - "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", + "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", "io_bonus_vacanze_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_bonus.yaml", "io_cgn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_cgn.yaml", "io_cgn_merchants_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_cgn_operator_search.yaml", From 17ce950e413e92527cb68fa724929ebdf0757d0d Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Fri, 17 Nov 2023 18:32:26 +0100 Subject: [PATCH 14/35] fix bugs on navigation. Need to add comments and make some tests before mark the PR as ready for review --- .../NewRemindEmailValidationOverlay.tsx | 19 +- ts/hooks/useValidateEmailModal.tsx | 11 +- ts/navigation/OnboardingNavigator.tsx | 9 +- ts/navigation/ProfileNavigator.tsx | 6 +- ts/sagas/profile.ts | 4 +- .../NewOnboardingEmailInsertScreen.tsx | 28 +- ts/screens/profile/EmailInsertScreen.tsx | 18 +- ts/screens/profile/NewEmailInsertScreen.tsx | 255 ++++++++++++++++++ ts/screens/profile/ProfileDataScreen.tsx | 17 +- 9 files changed, 302 insertions(+), 65 deletions(-) create mode 100644 ts/screens/profile/NewEmailInsertScreen.tsx diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 7eda48ee660..72321ad074f 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -19,6 +19,7 @@ import { import I18n from "../i18n"; import { + acknowledgeOnEmailValidation, profileLoadRequest, startEmailValidation } from "../store/actions/profile"; @@ -28,8 +29,6 @@ import { } from "../store/reducers/profile"; import { useIODispatch, useIOSelector } from "../store/hooks"; import { emailValidationSelector } from "../store/reducers/emailValidation"; -import NavigationService from "../navigation/NavigationService"; -import ROUTES from "../navigation/routes"; import { emailAcknowledged } from "../store/actions/onboarding"; import { IOStyles } from "./core/variables/IOStyles"; import FooterWithButtons from "./ui/FooterWithButtons"; @@ -79,6 +78,11 @@ const NewRemindEmailValidationOverlay = (props: Props) => { () => dispatch(profileLoadRequest()), [dispatch] ); + const dispatchAcknowledgeOnEmailValidation = useCallback( + (maybeAcknowledged: O.Option) => + dispatch(acknowledgeOnEmailValidation(maybeAcknowledged)), + [dispatch] + ); // function to localize the title of the button. If the email is validated and if it is not, whether the confirmation email was sent or not const buttonTitle = () => { @@ -122,14 +126,9 @@ const NewRemindEmailValidationOverlay = (props: Props) => { }; const navigateToInsertEmail = () => { - if (isOnboarding) { - hideModal(); - } else { - hideModal(); - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.INSERT_EMAIL_SCREEN - }); - } + // FIXME -> understand if this function is needed + dispatchAcknowledgeOnEmailValidation(O.none); + hideModal(); }; const renderFooter = () => ( diff --git a/ts/hooks/useValidateEmailModal.tsx b/ts/hooks/useValidateEmailModal.tsx index 49aa667e48c..5cb94f52463 100644 --- a/ts/hooks/useValidateEmailModal.tsx +++ b/ts/hooks/useValidateEmailModal.tsx @@ -32,17 +32,10 @@ export const useValidatedEmailModal = (isOnboarding?: boolean) => { useFocusEffect( React.useCallback(() => { - // AS-IS FLOW - if (!isNewCduFlow && !isEmailValidated) { + if (!isEmailValidated) { showModal(); - return () => hideModal(); - // CDU FLOW - } else if (isNewCduFlow && !isEmailValidated) { - showModal( - - ); } - return () => void 0; + return () => hideModal(); }, [hideModal, isEmailValidated, isOnboarding, showModal]) ); }; diff --git a/ts/navigation/OnboardingNavigator.tsx b/ts/navigation/OnboardingNavigator.tsx index db236947d91..026e63fb4c0 100644 --- a/ts/navigation/OnboardingNavigator.tsx +++ b/ts/navigation/OnboardingNavigator.tsx @@ -14,6 +14,8 @@ import ServicePreferenceCompleteScreen from "../screens/onboarding/ServicePrefer import { isGestureEnabled } from "../utils/navigation"; import MissingDevicePinScreen from "../screens/onboarding/biometric&securityChecks/MissingDevicePinScreen"; import MissingDeviceBiometricScreen from "../screens/onboarding/biometric&securityChecks/MissingDeviceBiometricScreen"; +import NewOnboardingEmailInsertScreen from "../screens/onboarding/NewOnboardingEmailInsertScreen"; +import { isNewCduFlow } from "../config"; import { OnboardingParamsList } from "./params/OnboardingParamsList"; import ROUTES from "./routes"; @@ -63,9 +65,14 @@ const navigator = () => ( name={ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN} component={OnboardingEmailInsertScreen} /> + {/* FIXME -> understand if this control is necessary */} { headerShown: false }} name={ROUTES.INSERT_EMAIL_SCREEN} - component={EmailInsertScreen} + // FIXME -> understand if this control is necessary + component={isNewCduFlow ? NewEmailInsertScreen : EmailInsertScreen} /> { [profile] ); - const acknowledgeEmailInsert = useCallback( - () => dispatch(emailInsert()), + const acknowledgeEmail = useCallback( + () => dispatch(emailAcknowledged()), [dispatch] ); @@ -161,25 +161,15 @@ const NewOnboardingEmailInsertScreen = () => { } } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { // the email is correctly inserted - acknowledgeEmailInsert(); + if (isEmailValidated) { + acknowledgeEmail(); + } else { + showModal(); + } return; } } - }, [acknowledgeEmailInsert, profile, prevUserProfile]); - - useEffect(() => { - // if the email is correct, the user can validate it. - // in fact, if the email il correct the validation modal is open - if ( - prevUserProfile && - pot.isUpdating(prevUserProfile) && - pot.isSome(profile) && - !pot.isUpdating(profile) && - !isEmailValidated - ) { - showModal(); - } - }, [isEmailValidated, prevUserProfile, profile, showModal]); + }, [acknowledgeEmail, profile, prevUserProfile, isEmailValidated, showModal]); // the user try to update the email const continueOnPress = () => { diff --git a/ts/screens/profile/EmailInsertScreen.tsx b/ts/screens/profile/EmailInsertScreen.tsx index f39bb541b57..6771d7c821a 100644 --- a/ts/screens/profile/EmailInsertScreen.tsx +++ b/ts/screens/profile/EmailInsertScreen.tsx @@ -36,7 +36,6 @@ import { areStringsEqual } from "../../utils/options"; import { showToast } from "../../utils/showToast"; import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; -import { useValidatedEmailModal } from "../../hooks/useValidateEmailModal"; type Props = IOStackNavigationRouteProps< ProfileParamsList, @@ -61,7 +60,6 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { * A screen to allow user to insert an email address. */ const EmailInsertScreen = (props: Props) => { - useValidatedEmailModal(); const dispatch = useIODispatch(); const profile = useIOSelector(profileSelector); @@ -167,22 +165,8 @@ const EmailInsertScreen = (props: Props) => { useEffect(() => { if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { - // the user is trying to enter an email already in use - if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { - Alert.alert( - I18n.t("email.insert.alertTitle"), - I18n.t("email.insert.alertDescription"), - [ - { - text: I18n.t("email.insert.alertButton"), - style: "cancel" - } - ] - ); - } else { - showToast(I18n.t("email.edit.upsert_ko"), "danger"); - } // display a toast with error + showToast(I18n.t("email.edit.upsert_ko"), "danger"); } else if (pot.isSome(profile)) { // user is inserting his email from onboarding phase // he comes from checkAcknowledgedEmailSaga if onboarding is not finished yet diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/NewEmailInsertScreen.tsx new file mode 100644 index 00000000000..445c57698fc --- /dev/null +++ b/ts/screens/profile/NewEmailInsertScreen.tsx @@ -0,0 +1,255 @@ +/** + * A screen where user after login (with CIE) can set email address if it is + * not present in the profile. + */ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { EmailString } from "@pagopa/ts-commons/lib/strings"; +import * as E from "fp-ts/lib/Either"; +import { pipe } from "fp-ts/lib/function"; +import * as O from "fp-ts/lib/Option"; +import { Content, Form } from "native-base"; +import React, { + useCallback, + useEffect, + useMemo, + useState, + useContext +} from "react"; +import { Alert, Keyboard, SafeAreaView, StyleSheet, View } from "react-native"; +import { VSpacer } from "@pagopa/io-app-design-system"; +import { H1 } from "../../components/core/typography/H1"; +import { LabelledItem } from "../../components/LabelledItem"; +import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; +import BaseScreenComponent, { + ContextualHelpPropsMarkdown +} from "../../components/screens/BaseScreenComponent"; +import FooterWithButtons from "../../components/ui/FooterWithButtons"; +import I18n from "../../i18n"; +import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; +import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; +import { profileUpsert } from "../../store/actions/profile"; +import { useIODispatch, useIOSelector } from "../../store/hooks"; +import { + isProfileEmailValidatedSelector, + profileEmailSelector, + profileSelector +} from "../../store/reducers/profile"; +import { usePrevious } from "../../utils/hooks/usePrevious"; +import { withKeyboard } from "../../utils/keyboard"; +import { areStringsEqual } from "../../utils/options"; +import { showToast } from "../../utils/showToast"; +import { Body } from "../../components/core/typography/Body"; +import { IOStyles } from "../../components/core/variables/IOStyles"; +import { LightModalContext } from "../../components/ui/LightModal"; +import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; + +type Props = IOStackNavigationRouteProps< + ProfileParamsList, + "INSERT_EMAIL_SCREEN" +>; + +const styles = StyleSheet.create({ + flex: { + flex: 1 + } +}); + +const EMPTY_EMAIL = ""; + +// TODO: update content (https://www.pivotaltracker.com/n/projects/2048617/stories/169392558) +const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { + title: "email.insert.help.title", + body: "email.insert.help.content" +}; + +/** + * A screen to allow user to insert an email address. + */ +const NewEmailInsertScreen = (props: Props) => { + const { showModal } = useContext(LightModalContext); + + const dispatch = useIODispatch(); + + const profile = useIOSelector(profileSelector); + const optionEmail = useIOSelector(profileEmailSelector); + const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); + const isLoading = useMemo( + () => pot.isUpdating(profile) || pot.isLoading(profile), + [profile] + ); + + const updateEmail = useCallback( + (email: EmailString) => + dispatch( + profileUpsert.request({ + email + }) + ), + [dispatch] + ); + + const [email, setEmail] = useState(optionEmail); + + /** validate email returning three possible values: + * - _true_, if email is valid. + * - _false_, if email has been already changed from the user and it is not + * valid. + * - _undefined_, if email field is empty. This state is consumed by + * LabelledItem Component and it used for style pourposes ONLY. + */ + const isValidEmail = () => + pipe( + email, + O.map(value => { + if (EMPTY_EMAIL === value) { + return undefined; + } + return E.isRight(EmailString.decode(value)); + }), + O.toUndefined + ); + + const continueOnPress = () => { + Keyboard.dismiss(); + const isTheSameEmail = areStringsEqual(optionEmail, email, true); + if (!isTheSameEmail) { + pipe( + email, + O.map(e => { + updateEmail(e as EmailString); + }) + ); + } else { + Alert.alert(I18n.t("email.insert.alert")); + } + }; + + const renderFooterButtons = () => { + const continueButtonProps = { + disabled: isValidEmail() !== true && !isLoading, + onPress: continueOnPress, + title: I18n.t("global.buttons.continue"), + block: true, + primary: isValidEmail() + }; + + return ( + + ); + }; + + const handleOnChangeEmailText = (value: string) => { + setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); + }; + + const handleGoBack = useCallback(() => { + // goback if the onboarding is completed + props.navigation.goBack(); + }, [props.navigation]); + + useEffect(() => { + setEmail(O.some(EMPTY_EMAIL)); + }, []); + + const prevUserProfile = usePrevious(profile); + + useEffect(() => { + if (prevUserProfile) { + const isPrevCurrentSameState = prevUserProfile.kind === profile.kind; + // do nothing if prev profile is in the same state of the current + if (isPrevCurrentSameState) { + return; + } + } + }, [prevUserProfile, profile]); + + useEffect(() => { + if (prevUserProfile && pot.isUpdating(prevUserProfile)) { + if (pot.isError(profile)) { + // the user is trying to enter an email already in use + if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { + Alert.alert( + I18n.t("email.insert.alertTitle"), + I18n.t("email.insert.alertDescription"), + [ + { + text: I18n.t("email.insert.alertButton"), + style: "cancel" + } + ] + ); + } else { + showToast(I18n.t("email.edit.upsert_ko"), "danger"); + } + + // display a toast with error + } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { + // the email is correctly inserted + if (isEmailValidated) { + handleGoBack(); + } else { + showModal(); + } + return; + } + } + }, [handleGoBack, isEmailValidated, prevUserProfile, profile, showModal]); + + return ( + + + + + +

+ {I18n.t("email.edit.title")} +

+ + + + {isEmailValidated + ? I18n.t("email.edit.validated") + : I18n.t("email.edit.subtitle")} + + {` ${pipe( + optionEmail, + O.getOrElse(() => "") + )}`} + + + +
+ EMPTY_EMAIL) + ), + onChangeText: handleOnChangeEmailText + }} + /> + +
+
+ {withKeyboard(renderFooterButtons())} +
+
+
+ ); +}; + +export default NewEmailInsertScreen; diff --git a/ts/screens/profile/ProfileDataScreen.tsx b/ts/screens/profile/ProfileDataScreen.tsx index af6cc51cd66..8f9128dea2d 100644 --- a/ts/screens/profile/ProfileDataScreen.tsx +++ b/ts/screens/profile/ProfileDataScreen.tsx @@ -20,6 +20,7 @@ import { profileNameSurnameSelector } from "../../store/reducers/profile"; import { GlobalState } from "../../store/reducers/types"; +import { isNewCduFlow } from "../../config"; const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { title: "profile.preferences.contextualHelpTitle", @@ -37,11 +38,17 @@ const ProfileDataScreen: React.FC = ({ hasProfileEmail, nameSurname }): React.ReactElement => { - const onPressEmail = () => - hasProfileEmail - ? navigateToEmailReadScreen() - : navigateToEmailInsertScreen(); - + const onPressEmail = () => { + if (hasProfileEmail) { + if (isNewCduFlow) { + navigateToEmailInsertScreen(); + } else { + navigateToEmailReadScreen(); + } + } else { + navigateToEmailInsertScreen(); + } + }; return ( Date: Tue, 21 Nov 2023 09:47:32 +0100 Subject: [PATCH 15/35] change version of io-backend --- package.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index c751094203b..e6253d5fb6f 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { "name": "italia-app", "version": "2.46.0-rc.3", - "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/IOPID-1045-1050/api_backend.yaml", - "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_public.yaml", + "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_backend.yaml", + "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_public.yaml", "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", - "io_bonus_vacanze_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_bonus.yaml", - "io_cgn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_cgn.yaml", - "io_cgn_merchants_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_cgn_operator_search.yaml", + "io_bonus_vacanze_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_bonus.yaml", + "io_cgn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_cgn.yaml", + "io_cgn_merchants_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_cgn_operator_search.yaml", "io_bpd_citizen": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/citizen.json", "io_bpd_citizen_v2": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/citizen_v2.json", "io_bpd_payment": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/payment.json", "io_bpd_award_periods": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/award.json", "io_bpd_winning_transactions": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/winning_transactions.json", "io_bpd_winning_transactions_v2": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/bonus/specs/bpd/winning_transactions_v2.json", - "api_fci": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_io_sign.yaml", - "io_eu_covid_cert": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_eucovidcert.yaml", - "io_sicilia_vola_token": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_mit_voucher.yaml", - "io_pn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_pn.yaml", - "io_consumed_pn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/openapi/consumed/api-piattaforma-notifiche.yaml", + "api_fci": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_io_sign.yaml", + "io_eu_covid_cert": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_eucovidcert.yaml", + "io_sicilia_vola_token": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_mit_voucher.yaml", + "io_pn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_pn.yaml", + "io_consumed_pn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/openapi/consumed/api-piattaforma-notifiche.yaml", "api_sicilia_vola": "assets/SiciliaVola.yml", "api_cdc": "assets/CdcSwagger.yml", "pagopa_api": "assets/paymentManager/spec.json", @@ -25,8 +25,8 @@ "pagopa_cobadge_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/pagopa/cobadge/abi_definitions.yml", "pagopa_privative_configuration": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/pagopa/privative/definitions.yml", "idpay_api": "https://raw.githubusercontent.com/pagopa/cstar-infrastructure/v5.8.0/src/domains/idpay-app/api/idpay_appio_full/openapi.appio.full.yml", - "lollipop_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/api_lollipop_first_consumer.yaml", - "fast_login_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.20.0-RELEASE/openapi/generated/api_fast_login.yaml", + "lollipop_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_lollipop_first_consumer.yaml", + "fast_login_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/openapi/generated/api_fast_login.yaml", "pagopa_api_walletv3": "https://raw.githubusercontent.com/pagopa/pagopa-infra/7907dba7e1e6a9de8ab746319bcd92a92326a66f/src/domains/wallet-app/api/payment-wallet/v1/_openapi.json.tpl", "private": true, "scripts": { From 978311c671dd6b86cd131d64df30bfe4db0801eb Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:24:18 +0100 Subject: [PATCH 16/35] change error from alert to red text --- ts/screens/profile/NewEmailInsertScreen.tsx | 122 +++++++++++++------- 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/NewEmailInsertScreen.tsx index 445c57698fc..6bc7b77b5e0 100644 --- a/ts/screens/profile/NewEmailInsertScreen.tsx +++ b/ts/screens/profile/NewEmailInsertScreen.tsx @@ -15,8 +15,14 @@ import React, { useState, useContext } from "react"; +import validator from "validator"; import { Alert, Keyboard, SafeAreaView, StyleSheet, View } from "react-native"; -import { VSpacer } from "@pagopa/io-app-design-system"; +import { + IOColors, + Icon, + LabelSmall, + VSpacer +} from "@pagopa/io-app-design-system"; import { H1 } from "../../components/core/typography/H1"; import { LabelledItem } from "../../components/LabelledItem"; import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; @@ -88,8 +94,8 @@ const NewEmailInsertScreen = (props: Props) => { [dispatch] ); - const [email, setEmail] = useState(optionEmail); - + const [areSameEmails, setAreSameEmails] = useState(false); + const [email, setEmail] = useState(optionEmail ?? O.some(EMPTY_EMAIL)); /** validate email returning three possible values: * - _true_, if email is valid. * - _false_, if email has been already changed from the user and it is not @@ -97,36 +103,40 @@ const NewEmailInsertScreen = (props: Props) => { * - _undefined_, if email field is empty. This state is consumed by * LabelledItem Component and it used for style pourposes ONLY. */ - const isValidEmail = () => - pipe( - email, - O.map(value => { - if (EMPTY_EMAIL === value) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ); + // this function return a boolean + const isValidEmail = useCallback( + () => + pipe( + email, + O.map(value => { + if ( + EMPTY_EMAIL === value || + !validator.isEmail(value) || + areSameEmails + ) { + return undefined; + } + return E.isRight(EmailString.decode(value)); + }), + O.toUndefined + ), + [areSameEmails, email] + ); const continueOnPress = () => { Keyboard.dismiss(); - const isTheSameEmail = areStringsEqual(optionEmail, email, true); - if (!isTheSameEmail) { - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - } else { - Alert.alert(I18n.t("email.insert.alert")); - } + + pipe( + email, + O.map(e => { + updateEmail(e as EmailString); + }) + ); }; const renderFooterButtons = () => { const continueButtonProps = { - disabled: isValidEmail() !== true && !isLoading, + disabled: !isValidEmail() && !isLoading, onPress: continueOnPress, title: I18n.t("global.buttons.continue"), block: true, @@ -142,6 +152,7 @@ const NewEmailInsertScreen = (props: Props) => { }; const handleOnChangeEmailText = (value: string) => { + setAreSameEmails(areStringsEqual(O.some(value), optionEmail, true)); setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); }; @@ -226,22 +237,49 @@ const NewEmailInsertScreen = (props: Props) => {
- EMPTY_EMAIL) - ), - onChangeText: handleOnChangeEmailText - }} - /> + + EMPTY_EMAIL) + ), + onChangeText: handleOnChangeEmailText + }} + /> + {areSameEmails && ( + + + + + + {I18n.t("email.newinsert.alert.description")} + + + )} +
From 9ee1333cf1c87a4eb30f76b7025c05bb4651df0c Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Wed, 22 Nov 2023 12:02:35 +0100 Subject: [PATCH 17/35] fix routing and logics --- ts/components/NewRemindEmailValidationOverlay.tsx | 5 +++++ ts/screens/profile/NewEmailInsertScreen.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 72321ad074f..9c4240a68e5 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -30,6 +30,8 @@ import { import { useIODispatch, useIOSelector } from "../store/hooks"; import { emailValidationSelector } from "../store/reducers/emailValidation"; import { emailAcknowledged } from "../store/actions/onboarding"; +import NavigationService from "../navigation/NavigationService"; +import ROUTES from "../navigation/routes"; import { IOStyles } from "./core/variables/IOStyles"; import FooterWithButtons from "./ui/FooterWithButtons"; import { IOToast } from "./Toast"; @@ -114,6 +116,9 @@ const NewRemindEmailValidationOverlay = (props: Props) => { acknowledgeEmail(); } hideModal(); + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.PROFILE_DATA + }); } else { // send email validation only if it exists pipe( diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/NewEmailInsertScreen.tsx index 6bc7b77b5e0..dba0cb2f9bb 100644 --- a/ts/screens/profile/NewEmailInsertScreen.tsx +++ b/ts/screens/profile/NewEmailInsertScreen.tsx @@ -96,6 +96,7 @@ const NewEmailInsertScreen = (props: Props) => { const [areSameEmails, setAreSameEmails] = useState(false); const [email, setEmail] = useState(optionEmail ?? O.some(EMPTY_EMAIL)); + /** validate email returning three possible values: * - _true_, if email is valid. * - _false_, if email has been already changed from the user and it is not @@ -103,7 +104,6 @@ const NewEmailInsertScreen = (props: Props) => { * - _undefined_, if email field is empty. This state is consumed by * LabelledItem Component and it used for style pourposes ONLY. */ - // this function return a boolean const isValidEmail = useCallback( () => pipe( @@ -162,6 +162,7 @@ const NewEmailInsertScreen = (props: Props) => { }, [props.navigation]); useEffect(() => { + setAreSameEmails(false); setEmail(O.some(EMPTY_EMAIL)); }, []); From aeb4b449b6e4953e0732145062d7a4154dddb0b3 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:41:01 +0100 Subject: [PATCH 18/35] delete unsed import --- ts/hooks/useValidateEmailModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ts/hooks/useValidateEmailModal.tsx b/ts/hooks/useValidateEmailModal.tsx index 5cb94f52463..b40deebdf37 100644 --- a/ts/hooks/useValidateEmailModal.tsx +++ b/ts/hooks/useValidateEmailModal.tsx @@ -8,8 +8,6 @@ import { LightModalContext } from "../components/ui/LightModal"; import { useIOSelector } from "../store/hooks"; import { emailValidationSelector } from "../store/reducers/emailValidation"; import { isProfileEmailValidatedSelector } from "../store/reducers/profile"; -import NewRemindEmailValidationOverlay from "../components/NewRemindEmailValidationOverlay"; -import { isNewCduFlow } from "../config"; export const useValidatedEmailModal = (isOnboarding?: boolean) => { const { showModal, hideModal } = useContext(LightModalContext); From 8bedbac956ab320d7380dfd85042881848b766a2 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:30:55 +0100 Subject: [PATCH 19/35] fix navigation on onboarding flow --- ts/components/NewRemindEmailValidationOverlay.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 9c4240a68e5..5aadf161146 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -114,11 +114,13 @@ const NewRemindEmailValidationOverlay = (props: Props) => { // if the user is in the onboarding flow and the email il correctly validated, // the email validation flow is finished acknowledgeEmail(); + hideModal(); + } else { + hideModal(); + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.PROFILE_DATA + }); } - hideModal(); - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.PROFILE_DATA - }); } else { // send email validation only if it exists pipe( From 2d44f85aaedc99a9b9516d5b3f16f391420011c3 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:01:26 +0100 Subject: [PATCH 20/35] Start adding check email startup navigation --- ts/navigation/AuthenticatedStackNavigator.tsx | 3 + ts/navigation/CheckEmailNavigator.tsx | 29 ++++ ts/navigation/params/AppParamsList.ts | 2 + ts/navigation/params/CheckEmailParamsList.ts | 7 + ts/navigation/routes.ts | 5 + ts/sagas/startup.ts | 3 + ts/sagas/startup/checkEmailSaga.ts | 27 ++++ .../modal/mailCheck/EmailAlreadyUsedModal.tsx | 62 --------- .../modal/mailCheck/ValidateEmailModal.tsx | 71 ---------- .../mailCheck/EmailAlreadyTakenScreen.tsx | 125 ++++++++++++++++++ .../profile/mailCheck/ValidateEmailScreen.tsx | 116 ++++++++++++++++ ts/store/reducers/profile.ts | 5 + 12 files changed, 322 insertions(+), 133 deletions(-) create mode 100644 ts/navigation/CheckEmailNavigator.tsx create mode 100644 ts/navigation/params/CheckEmailParamsList.ts create mode 100644 ts/sagas/startup/checkEmailSaga.ts delete mode 100644 ts/screens/modal/mailCheck/EmailAlreadyUsedModal.tsx delete mode 100644 ts/screens/modal/mailCheck/ValidateEmailModal.tsx create mode 100644 ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx create mode 100644 ts/screens/profile/mailCheck/ValidateEmailScreen.tsx diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index 3c729de76fc..4d77434900e 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -70,6 +70,7 @@ import ROUTES from "./routes"; import ServicesNavigator from "./ServicesNavigator"; import { MainTabNavigator } from "./TabNavigator"; import WalletNavigator from "./WalletNavigator"; +import CheckEmailNavigator from "./CheckEmailNavigator"; const Stack = createStackNavigator(); @@ -90,6 +91,8 @@ const AuthenticatedStackNavigator = () => { + + ( + + + + +); + +export default navigator; diff --git a/ts/navigation/params/AppParamsList.ts b/ts/navigation/params/AppParamsList.ts index 1382b728b0d..35e751c1f4a 100644 --- a/ts/navigation/params/AppParamsList.ts +++ b/ts/navigation/params/AppParamsList.ts @@ -59,12 +59,14 @@ import { OnboardingParamsList } from "./OnboardingParamsList"; import { ProfileParamsList } from "./ProfileParamsList"; import { ServicesParamsList } from "./ServicesParamsList"; import { WalletParamsList } from "./WalletParamsList"; +import { CheckEmailParamsList } from "./CheckEmailParamsList"; export type AppParamsList = { [ROUTES.INGRESS]: undefined; [ROUTES.UNSUPPORTED_DEVICE]: undefined; [ROUTES.BACKGROUND]: undefined; [ROUTES.AUTHENTICATION]: NavigatorScreenParams; + [ROUTES.CHECK_EMAIL]: NavigatorScreenParams; [ROUTES.ONBOARDING]: NavigatorScreenParams; [ROUTES.MAIN]: NavigatorScreenParams; diff --git a/ts/navigation/params/CheckEmailParamsList.ts b/ts/navigation/params/CheckEmailParamsList.ts new file mode 100644 index 00000000000..6024e956123 --- /dev/null +++ b/ts/navigation/params/CheckEmailParamsList.ts @@ -0,0 +1,7 @@ +import { EmailAlreadyUsedScreenParamList } from "../../screens/profile/mailCheck/EmailAlreadyTakenScreen"; +import ROUTES from "../routes"; + +export type CheckEmailParamsList = { + [ROUTES.CHECK_EMAIL_ALREADY_TAKEN]: EmailAlreadyUsedScreenParamList; + [ROUTES.CHECK_EMAIL_NOT_VERIFIED]: undefined; +}; diff --git a/ts/navigation/routes.ts b/ts/navigation/routes.ts index 9473c018ff0..865615f7ff0 100644 --- a/ts/navigation/routes.ts +++ b/ts/navigation/routes.ts @@ -26,6 +26,11 @@ const ROUTES = { CIE_WRONG_PIN_SCREEN: "CIE_WRONG_PIN_SCREEN", CIE_PIN_TEMP_LOCKED_SCREEN: "CIE_PIN_TEMP_LOCKED_SCREEN", + // Mail verification + CHECK_EMAIL: "CHECK_EMAIL", + CHECK_EMAIL_NOT_VERIFIED: "CHECK_EMAIL_NOT_VERIFIED", + CHECK_EMAIL_ALREADY_TAKEN: "CHECK_EMAIL_ALREADY_TAKEN", + // Onboarding ONBOARDING: "ONBOARDING", ONBOARDING_TOS: "ONBOARDING_TOS", diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 39041174474..24a2f77a4e8 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -166,6 +166,7 @@ import { } from "./../store/storages/keychain"; import { watchMessagePrecondition } from "./messages/watchMessagePrecondition"; import { setLanguageFromProfileIfExists } from "./preferences"; +import { checkEmailSaga } from "./startup/checkEmailSaga"; const WAIT_INITIALIZE_SAGA = 5000 as Millisecond; const navigatorPollingTime = 125 as Millisecond; @@ -511,6 +512,8 @@ export function* initializeApplicationSaga( yield* call(checkAcknowledgedEmailSaga, userProfile); } + yield* call(checkEmailSaga, userProfile); + // check if the user must set preferences for push notifications (e.g. reminders) yield* call(checkNotificationsPreferencesSaga, userProfile); diff --git a/ts/sagas/startup/checkEmailSaga.ts b/ts/sagas/startup/checkEmailSaga.ts new file mode 100644 index 00000000000..c6e6f55e489 --- /dev/null +++ b/ts/sagas/startup/checkEmailSaga.ts @@ -0,0 +1,27 @@ +import { call } from "typed-redux-saga/macro"; +import { InitializedProfile } from "../../../definitions/backend/InitializedProfile"; +import NavigationService from "../../navigation/NavigationService"; +import ROUTES from "../../navigation/routes"; +import { + isProfileEmailValidated, + isProfileEmailAlreadyTaken +} from "../../store/reducers/profile"; +import { ReduxSagaEffect } from "../../types/utils"; +import { isNewCduFlow } from "../../config"; + +export function* checkEmailSaga( + userProfile: InitializedProfile +): IterableIterator { + if (isNewCduFlow && !isProfileEmailValidated(userProfile)) { + if (isProfileEmailAlreadyTaken(userProfile)) { + yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { + screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, + params: { email: userProfile.email } + }); + } else { + yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { + screen: ROUTES.CHECK_EMAIL_NOT_VERIFIED + }); + } + } +} diff --git a/ts/screens/modal/mailCheck/EmailAlreadyUsedModal.tsx b/ts/screens/modal/mailCheck/EmailAlreadyUsedModal.tsx deleted file mode 100644 index e0be28b3118..00000000000 --- a/ts/screens/modal/mailCheck/EmailAlreadyUsedModal.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react"; -import { View, SafeAreaView, StyleSheet, Modal } from "react-native"; -import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; -import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { H3 } from "../../../components/core/typography/H3"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import themeVariables from "../../../theme/variables"; -import { useAvoidHardwareBackButton } from "../../../utils/useAvoidHardwareBackButton"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; - -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: themeVariables.contentPaddingLarge - }, - title: { - textAlign: "center" - } -}); - -export type Props = { - email: string; -}; - -const EmailAlreadyUsedModal = (props: Props) => { - useAvoidHardwareBackButton(); - - const continueButtonProps = { - // TODO: Jira ticket IOPID-689. Add new logic. - onPress: () => undefined, - title: I18n.t("email.cduModal.editMail.editButton"), - block: true - }; - - return ( - - - - - -

- {I18n.t("email.cduModal.editMail.title")} -

- - - {I18n.t("email.cduModal.editMail.subtitleStart")} - {" " + props.email + " "} - {I18n.t("email.cduModal.editMail.subtitleEnd")} - -
- -
-
- ); -}; -export default EmailAlreadyUsedModal; diff --git a/ts/screens/modal/mailCheck/ValidateEmailModal.tsx b/ts/screens/modal/mailCheck/ValidateEmailModal.tsx deleted file mode 100644 index a3558d8fbe7..00000000000 --- a/ts/screens/modal/mailCheck/ValidateEmailModal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react"; -import { View, SafeAreaView, StyleSheet, Modal } from "react-native"; -import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; -import I18n from "../../../i18n"; -import { Body } from "../../../components/core/typography/Body"; -import { H3 } from "../../../components/core/typography/H3"; -import { IOStyles } from "../../../components/core/variables/IOStyles"; -import themeVariables from "../../../theme/variables"; -import { useAvoidHardwareBackButton } from "../../../utils/useAvoidHardwareBackButton"; -import FooterWithButtons from "../../../components/ui/FooterWithButtons"; -import { Link } from "../../../components/core/typography/Link"; - -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: themeVariables.contentPaddingLarge - }, - title: { - textAlign: "center" - } -}); - -export type Props = { - email: string; -}; - -const ValidateEmailModal = (props: Props) => { - useAvoidHardwareBackButton(); - - const continueButtonProps = { - // TODO: Jira ticket IOPID-689. Add new logic. - onPress: () => undefined, - title: I18n.t("email.cduModal.validateMail.validateButton"), - block: true - }; - - return ( - - - - - -

- {I18n.t("email.cduModal.validateMail.title")} -

- - - {I18n.t("email.cduModal.validateMail.subtitle")} - - {props.email} - - undefined - } - > - {I18n.t("email.cduModal.validateMail.editButton")} - -
- -
-
- ); -}; -export default ValidateEmailModal; diff --git a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx new file mode 100644 index 00000000000..7fa68dcb2a9 --- /dev/null +++ b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx @@ -0,0 +1,125 @@ +import * as React from "react"; +import { View, SafeAreaView, StyleSheet } from "react-native"; +import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { useNavigation } from "@react-navigation/native"; +import I18n from "../../../i18n"; +import { Body } from "../../../components/core/typography/Body"; +import { H3 } from "../../../components/core/typography/H3"; +import { IOStyles } from "../../../components/core/variables/IOStyles"; +import themeVariables from "../../../theme/variables"; +import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; +import { CheckEmailParamsList } from "../../../navigation/params/CheckEmailParamsList"; +import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; +import NavigationService from "../../../navigation/NavigationService"; +import ROUTES from "../../../navigation/routes"; +import { useIOSelector } from "../../../store/hooks"; +import { usePrevious } from "../../../utils/hooks/usePrevious"; +import { emailValidationSelector } from "../../../store/reducers/emailValidation"; + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: themeVariables.contentPaddingLarge + }, + title: { + textAlign: "center" + } +}); + +export type OnboardingServicesPreferenceScreenNavigationParams = { + isFirstOnboarding: boolean; +}; +type Props = IOStackNavigationRouteProps< + CheckEmailParamsList, + "CHECK_EMAIL_ALREADY_TAKEN" +>; + +export type EmailAlreadyUsedScreenParamList = { + email: string; +}; + +const confirmButtonOnPress = () => { + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.READ_EMAIL_SCREEN + }); +}; + +const EmailAlreadyTakenScreen = (props: Props) => { + const { email } = props.route.params; + + const navigation = useNavigation(); + + const acknowledgeOnEmailValidated = useIOSelector( + emailValidationSelector + ).acknowledgeOnEmailValidated; + const previousAcknowledgeOnEmailValidated = usePrevious( + acknowledgeOnEmailValidated + ); + + // If the user has acknowledged the email validation, go back to the previous screen. + // The acknowledgeOnEmailValidated is set to NONE when the user presses the "Continue" button. + // The previousAcknowledgeOnEmailValidated is set to SOME(false) when the user see the success screen. + React.useEffect(() => { + if ( + previousAcknowledgeOnEmailValidated && + O.isSome(previousAcknowledgeOnEmailValidated) && + O.isNone(acknowledgeOnEmailValidated) + ) { + navigation.goBack(); + } + }, [ + acknowledgeOnEmailValidated, + navigation, + previousAcknowledgeOnEmailValidated + ]); + + const continueButtonProps = { + onPress: confirmButtonOnPress, + title: I18n.t("email.cduModal.editMail.editButton"), + block: true + }; + + return ( + + + + + +

+ {I18n.t("email.cduModal.editMail.title")} +

+ + + + {I18n.t("email.cduModal.editMail.subtitleStart")} + + + {" "} + {" " + email + " "} + + + {I18n.t("email.cduModal.editMail.subtitleEnd")} + + +
+ +
+
+ ); +}; +export default EmailAlreadyTakenScreen; diff --git a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx new file mode 100644 index 00000000000..cec488db891 --- /dev/null +++ b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { View, SafeAreaView, StyleSheet } from "react-native"; +import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; +import * as O from "fp-ts/lib/Option"; +import { useNavigation } from "@react-navigation/native"; +import I18n from "../../../i18n"; +import { Body } from "../../../components/core/typography/Body"; +import { H3 } from "../../../components/core/typography/H3"; +import { IOStyles } from "../../../components/core/variables/IOStyles"; +import themeVariables from "../../../theme/variables"; +import FooterWithButtons from "../../../components/ui/FooterWithButtons"; +import { Link } from "../../../components/core/typography/Link"; +import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; +import NavigationService from "../../../navigation/NavigationService"; +import ROUTES from "../../../navigation/routes"; +import { useIOSelector } from "../../../store/hooks"; +import { usePrevious } from "../../../utils/hooks/usePrevious"; +import { emailValidationSelector } from "../../../store/reducers/emailValidation"; + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: themeVariables.contentPaddingLarge + }, + title: { + textAlign: "center" + } +}); + +export type Props = { + email: string; +}; + +const confirmButtonOnPress = () => { + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.READ_EMAIL_SCREEN + }); +}; + +const modifyEmailButtonOnPress = () => { + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.READ_EMAIL_SCREEN + }); +}; + +const ValidateEmailScreen = (props: Props) => { + const continueButtonProps = { + onPress: confirmButtonOnPress, + title: I18n.t("email.cduModal.validateMail.validateButton"), + block: true + }; + + const navigation = useNavigation(); + + const acknowledgeOnEmailValidated = useIOSelector( + emailValidationSelector + ).acknowledgeOnEmailValidated; + const previousAcknowledgeOnEmailValidated = usePrevious( + acknowledgeOnEmailValidated + ); + + // If the user has acknowledged the email validation, go back to the previous screen. + // The acknowledgeOnEmailValidated is set to NONE when the user presses the "Continue" button. + // The previousAcknowledgeOnEmailValidated is set to SOME(false) when the user see the success screen. + React.useEffect(() => { + if ( + previousAcknowledgeOnEmailValidated && + O.isSome(previousAcknowledgeOnEmailValidated) && + O.isNone(acknowledgeOnEmailValidated) + ) { + navigation.goBack(); + } + }, [ + acknowledgeOnEmailValidated, + navigation, + previousAcknowledgeOnEmailValidated + ]); + + return ( + + + + + +

+ {I18n.t("email.cduModal.validateMail.title")} +

+ + + {I18n.t("email.cduModal.validateMail.subtitle")} + + {props.email} + + + {I18n.t("email.cduModal.validateMail.editButton")} + +
+ +
+
+ ); +}; +export default ValidateEmailScreen; diff --git a/ts/store/reducers/profile.ts b/ts/store/reducers/profile.ts index b8a97e85d3a..e2488a1ae05 100644 --- a/ts/store/reducers/profile.ts +++ b/ts/store/reducers/profile.ts @@ -135,6 +135,11 @@ export const isServicesPreferenceModeSet = ( export const isProfileEmailValidated = (user: InitializedProfile): boolean => user.is_email_validated !== undefined && user.is_email_validated === true; +// return true if the profile has an email and it is validated +export const isProfileEmailAlreadyTaken = ( + _user: InitializedProfile +): boolean => true; + // Returns true if the profile has service_preferences_settings set to Legacy. // A profile that has completed onboarding will have this value mandatory set to auto or manual export const isProfileFirstOnBoarding = (user: InitializedProfile): boolean => From 73e47470cbc1818fb822d5240a01438216faa3c3 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:00:52 +0100 Subject: [PATCH 21/35] Delete fixme --- ts/components/NewRemindEmailValidationOverlay.tsx | 1 - ts/navigation/OnboardingNavigator.tsx | 1 - ts/navigation/ProfileNavigator.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 5aadf161146..d64112eda75 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -133,7 +133,6 @@ const NewRemindEmailValidationOverlay = (props: Props) => { }; const navigateToInsertEmail = () => { - // FIXME -> understand if this function is needed dispatchAcknowledgeOnEmailValidation(O.none); hideModal(); }; diff --git a/ts/navigation/OnboardingNavigator.tsx b/ts/navigation/OnboardingNavigator.tsx index 026e63fb4c0..58ec177a8ae 100644 --- a/ts/navigation/OnboardingNavigator.tsx +++ b/ts/navigation/OnboardingNavigator.tsx @@ -65,7 +65,6 @@ const navigator = () => ( name={ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN} component={OnboardingEmailInsertScreen} /> - {/* FIXME -> understand if this control is necessary */} { headerShown: false }} name={ROUTES.INSERT_EMAIL_SCREEN} - // FIXME -> understand if this control is necessary component={isNewCduFlow ? NewEmailInsertScreen : EmailInsertScreen} /> Date: Mon, 27 Nov 2023 10:07:52 +0100 Subject: [PATCH 22/35] chore: fix space in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index baa49fb271c..23d08290291 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.47.0-rc.0", "io_backend_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_backend.yaml", "io_public_api": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_public.yaml", - "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", + "io_content_specs": "https://raw.githubusercontent.com/pagopa/io-services-metadata/1.0.28/definitions.yml", "io_bonus_vacanze_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_bonus.yaml", "io_cgn_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_cgn.yaml", "io_cgn_merchants_specs": "https://raw.githubusercontent.com/pagopa/io-backend/v13.22.0-RELEASE/api_cgn_operator_search.yaml", From 99f306b49389e474c225e03dfc9653d4ffa37212 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:13:19 +0100 Subject: [PATCH 23/35] chore: add new data handling --- ts/navigation/AuthenticatedStackNavigator.tsx | 6 +++++- ts/store/reducers/profile.ts | 5 ++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ts/navigation/AuthenticatedStackNavigator.tsx b/ts/navigation/AuthenticatedStackNavigator.tsx index d7981589d67..2c9e85c0f31 100644 --- a/ts/navigation/AuthenticatedStackNavigator.tsx +++ b/ts/navigation/AuthenticatedStackNavigator.tsx @@ -105,7 +105,11 @@ const AuthenticatedStackNavigator = () => { component={OnboardingNavigator} /> - + user.is_email_validated !== undefined && user.is_email_validated === true; // return true if the profile has an email and it is validated -export const isProfileEmailAlreadyTaken = ( - _user: InitializedProfile -): boolean => true; +export const isProfileEmailAlreadyTaken = (user: InitializedProfile): boolean => + !!user.is_email_already_taken; // Returns true if the profile has service_preferences_settings set to Legacy. // A profile that has completed onboarding will have this value mandatory set to auto or manual From baacefa758b1e73a33aa4ee9f24ad0b141c9ca0c Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:34:21 +0100 Subject: [PATCH 24/35] chore: Add email check redux state additional field --- .../NewRemindEmailValidationOverlay.tsx | 21 +++++-- ts/sagas/startup.ts | 3 +- .../startup/checkAcknowledgedEmailSaga.ts | 3 +- ts/sagas/startup/checkEmailSaga.ts | 14 ++++- ts/screens/profile/NewEmailInsertScreen.tsx | 17 +++++ .../mailCheck/EmailAlreadyTakenScreen.tsx | 39 +++--------- .../profile/mailCheck/ValidateEmailScreen.tsx | 62 +++++++------------ ts/store/actions/profile.ts | 7 ++- ts/store/reducers/emailValidation.ts | 7 ++- 9 files changed, 93 insertions(+), 80 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index d64112eda75..24caaa5b338 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -21,6 +21,7 @@ import I18n from "../i18n"; import { acknowledgeOnEmailValidation, profileLoadRequest, + setEmailCheckAtStartup, startEmailValidation } from "../store/actions/profile"; import { @@ -29,9 +30,9 @@ import { } from "../store/reducers/profile"; import { useIODispatch, useIOSelector } from "../store/hooks"; import { emailValidationSelector } from "../store/reducers/emailValidation"; -import { emailAcknowledged } from "../store/actions/onboarding"; import NavigationService from "../navigation/NavigationService"; import ROUTES from "../navigation/routes"; +import { emailAcknowledged } from "../store/actions/onboarding"; import { IOStyles } from "./core/variables/IOStyles"; import FooterWithButtons from "./ui/FooterWithButtons"; import { IOToast } from "./Toast"; @@ -72,14 +73,17 @@ const NewRemindEmailValidationOverlay = (props: Props) => { () => dispatch(startEmailValidation.request()), [dispatch] ); + const acknowledgeEmail = useCallback( () => dispatch(emailAcknowledged()), [dispatch] ); + const reloadProfile = useCallback( () => dispatch(profileLoadRequest()), [dispatch] ); + const dispatchAcknowledgeOnEmailValidation = useCallback( (maybeAcknowledged: O.Option) => dispatch(acknowledgeOnEmailValidation(maybeAcknowledged)), @@ -117,9 +121,18 @@ const NewRemindEmailValidationOverlay = (props: Props) => { hideModal(); } else { hideModal(); - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.PROFILE_DATA - }); + if ( + O.isSome(emailValidation.emailCheckAtStartup) && + emailValidation.emailCheckAtStartup.value + ) { + acknowledgeEmail(); + dispatchAcknowledgeOnEmailValidation(O.none); + dispatch(setEmailCheckAtStartup(O.none)); + } else { + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.PROFILE_DATA + }); + } } } else { // send email validation only if it exists diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 24a2f77a4e8..fe3b4272812 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -27,6 +27,7 @@ import { bpdEnabled, cdcEnabled, euCovidCertificateEnabled, + isNewCduFlow, pagoPaApiUrlPrefix, pagoPaApiUrlPrefixTest, svEnabled, @@ -506,9 +507,9 @@ export function* initializeApplicationSaga( yield* call(clearKeychainError); yield* call(checkConfiguredPinSaga); + yield* call(checkAcknowledgedFingerprintSaga); if (!hasPreviousSessionAndPin) { - yield* call(checkAcknowledgedFingerprintSaga); yield* call(checkAcknowledgedEmailSaga, userProfile); } diff --git a/ts/sagas/startup/checkAcknowledgedEmailSaga.ts b/ts/sagas/startup/checkAcknowledgedEmailSaga.ts index 76c93994c9b..2872e3e0939 100644 --- a/ts/sagas/startup/checkAcknowledgedEmailSaga.ts +++ b/ts/sagas/startup/checkAcknowledgedEmailSaga.ts @@ -10,6 +10,7 @@ import { isProfileFirstOnBoarding } from "../../store/reducers/profile"; import { ReduxSagaEffect } from "../../types/utils"; +import { isNewCduFlow } from "../../config"; /** * Launch email saga that consists of: @@ -25,7 +26,7 @@ export function* checkAcknowledgedEmailSaga( if (hasProfileEmail(userProfile)) { if ( isProfileFirstOnBoarding(userProfile) || - !isProfileEmailValidated(userProfile) + (!isNewCduFlow && !isProfileEmailValidated(userProfile)) ) { // The user profile is just created (first onboarding), the conditional // view displays the screen to show the user's email used in app diff --git a/ts/sagas/startup/checkEmailSaga.ts b/ts/sagas/startup/checkEmailSaga.ts index c6e6f55e489..5de34ed84c2 100644 --- a/ts/sagas/startup/checkEmailSaga.ts +++ b/ts/sagas/startup/checkEmailSaga.ts @@ -1,4 +1,6 @@ -import { call } from "typed-redux-saga/macro"; +import { call, put, take } from "typed-redux-saga/macro"; +import * as O from "fp-ts/lib/Option"; +import { StackActions } from "@react-navigation/native"; import { InitializedProfile } from "../../../definitions/backend/InitializedProfile"; import NavigationService from "../../navigation/NavigationService"; import ROUTES from "../../navigation/routes"; @@ -8,11 +10,14 @@ import { } from "../../store/reducers/profile"; import { ReduxSagaEffect } from "../../types/utils"; import { isNewCduFlow } from "../../config"; +import { setEmailCheckAtStartup } from "../../store/actions/profile"; +import { emailAcknowledged } from "../../store/actions/onboarding"; export function* checkEmailSaga( userProfile: InitializedProfile ): IterableIterator { if (isNewCduFlow && !isProfileEmailValidated(userProfile)) { + yield* put(setEmailCheckAtStartup(O.some(true))); if (isProfileEmailAlreadyTaken(userProfile)) { yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, @@ -23,5 +28,12 @@ export function* checkEmailSaga( screen: ROUTES.CHECK_EMAIL_NOT_VERIFIED }); } + // Wait for the user to press "Continue" button after having checked out + // his own email + yield* take(emailAcknowledged); + yield* call( + NavigationService.dispatchNavigationAction, + StackActions.popToTop() + ); } } diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/NewEmailInsertScreen.tsx index dba0cb2f9bb..ac1e1aa16e8 100644 --- a/ts/screens/profile/NewEmailInsertScreen.tsx +++ b/ts/screens/profile/NewEmailInsertScreen.tsx @@ -48,6 +48,7 @@ import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { LightModalContext } from "../../components/ui/LightModal"; import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; +import { emailValidationSelector } from "../../store/reducers/emailValidation"; type Props = IOStackNavigationRouteProps< ProfileParamsList, @@ -79,6 +80,10 @@ const NewEmailInsertScreen = (props: Props) => { const profile = useIOSelector(profileSelector); const optionEmail = useIOSelector(profileEmailSelector); const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); + const acknowledgeOnEmailValidated = useIOSelector( + emailValidationSelector + ).acknowledgeOnEmailValidated; + const isLoading = useMemo( () => pot.isUpdating(profile) || pot.isLoading(profile), [profile] @@ -178,6 +183,18 @@ const NewEmailInsertScreen = (props: Props) => { } }, [prevUserProfile, profile]); + // If we navigate to this screen with acknowledgeOnEmailValidated set to false, + // we show the modal to remind the user to validate the email. + // This is used during the check of the email at startup. + useEffect(() => { + if ( + O.isSome(acknowledgeOnEmailValidated) && + acknowledgeOnEmailValidated.value === false + ) { + showModal(); + } + }, [acknowledgeOnEmailValidated, showModal]); + useEffect(() => { if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { diff --git a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx index 7fa68dcb2a9..939c091d516 100644 --- a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx +++ b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { View, SafeAreaView, StyleSheet } from "react-native"; import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; -import { useNavigation } from "@react-navigation/native"; import I18n from "../../../i18n"; import { Body } from "../../../components/core/typography/Body"; import { H3 } from "../../../components/core/typography/H3"; @@ -14,9 +13,8 @@ import { CheckEmailParamsList } from "../../../navigation/params/CheckEmailParam import { IOStackNavigationRouteProps } from "../../../navigation/params/AppParamsList"; import NavigationService from "../../../navigation/NavigationService"; import ROUTES from "../../../navigation/routes"; -import { useIOSelector } from "../../../store/hooks"; -import { usePrevious } from "../../../utils/hooks/usePrevious"; -import { emailValidationSelector } from "../../../store/reducers/emailValidation"; +import { useIODispatch } from "../../../store/hooks"; +import { acknowledgeOnEmailValidation } from "../../../store/actions/profile"; const styles = StyleSheet.create({ mainContainer: { @@ -42,40 +40,21 @@ export type EmailAlreadyUsedScreenParamList = { email: string; }; -const confirmButtonOnPress = () => { +const navigateToInsertEmailScreen = () => { NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.READ_EMAIL_SCREEN + screen: ROUTES.INSERT_EMAIL_SCREEN }); }; const EmailAlreadyTakenScreen = (props: Props) => { const { email } = props.route.params; - const navigation = useNavigation(); + const dispatch = useIODispatch(); - const acknowledgeOnEmailValidated = useIOSelector( - emailValidationSelector - ).acknowledgeOnEmailValidated; - const previousAcknowledgeOnEmailValidated = usePrevious( - acknowledgeOnEmailValidated - ); - - // If the user has acknowledged the email validation, go back to the previous screen. - // The acknowledgeOnEmailValidated is set to NONE when the user presses the "Continue" button. - // The previousAcknowledgeOnEmailValidated is set to SOME(false) when the user see the success screen. - React.useEffect(() => { - if ( - previousAcknowledgeOnEmailValidated && - O.isSome(previousAcknowledgeOnEmailValidated) && - O.isNone(acknowledgeOnEmailValidated) - ) { - navigation.goBack(); - } - }, [ - acknowledgeOnEmailValidated, - navigation, - previousAcknowledgeOnEmailValidated - ]); + const confirmButtonOnPress = () => { + dispatch(acknowledgeOnEmailValidation(O.none)); + navigateToInsertEmailScreen(); + }; const continueButtonProps = { onPress: confirmButtonOnPress, diff --git a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx index cec488db891..2dc708401af 100644 --- a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx +++ b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { View, SafeAreaView, StyleSheet } from "react-native"; import { Pictogram, VSpacer } from "@pagopa/io-app-design-system"; import * as O from "fp-ts/lib/Option"; -import { useNavigation } from "@react-navigation/native"; import I18n from "../../../i18n"; import { Body } from "../../../components/core/typography/Body"; import { H3 } from "../../../components/core/typography/H3"; @@ -13,9 +12,8 @@ import { Link } from "../../../components/core/typography/Link"; import BaseScreenComponent from "../../../components/screens/BaseScreenComponent"; import NavigationService from "../../../navigation/NavigationService"; import ROUTES from "../../../navigation/routes"; -import { useIOSelector } from "../../../store/hooks"; -import { usePrevious } from "../../../utils/hooks/usePrevious"; -import { emailValidationSelector } from "../../../store/reducers/emailValidation"; +import { useIODispatch } from "../../../store/hooks"; +import { acknowledgeOnEmailValidation } from "../../../store/actions/profile"; const styles = StyleSheet.create({ mainContainer: { @@ -33,51 +31,33 @@ export type Props = { email: string; }; -const confirmButtonOnPress = () => { - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.READ_EMAIL_SCREEN - }); -}; +const ValidateEmailScreen = (props: Props) => { + const dispatch = useIODispatch(); -const modifyEmailButtonOnPress = () => { - NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { - screen: ROUTES.READ_EMAIL_SCREEN - }); -}; + const navigateToInsertEmailScreen = () => { + NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { + screen: ROUTES.INSERT_EMAIL_SCREEN + }); + }; + + const confirmButtonOnPress = () => { + // We dispatch this action to show the InsertEmailScreen with + // the validation modal already opened. + dispatch(acknowledgeOnEmailValidation(O.some(false))); + navigateToInsertEmailScreen(); + }; + + const modifyEmailButtonOnPress = () => { + dispatch(acknowledgeOnEmailValidation(O.none)); + navigateToInsertEmailScreen(); + }; -const ValidateEmailScreen = (props: Props) => { const continueButtonProps = { onPress: confirmButtonOnPress, title: I18n.t("email.cduModal.validateMail.validateButton"), block: true }; - const navigation = useNavigation(); - - const acknowledgeOnEmailValidated = useIOSelector( - emailValidationSelector - ).acknowledgeOnEmailValidated; - const previousAcknowledgeOnEmailValidated = usePrevious( - acknowledgeOnEmailValidated - ); - - // If the user has acknowledged the email validation, go back to the previous screen. - // The acknowledgeOnEmailValidated is set to NONE when the user presses the "Continue" button. - // The previousAcknowledgeOnEmailValidated is set to SOME(false) when the user see the success screen. - React.useEffect(() => { - if ( - previousAcknowledgeOnEmailValidated && - O.isSome(previousAcknowledgeOnEmailValidated) && - O.isNone(acknowledgeOnEmailValidated) - ) { - navigation.goBack(); - } - }, [ - acknowledgeOnEmailValidated, - navigation, - previousAcknowledgeOnEmailValidated - ]); - return ( >(); +export const setEmailCheckAtStartup = createStandardAction( + "SET_EMAIL_CHECK_AT_STARTUP" +)>(); + export const profileFirstLogin = createStandardAction("PROFILE_FIRST_LOGIN")(); export const clearCache = createStandardAction("CLEAR_CACHE")(); @@ -88,4 +92,5 @@ export type ProfileActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType + | ActionType; diff --git a/ts/store/reducers/emailValidation.ts b/ts/store/reducers/emailValidation.ts index 30b04db740c..f1353db2348 100644 --- a/ts/store/reducers/emailValidation.ts +++ b/ts/store/reducers/emailValidation.ts @@ -9,6 +9,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; import { acknowledgeOnEmailValidation, + setEmailCheckAtStartup, startEmailValidation } from "../actions/profile"; import { Action } from "../actions/types"; @@ -17,11 +18,13 @@ import { GlobalState } from "./types"; export type EmailValidationState = { sendEmailValidationRequest: pot.Pot; acknowledgeOnEmailValidated: O.Option; + emailCheckAtStartup: O.Option; }; const INITIAL_STATE: EmailValidationState = { sendEmailValidationRequest: pot.none, - acknowledgeOnEmailValidated: O.none + acknowledgeOnEmailValidated: O.none, + emailCheckAtStartup: O.none }; // Selector @@ -50,6 +53,8 @@ const reducer = ( return { ...state, sendEmailValidationRequest: pot.some(undefined) }; case getType(acknowledgeOnEmailValidation): return { ...state, acknowledgeOnEmailValidated: action.payload }; + case getType(setEmailCheckAtStartup): + return { ...state, emailCheckAtStartup: action.payload }; default: return state; } From 09aea037cd8731786731809a0062fe06b8f80d86 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:09:37 +0100 Subject: [PATCH 25/35] chore: add some useCallback --- ts/sagas/startup.ts | 1 - ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx | 4 ++-- ts/screens/profile/mailCheck/ValidateEmailScreen.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index fe3b4272812..9a5dba46e73 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -27,7 +27,6 @@ import { bpdEnabled, cdcEnabled, euCovidCertificateEnabled, - isNewCduFlow, pagoPaApiUrlPrefix, pagoPaApiUrlPrefixTest, svEnabled, diff --git a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx index 939c091d516..15227e24079 100644 --- a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx +++ b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx @@ -51,10 +51,10 @@ const EmailAlreadyTakenScreen = (props: Props) => { const dispatch = useIODispatch(); - const confirmButtonOnPress = () => { + const confirmButtonOnPress = React.useCallback(() => { dispatch(acknowledgeOnEmailValidation(O.none)); navigateToInsertEmailScreen(); - }; + }, [dispatch]); const continueButtonProps = { onPress: confirmButtonOnPress, diff --git a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx index 2dc708401af..bdd6717ff67 100644 --- a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx +++ b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx @@ -40,17 +40,17 @@ const ValidateEmailScreen = (props: Props) => { }); }; - const confirmButtonOnPress = () => { + const confirmButtonOnPress = React.useCallback(() => { // We dispatch this action to show the InsertEmailScreen with // the validation modal already opened. dispatch(acknowledgeOnEmailValidation(O.some(false))); navigateToInsertEmailScreen(); - }; + }, [dispatch]); - const modifyEmailButtonOnPress = () => { + const modifyEmailButtonOnPress = React.useCallback(() => { dispatch(acknowledgeOnEmailValidation(O.none)); navigateToInsertEmailScreen(); - }; + }, [dispatch]); const continueButtonProps = { onPress: confirmButtonOnPress, From 6aca40ecb0f293408fe60685901c313ff2385701 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:16:31 +0100 Subject: [PATCH 26/35] chore: restore hideModal to its previous position --- ts/components/NewRemindEmailValidationOverlay.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 24caaa5b338..79eaaa1e0a4 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -114,13 +114,12 @@ const NewRemindEmailValidationOverlay = (props: Props) => { const handleSendEmailValidationButton = () => { if (isEmailValidated) { + hideModal(); if (isOnboarding) { // if the user is in the onboarding flow and the email il correctly validated, // the email validation flow is finished acknowledgeEmail(); - hideModal(); } else { - hideModal(); if ( O.isSome(emailValidation.emailCheckAtStartup) && emailValidation.emailCheckAtStartup.value From 7bc0fe735b71bddaa751c696b47f76d2fb0aeeb3 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:25:52 +0100 Subject: [PATCH 27/35] chore: renaming --- ts/components/NewRemindEmailValidationOverlay.tsx | 8 ++++---- ts/sagas/startup/checkEmailSaga.ts | 4 ++-- ts/store/actions/profile.ts | 6 +++--- ts/store/reducers/emailValidation.ts | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 79eaaa1e0a4..68d6fc137f5 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -21,7 +21,7 @@ import I18n from "../i18n"; import { acknowledgeOnEmailValidation, profileLoadRequest, - setEmailCheckAtStartup, + setEmailCheckAtStartupFailure, startEmailValidation } from "../store/actions/profile"; import { @@ -121,12 +121,12 @@ const NewRemindEmailValidationOverlay = (props: Props) => { acknowledgeEmail(); } else { if ( - O.isSome(emailValidation.emailCheckAtStartup) && - emailValidation.emailCheckAtStartup.value + O.isSome(emailValidation.emailCheckAtStartupFailed) && + emailValidation.emailCheckAtStartupFailed.value ) { acknowledgeEmail(); dispatchAcknowledgeOnEmailValidation(O.none); - dispatch(setEmailCheckAtStartup(O.none)); + dispatch(setEmailCheckAtStartupFailure(O.none)); } else { NavigationService.navigate(ROUTES.PROFILE_NAVIGATOR, { screen: ROUTES.PROFILE_DATA diff --git a/ts/sagas/startup/checkEmailSaga.ts b/ts/sagas/startup/checkEmailSaga.ts index 5de34ed84c2..274251ece50 100644 --- a/ts/sagas/startup/checkEmailSaga.ts +++ b/ts/sagas/startup/checkEmailSaga.ts @@ -10,14 +10,14 @@ import { } from "../../store/reducers/profile"; import { ReduxSagaEffect } from "../../types/utils"; import { isNewCduFlow } from "../../config"; -import { setEmailCheckAtStartup } from "../../store/actions/profile"; +import { setEmailCheckAtStartupFailure } from "../../store/actions/profile"; import { emailAcknowledged } from "../../store/actions/onboarding"; export function* checkEmailSaga( userProfile: InitializedProfile ): IterableIterator { if (isNewCduFlow && !isProfileEmailValidated(userProfile)) { - yield* put(setEmailCheckAtStartup(O.some(true))); + yield* put(setEmailCheckAtStartupFailure(O.some(true))); if (isProfileEmailAlreadyTaken(userProfile)) { yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, diff --git a/ts/store/actions/profile.ts b/ts/store/actions/profile.ts index e70a7ad8183..8b77dcba261 100644 --- a/ts/store/actions/profile.ts +++ b/ts/store/actions/profile.ts @@ -50,8 +50,8 @@ export const acknowledgeOnEmailValidation = createStandardAction( "ACKNOWLEDGE_ON_EMAIL_VALIDATION" )>(); -export const setEmailCheckAtStartup = createStandardAction( - "SET_EMAIL_CHECK_AT_STARTUP" +export const setEmailCheckAtStartupFailure = createStandardAction( + "SET_EMAIL_CHECK_AT_STARTUP_FAILURE" )>(); export const profileFirstLogin = createStandardAction("PROFILE_FIRST_LOGIN")(); @@ -93,4 +93,4 @@ export type ProfileActions = | ActionType | ActionType | ActionType - | ActionType; + | ActionType; diff --git a/ts/store/reducers/emailValidation.ts b/ts/store/reducers/emailValidation.ts index f1353db2348..24f667b11a3 100644 --- a/ts/store/reducers/emailValidation.ts +++ b/ts/store/reducers/emailValidation.ts @@ -9,7 +9,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { getType } from "typesafe-actions"; import { acknowledgeOnEmailValidation, - setEmailCheckAtStartup, + setEmailCheckAtStartupFailure, startEmailValidation } from "../actions/profile"; import { Action } from "../actions/types"; @@ -18,13 +18,13 @@ import { GlobalState } from "./types"; export type EmailValidationState = { sendEmailValidationRequest: pot.Pot; acknowledgeOnEmailValidated: O.Option; - emailCheckAtStartup: O.Option; + emailCheckAtStartupFailed: O.Option; }; const INITIAL_STATE: EmailValidationState = { sendEmailValidationRequest: pot.none, acknowledgeOnEmailValidated: O.none, - emailCheckAtStartup: O.none + emailCheckAtStartupFailed: O.none }; // Selector @@ -53,8 +53,8 @@ const reducer = ( return { ...state, sendEmailValidationRequest: pot.some(undefined) }; case getType(acknowledgeOnEmailValidation): return { ...state, acknowledgeOnEmailValidated: action.payload }; - case getType(setEmailCheckAtStartup): - return { ...state, emailCheckAtStartup: action.payload }; + case getType(setEmailCheckAtStartupFailure): + return { ...state, emailCheckAtStartupFailed: action.payload }; default: return state; } From 75610263eed3cd6a4a850762e272469a575a87d4 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:11:49 +0100 Subject: [PATCH 28/35] chore: update translations --- locales/en/index.yml | 12 ++++++++++-- locales/it/index.yml | 16 ++++++++++++---- .../mailCheck/EmailAlreadyTakenScreen.tsx | 14 +++++++------- .../profile/mailCheck/ValidateEmailScreen.tsx | 14 +++++++------- 4 files changed, 36 insertions(+), 20 deletions(-) diff --git a/locales/en/index.yml b/locales/en/index.yml index 03d30459cac..25930bdf506 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -749,17 +749,25 @@ authentication: listitem3_4: recovery code listitem3_5: to confirm the operation. email: - cduModal: + cduScreens: validateMail: title: Email address not validated subtitle: To continue using the app, you must validate your email address editButton: Edit email address validateButton: Validate email address - editMail: + header: + title: Configure IO + help: + body: To continue using the app, you must validate your email address. + emailAlreadyTaken: title: Edit your email address subtitleStart: This email address subtitleEnd: seems to be already in use on IO. Please enter a different one to continue using the app. editButton: Edit email address + header: + title: Configure IO + help: + body: This email address seems to be already in use on IO. Please enter a different one to continue using the app. read: title: Your email address info: This is the email address associated to your SPID account. You can use in IO app a different email address, if you wish. To make operative the new email address, you will need to validate it by clicking on the link you will receive by email. diff --git a/locales/it/index.yml b/locales/it/index.yml index 04d0f26953c..38886e3bd14 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -749,17 +749,25 @@ authentication: listitem3_4: codice di ripristino listitem3_5: per confermare l’operazione. email: - cduModal: + cduScreens: validateMail: - title: Indirizzo email non validato + title: Non hai confermato il tuo indirizzo email subtitle: Per continuare a utilizzare l’app è necessario validare il tuo indirizzo email editButton: Modifica email - validateButton: Valida email - editMail: + validateButton: Conferma email + header: + title: Configura IO + help: + body: Per continuare a utilizzare l’app è necessario validare il tuo indirizzo email. + emailAlreadyTaken: title: Modifica la tua email subtitleStart: Il tuo indirizzo email subtitleEnd: risulta già in uso su IO, è necessario inserirne uno diverso per continuare a utilizzare l’app. editButton: Modifica email + header: + title: Configura IO + help: + body: Il tuo indirizzo email risulta già in uso su IO, è necessario inserirne uno diverso per continuare a utilizzare l’app. read: title: Il tuo indirizzo email info: Questo è l'indirizzo email che hai associato all'account SPID. Puoi usare in IO un indirizzo email diverso, se preferisci. Per rendere operativo il nuovo indirizzo, dovrai validarlo cliccando sul link che riceverai nella tua casella di posta elettronica. diff --git a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx index 15227e24079..424eda85a04 100644 --- a/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx +++ b/ts/screens/profile/mailCheck/EmailAlreadyTakenScreen.tsx @@ -58,7 +58,7 @@ const EmailAlreadyTakenScreen = (props: Props) => { const continueButtonProps = { onPress: confirmButtonOnPress, - title: I18n.t("email.cduModal.editMail.editButton"), + title: I18n.t("email.cduScreens.emailAlreadyTaken.editButton"), block: true }; @@ -67,29 +67,29 @@ const EmailAlreadyTakenScreen = (props: Props) => { goBack={false} accessibilityEvents={{ avoidNavigationEventsUsage: true }} contextualHelpMarkdown={{ - title: "email.validate.title", - body: "email.validate.help" + title: "email.cduScreens.emailAlreadyTaken.title", + body: "email.cduScreens.emailAlreadyTaken.help.body" }} - headerTitle={I18n.t("email.newinsert.header")} + headerTitle={I18n.t("email.cduScreens.emailAlreadyTaken.header.title")} >

- {I18n.t("email.cduModal.editMail.title")} + {I18n.t("email.cduScreens.emailAlreadyTaken.title")}

- {I18n.t("email.cduModal.editMail.subtitleStart")} + {I18n.t("email.cduScreens.emailAlreadyTaken.subtitleStart")} {" "} {" " + email + " "} - {I18n.t("email.cduModal.editMail.subtitleEnd")} + {I18n.t("email.cduScreens.emailAlreadyTaken.subtitleEnd")}
diff --git a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx index bdd6717ff67..64b22f12ae9 100644 --- a/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx +++ b/ts/screens/profile/mailCheck/ValidateEmailScreen.tsx @@ -54,7 +54,7 @@ const ValidateEmailScreen = (props: Props) => { const continueButtonProps = { onPress: confirmButtonOnPress, - title: I18n.t("email.cduModal.validateMail.validateButton"), + title: I18n.t("email.cduScreens.validateMail.validateButton"), block: true }; @@ -63,26 +63,26 @@ const ValidateEmailScreen = (props: Props) => { goBack={false} accessibilityEvents={{ avoidNavigationEventsUsage: true }} contextualHelpMarkdown={{ - title: "email.validate.title", - body: "email.validate.help" + title: "email.cduScreens.validateMail.title", + body: "email.cduScreens.validateMail.help.body" }} - headerTitle={I18n.t("email.newinsert.header")} + headerTitle={I18n.t("email.cduScreens.validateMail.header.title")} >

- {I18n.t("email.cduModal.validateMail.title")} + {I18n.t("email.cduScreens.validateMail.title")}

- {I18n.t("email.cduModal.validateMail.subtitle")} + {I18n.t("email.cduScreens.validateMail.subtitle")} {props.email} - {I18n.t("email.cduModal.validateMail.editButton")} + {I18n.t("email.cduScreens.validateMail.editButton")}
Date: Tue, 28 Nov 2023 14:57:11 +0100 Subject: [PATCH 29/35] chore: fix conflicts hell --- ts/components/NewRemindEmailValidationOverlay.tsx | 6 ------ ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx | 6 ------ ts/screens/profile/NewEmailInsertScreen.tsx | 2 -- 3 files changed, 14 deletions(-) diff --git a/ts/components/NewRemindEmailValidationOverlay.tsx b/ts/components/NewRemindEmailValidationOverlay.tsx index 2bbc5fd74b7..f503c77da02 100644 --- a/ts/components/NewRemindEmailValidationOverlay.tsx +++ b/ts/components/NewRemindEmailValidationOverlay.tsx @@ -33,7 +33,6 @@ import { emailValidationSelector } from "../store/reducers/emailValidation"; import { emailAcknowledged } from "../store/actions/onboarding"; import NavigationService from "../navigation/NavigationService"; import ROUTES from "../navigation/routes"; -import { emailAcknowledged } from "../store/actions/onboarding"; import { IOStyles } from "./core/variables/IOStyles"; import FooterWithButtons from "./ui/FooterWithButtons"; import { IOToast } from "./Toast"; @@ -84,11 +83,6 @@ const NewRemindEmailValidationOverlay = (props: Props) => { () => dispatch(profileLoadRequest()), [dispatch] ); - const dispatchAcknowledgeOnEmailValidation = useCallback( - (maybeAcknowledged: O.Option) => - dispatch(acknowledgeOnEmailValidation(maybeAcknowledged)), - [dispatch] - ); const dispatchAcknowledgeOnEmailValidation = useCallback( (maybeAcknowledged: O.Option) => diff --git a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx index e14e3296e92..63b3469cea8 100644 --- a/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx +++ b/ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx @@ -50,8 +50,6 @@ import { usePrevious } from "../../utils/hooks/usePrevious"; import { LightModalContext } from "../../components/ui/LightModal"; import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; -import { emailValidationSelector } from "../../store/reducers/emailValidation"; - const styles = StyleSheet.create({ flex: { flex: 1 @@ -81,10 +79,6 @@ const NewOnboardingEmailInsertScreen = () => { isProfileEmailAlreadyTakenSelector ); - const { acknowledgeOnEmailValidated } = useIOSelector( - emailValidationSelector - ); - const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); const prevUserProfile = usePrevious(profile); diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/NewEmailInsertScreen.tsx index fd8b82a0a89..4c44189348c 100644 --- a/ts/screens/profile/NewEmailInsertScreen.tsx +++ b/ts/screens/profile/NewEmailInsertScreen.tsx @@ -176,8 +176,6 @@ const NewEmailInsertScreen = (props: Props) => { setEmail(O.some(EMPTY_EMAIL)); }, []); - const prevUserProfile = usePrevious(profile); - useEffect(() => { if (prevUserProfile) { const isPrevCurrentSameState = prevUserProfile.kind === profile.kind; From 69cf4525fbb321f5d373b08bfed1a36491ee528a Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:26:51 +0100 Subject: [PATCH 30/35] fix: email check flow during first onboarding --- ts/sagas/startup.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index 9a5dba46e73..e1ef4cfc184 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -415,7 +415,8 @@ export function* initializeApplicationSaga( return; } - const userProfile = maybeUserProfile.value; + // eslint-disable-next-line functional/no-let + let userProfile = maybeUserProfile.value; // If user logged in with different credentials, but this device still has // user data loaded, then delete data keeping current session (user already @@ -512,7 +513,13 @@ export function* initializeApplicationSaga( yield* call(checkAcknowledgedEmailSaga, userProfile); } - yield* call(checkEmailSaga, userProfile); + // If we enetered checkAcknowledgedEmailSaga, + // the profile may have been updated, so we need to retrieve it again. + const maybeUpdatedEmailFieldUserProfile = yield* select(profileSelector); + if (pot.isSome(maybeUpdatedEmailFieldUserProfile)) { + userProfile = maybeUpdatedEmailFieldUserProfile.value; + yield* call(checkEmailSaga, userProfile); + } // check if the user must set preferences for push notifications (e.g. reminders) yield* call(checkNotificationsPreferencesSaga, userProfile); From 738aa2330ed21b2ec9868fbc724493125a76eed2 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:59:35 +0100 Subject: [PATCH 31/35] chore: refactor checkEmailSaga --- ts/sagas/startup.ts | 8 +--- ts/sagas/startup/checkEmailSaga.ts | 63 ++++++++++++++++++------------ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/ts/sagas/startup.ts b/ts/sagas/startup.ts index e1ef4cfc184..6a130a2e726 100644 --- a/ts/sagas/startup.ts +++ b/ts/sagas/startup.ts @@ -513,13 +513,7 @@ export function* initializeApplicationSaga( yield* call(checkAcknowledgedEmailSaga, userProfile); } - // If we enetered checkAcknowledgedEmailSaga, - // the profile may have been updated, so we need to retrieve it again. - const maybeUpdatedEmailFieldUserProfile = yield* select(profileSelector); - if (pot.isSome(maybeUpdatedEmailFieldUserProfile)) { - userProfile = maybeUpdatedEmailFieldUserProfile.value; - yield* call(checkEmailSaga, userProfile); - } + userProfile = (yield* call(checkEmailSaga)) || userProfile; // check if the user must set preferences for push notifications (e.g. reminders) yield* call(checkNotificationsPreferencesSaga, userProfile); diff --git a/ts/sagas/startup/checkEmailSaga.ts b/ts/sagas/startup/checkEmailSaga.ts index 274251ece50..b75c191154b 100644 --- a/ts/sagas/startup/checkEmailSaga.ts +++ b/ts/sagas/startup/checkEmailSaga.ts @@ -1,39 +1,52 @@ -import { call, put, take } from "typed-redux-saga/macro"; +import { call, put, select, take } from "typed-redux-saga/macro"; import * as O from "fp-ts/lib/Option"; +import * as pot from "@pagopa/ts-commons/lib/pot"; import { StackActions } from "@react-navigation/native"; -import { InitializedProfile } from "../../../definitions/backend/InitializedProfile"; import NavigationService from "../../navigation/NavigationService"; import ROUTES from "../../navigation/routes"; import { isProfileEmailValidated, - isProfileEmailAlreadyTaken + isProfileEmailAlreadyTaken, + profileSelector } from "../../store/reducers/profile"; -import { ReduxSagaEffect } from "../../types/utils"; import { isNewCduFlow } from "../../config"; import { setEmailCheckAtStartupFailure } from "../../store/actions/profile"; import { emailAcknowledged } from "../../store/actions/onboarding"; -export function* checkEmailSaga( - userProfile: InitializedProfile -): IterableIterator { - if (isNewCduFlow && !isProfileEmailValidated(userProfile)) { - yield* put(setEmailCheckAtStartupFailure(O.some(true))); - if (isProfileEmailAlreadyTaken(userProfile)) { - yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { - screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, - params: { email: userProfile.email } - }); - } else { - yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { - screen: ROUTES.CHECK_EMAIL_NOT_VERIFIED - }); +export function* checkEmailSaga() { + // We get the latest profile from the store + const profile = yield* select(profileSelector); + // When we use this saga, we are sure that the profile is not none + if (pot.isSome(profile)) { + // eslint-disable-next-line functional/no-let + let userProfile = profile.value; + if (isNewCduFlow && !isProfileEmailValidated(userProfile)) { + yield* put(setEmailCheckAtStartupFailure(O.some(true))); + if (isProfileEmailAlreadyTaken(userProfile)) { + yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { + screen: ROUTES.CHECK_EMAIL_ALREADY_TAKEN, + params: { email: userProfile.email } + }); + } else { + yield* call(NavigationService.navigate, ROUTES.CHECK_EMAIL, { + screen: ROUTES.CHECK_EMAIL_NOT_VERIFIED + }); + } + // Wait for the user to press "Continue" button after having checked out + // his own email + yield* take(emailAcknowledged); + yield* call( + NavigationService.dispatchNavigationAction, + StackActions.popToTop() + ); + + // We get the latest profile from the store and return it + const maybeUpdatedProfile = yield* select(profileSelector); + if (pot.isSome(maybeUpdatedProfile)) { + userProfile = maybeUpdatedProfile.value; + } } - // Wait for the user to press "Continue" button after having checked out - // his own email - yield* take(emailAcknowledged); - yield* call( - NavigationService.dispatchNavigationAction, - StackActions.popToTop() - ); + return userProfile; } + return undefined; } From 6ec3db27233fe8037c06dd4dab330cb4ed862e8a Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:36:19 +0100 Subject: [PATCH 32/35] chore: Merge two email insert screens in a single one --- ts/navigation/OnboardingNavigator.tsx | 6 +- ts/navigation/ProfileNavigator.tsx | 4 +- .../NewOnboardingEmailInsertScreen.tsx | 284 ------------------ ...test.tsx => CduEmailInsertScreen.test.tsx} | 6 +- ...ertScreen.tsx => CduEmailInsertScreen.tsx} | 151 +++++++--- 5 files changed, 112 insertions(+), 339 deletions(-) delete mode 100644 ts/screens/onboarding/NewOnboardingEmailInsertScreen.tsx rename ts/screens/onboarding/__tests__/{NewOnboardingEmailInsertScreen.test.tsx => CduEmailInsertScreen.test.tsx} (92%) rename ts/screens/profile/{NewEmailInsertScreen.tsx => CduEmailInsertScreen.tsx} (69%) diff --git a/ts/navigation/OnboardingNavigator.tsx b/ts/navigation/OnboardingNavigator.tsx index 58ec177a8ae..1bce997f5a1 100644 --- a/ts/navigation/OnboardingNavigator.tsx +++ b/ts/navigation/OnboardingNavigator.tsx @@ -14,8 +14,8 @@ import ServicePreferenceCompleteScreen from "../screens/onboarding/ServicePrefer import { isGestureEnabled } from "../utils/navigation"; import MissingDevicePinScreen from "../screens/onboarding/biometric&securityChecks/MissingDevicePinScreen"; import MissingDeviceBiometricScreen from "../screens/onboarding/biometric&securityChecks/MissingDeviceBiometricScreen"; -import NewOnboardingEmailInsertScreen from "../screens/onboarding/NewOnboardingEmailInsertScreen"; import { isNewCduFlow } from "../config"; +import CduEmailInsertScreen from "../screens/profile/CduEmailInsertScreen"; import { OnboardingParamsList } from "./params/OnboardingParamsList"; import ROUTES from "./routes"; @@ -68,9 +68,7 @@ const navigator = () => ( { headerShown: false }} name={ROUTES.INSERT_EMAIL_SCREEN} - component={isNewCduFlow ? NewEmailInsertScreen : EmailInsertScreen} + component={isNewCduFlow ? CduEmailInsertScreen : EmailInsertScreen} /> refactor logic. Need to integrate the logic of this screen and the NewEmailInsertScreen - -/** - * A screen to allow user to insert an email address. - */ -const NewOnboardingEmailInsertScreen = () => { - const dispatch = useIODispatch(); - const { showModal } = useContext(LightModalContext); - - const viewRef = createRef(); - - const profile = useIOSelector(profileSelector); - const optionEmail = useIOSelector(profileEmailSelector); - const isProfileEmailAlreadyTaken = useIOSelector( - isProfileEmailAlreadyTakenSelector - ); - - const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); - - const prevUserProfile = usePrevious(profile); - - const isLoading = useMemo( - () => pot.isUpdating(profile) || pot.isLoading(profile), - [profile] - ); - - const acknowledgeEmail = useCallback( - () => dispatch(emailAcknowledged()), - [dispatch] - ); - - const updateEmail = useCallback( - (email: EmailString) => - dispatch( - profileUpsert.request({ - email - }) - ), - [dispatch] - ); - - const [areSameEmails, setAreSameEmails] = useState(false); - const [email, setEmail] = useState( - isProfileEmailAlreadyTaken ? optionEmail : O.some(EMPTY_EMAIL) - ); - - // this function return a boolean - const isValidEmail = useCallback( - () => - pipe( - email, - O.map(value => { - if ( - EMPTY_EMAIL === value || - !validator.isEmail(value) || - areSameEmails - ) { - return undefined; - } - return E.isRight(EmailString.decode(value)); - }), - O.toUndefined - ), - [areSameEmails, email] - ); - - useEffect(() => { - // this control is true only if the user try to insert a email, - // only if the continueOnPress function should be call we exeute this code - if (prevUserProfile && pot.isUpdating(prevUserProfile)) { - if (pot.isError(profile)) { - // the user is trying to enter an email already in use - if (profile.error.type === "PROFILE_EMAIL_IS_NOT_UNIQUE_ERROR") { - Alert.alert( - I18n.t("email.insert.alertTitle"), - I18n.t("email.insert.alertDescription"), - [ - { - text: I18n.t("email.insert.alertButton"), - style: "cancel" - } - ] - ); - } - } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { - // the email is correctly inserted - if (isEmailValidated) { - acknowledgeEmail(); - } else { - showModal(); - } - return; - } - } - }, [acknowledgeEmail, profile, prevUserProfile, isEmailValidated, showModal]); - - // the user try to update the email - const continueOnPress = () => { - Keyboard.dismiss(); - pipe( - email, - O.map(e => { - updateEmail(e as EmailString); - }) - ); - }; - - const renderFooterButtons = () => { - const continueButtonProps = { - disabled: !isValidEmail() && !isLoading, - onPress: continueOnPress, - title: I18n.t("global.buttons.continue"), - block: true, - primary: isValidEmail() - }; - - return ( - - ); - }; - - const handleOnChangeEmailText = (value: string) => { - setAreSameEmails(areStringsEqual(O.some(value), optionEmail, true)); - setEmail(value !== EMPTY_EMAIL ? O.some(value) : O.none); - }; - - return ( - - - - - -

- {I18n.t("email.newinsert.title")} -

- - {I18n.t("email.newinsert.subtitle")} - {isProfileEmailAlreadyTaken && ( - <> - - EMPTY_EMAIL) - ) - })} - /> - - )} - - - - EMPTY_EMAIL) - ) - : EMPTY_EMAIL, - onChangeText: handleOnChangeEmailText - }} - testID="TextField" - /> - {areSameEmails && ( - - - - - - {I18n.t("email.newinsert.alert.description")} - - - )} - -
-
- {withKeyboard(renderFooterButtons())} -
-
-
- ); -}; - -export default NewOnboardingEmailInsertScreen; diff --git a/ts/screens/onboarding/__tests__/NewOnboardingEmailInsertScreen.test.tsx b/ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx similarity index 92% rename from ts/screens/onboarding/__tests__/NewOnboardingEmailInsertScreen.test.tsx rename to ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx index 8a29d833430..8c0ef321eac 100644 --- a/ts/screens/onboarding/__tests__/NewOnboardingEmailInsertScreen.test.tsx +++ b/ts/screens/onboarding/__tests__/CduEmailInsertScreen.test.tsx @@ -5,9 +5,9 @@ import { applicationChangeState } from "../../../store/actions/application"; import { appReducer } from "../../../store/reducers"; import I18n from "../../../i18n"; import { renderScreenWithNavigationStoreContext } from "../../../utils/testWrapper"; -import NewOnboardingEmailInsertScreen from "../NewOnboardingEmailInsertScreen"; +import CduEmailInsertScreen from "../../profile/CduEmailInsertScreen"; -describe("NewOnboardingEmailInsertScreen", async () => { +describe("CduEmailInsertScreen", async () => { it("the components into the page should be render correctly", () => { const component = renderComponent(); expect(component).toBeDefined(); @@ -61,7 +61,7 @@ const renderComponent = () => { const store = createStore(appReducer, globalState as any); return renderScreenWithNavigationStoreContext( - NewOnboardingEmailInsertScreen, + CduEmailInsertScreen, ROUTES.ONBOARDING_INSERT_EMAIL_SCREEN, {}, store diff --git a/ts/screens/profile/NewEmailInsertScreen.tsx b/ts/screens/profile/CduEmailInsertScreen.tsx similarity index 69% rename from ts/screens/profile/NewEmailInsertScreen.tsx rename to ts/screens/profile/CduEmailInsertScreen.tsx index 4c44189348c..f7b74364ac7 100644 --- a/ts/screens/profile/NewEmailInsertScreen.tsx +++ b/ts/screens/profile/CduEmailInsertScreen.tsx @@ -13,7 +13,8 @@ import React, { useEffect, useMemo, useState, - useContext + useContext, + createRef } from "react"; import validator from "validator"; import { Alert, Keyboard, SafeAreaView, StyleSheet, View } from "react-native"; @@ -21,7 +22,10 @@ import { IOColors, Icon, LabelSmall, - VSpacer + VSpacer, + Alert as AlertComponent, + FooterWithButtons, + IOToast } from "@pagopa/io-app-design-system"; import { H1 } from "../../components/core/typography/H1"; import { LabelledItem } from "../../components/LabelledItem"; @@ -29,26 +33,27 @@ import LoadingSpinnerOverlay from "../../components/LoadingSpinnerOverlay"; import BaseScreenComponent, { ContextualHelpPropsMarkdown } from "../../components/screens/BaseScreenComponent"; -import FooterWithButtons from "../../components/ui/FooterWithButtons"; import I18n from "../../i18n"; import { IOStackNavigationRouteProps } from "../../navigation/params/AppParamsList"; import { ProfileParamsList } from "../../navigation/params/ProfileParamsList"; import { profileUpsert } from "../../store/actions/profile"; import { useIODispatch, useIOSelector } from "../../store/hooks"; import { + isProfileEmailAlreadyTakenSelector, isProfileEmailValidatedSelector, + isProfileFirstOnBoardingSelector, profileEmailSelector, profileSelector } from "../../store/reducers/profile"; import { usePrevious } from "../../utils/hooks/usePrevious"; import { withKeyboard } from "../../utils/keyboard"; import { areStringsEqual } from "../../utils/options"; -import { showToast } from "../../utils/showToast"; import { Body } from "../../components/core/typography/Body"; import { IOStyles } from "../../components/core/variables/IOStyles"; import { LightModalContext } from "../../components/ui/LightModal"; import NewRemindEmailValidationOverlay from "../../components/NewRemindEmailValidationOverlay"; import { emailValidationSelector } from "../../store/reducers/emailValidation"; +import { emailAcknowledged } from "../../store/actions/onboarding"; type Props = IOStackNavigationRouteProps< ProfileParamsList, @@ -69,12 +74,11 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = { body: "email.insert.help.content" }; -// FIXME -> refactor logic. Need to integrate the logic of this screen and the NewOnboardingEmailInsertScreen - /** * A screen to allow user to insert an email address. */ -const NewEmailInsertScreen = (props: Props) => { +const CduEmailInsertScreen = (props: Props) => { + const viewRef = createRef(); const { showModal } = useContext(LightModalContext); const dispatch = useIODispatch(); @@ -82,6 +86,10 @@ const NewEmailInsertScreen = (props: Props) => { const profile = useIOSelector(profileSelector); const optionEmail = useIOSelector(profileEmailSelector); const isEmailValidated = useIOSelector(isProfileEmailValidatedSelector); + const isFirstOnboarding = useIOSelector(isProfileFirstOnBoardingSelector); + const isProfileEmailAlreadyTaken = useIOSelector( + isProfileEmailAlreadyTakenSelector + ); const acknowledgeOnEmailValidated = useIOSelector( emailValidationSelector @@ -94,6 +102,11 @@ const NewEmailInsertScreen = (props: Props) => { [profile] ); + const acknowledgeEmail = useCallback( + () => dispatch(emailAcknowledged()), + [dispatch] + ); + const updateEmail = useCallback( (email: EmailString) => dispatch( @@ -104,8 +117,11 @@ const NewEmailInsertScreen = (props: Props) => { [dispatch] ); + const getEmail = (email: O.Option) => + !isProfileEmailAlreadyTaken ? email : O.some(EMPTY_EMAIL); + const [areSameEmails, setAreSameEmails] = useState(false); - const [email, setEmail] = useState(optionEmail ?? O.some(EMPTY_EMAIL)); + const [email, setEmail] = useState(getEmail(optionEmail)); /** validate email returning three possible values: * - _true_, if email is valid. @@ -135,7 +151,6 @@ const NewEmailInsertScreen = (props: Props) => { const continueOnPress = () => { Keyboard.dismiss(); - pipe( email, O.map(e => { @@ -148,7 +163,8 @@ const NewEmailInsertScreen = (props: Props) => { const continueButtonProps = { disabled: !isValidEmail() && !isLoading, onPress: continueOnPress, - title: I18n.t("global.buttons.continue"), + label: I18n.t("global.buttons.continue"), + accessibilityLabel: I18n.t("global.buttons.continue"), block: true, primary: isValidEmail() }; @@ -156,7 +172,10 @@ const NewEmailInsertScreen = (props: Props) => { return ( ); }; @@ -176,16 +195,6 @@ const NewEmailInsertScreen = (props: Props) => { setEmail(O.some(EMPTY_EMAIL)); }, []); - useEffect(() => { - if (prevUserProfile) { - const isPrevCurrentSameState = prevUserProfile.kind === profile.kind; - // do nothing if prev profile is in the same state of the current - if (isPrevCurrentSameState) { - return; - } - } - }, [prevUserProfile, profile]); - // If we navigate to this screen with acknowledgeOnEmailValidated set to false, // we show the modal to remind the user to validate the email. // This is used during the check of the email at startup. @@ -194,10 +203,13 @@ const NewEmailInsertScreen = (props: Props) => { O.isSome(acknowledgeOnEmailValidated) && acknowledgeOnEmailValidated.value === false ) { - showModal(); + showModal( + + ); } - }, [acknowledgeOnEmailValidated, showModal]); + }, [acknowledgeOnEmailValidated, isFirstOnboarding, showModal]); + // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { if (prevUserProfile && pot.isUpdating(prevUserProfile)) { if (pot.isError(profile)) { @@ -214,53 +226,99 @@ const NewEmailInsertScreen = (props: Props) => { ] ); } else { - showToast(I18n.t("email.edit.upsert_ko"), "danger"); + IOToast.error(I18n.t("email.edit.upsert_ko")); } - // display a toast with error } else if (pot.isSome(profile) && !pot.isUpdating(profile)) { // the email is correctly inserted if (isEmailValidated) { - handleGoBack(); + if (!isFirstOnboarding) { + handleGoBack(); + } } else { - showModal(); + showModal( + + ); } return; } } - }, [handleGoBack, isEmailValidated, prevUserProfile, profile, showModal]); + }, [ + acknowledgeEmail, + handleGoBack, + isEmailValidated, + isFirstOnboarding, + prevUserProfile, + profile, + showModal + ]); return ( - + -

- {I18n.t("email.edit.title")} +

+ {isFirstOnboarding + ? I18n.t("email.newinsert.title") + : I18n.t("email.edit.title")}

- {isEmailValidated - ? I18n.t("email.edit.validated") - : I18n.t("email.edit.subtitle")} - - {` ${pipe( - optionEmail, - O.getOrElse(() => "") - )}`} - + {isFirstOnboarding ? ( + I18n.t("email.newinsert.subtitle") + ) : ( + <> + {I18n.t("email.edit.subtitle")} + + {` ${pipe( + optionEmail, + O.getOrElse(() => "") + )}`} + + + )} + {isProfileEmailAlreadyTaken && isFirstOnboarding && ( + <> + + EMPTY_EMAIL) + ) + })} + /> + + )}
{ onSubmitEditing: continueOnPress, autoCapitalize: "none", keyboardType: "email-address", - value: pipe( - email, + defaultValue: pipe( + getEmail(email), O.getOrElse(() => EMPTY_EMAIL) ), onChangeText: handleOnChangeEmailText }} + testID="TextField" /> {areSameEmails && ( { ); }; -export default NewEmailInsertScreen; +export default CduEmailInsertScreen; From 404f6412dea7c6246bb757d01d7b70d09a196156 Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:00:15 +0100 Subject: [PATCH 33/35] chore: wrap user data processing api with fl --- ts/sagas/user/userDataProcessing.ts | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ts/sagas/user/userDataProcessing.ts b/ts/sagas/user/userDataProcessing.ts index e8c3c7b4826..4411d8039ba 100644 --- a/ts/sagas/user/userDataProcessing.ts +++ b/ts/sagas/user/userDataProcessing.ts @@ -12,6 +12,7 @@ import { } from "../../store/actions/userDataProcessing"; import { SagaCallReturnType } from "../../types/utils"; import { convertUnknownToError, getError } from "../../utils/errors"; +import { withRefreshApiCall } from "../../features/fastLogin/saga/utils"; /** * The following logic: @@ -26,10 +27,14 @@ export function* loadUserDataProcessingSaga( ): SagaIterator { const choice = action.payload; try { - const response: SagaCallReturnType = - yield* call(getUserDataProcessingRequest, { + const response = (yield* call( + withRefreshApiCall, + getUserDataProcessingRequest({ choice - }); + }), + action + )) as unknown as SagaCallReturnType; + if (E.isRight(response)) { if (response.right.status === 404 || response.right.status === 200) { yield* put( @@ -65,10 +70,13 @@ export function* upsertUserDataProcessingSaga( ): SagaIterator { const choice = action.payload; try { - const response: SagaCallReturnType = - yield* call(postUserDataProcessingRequest, { + const response = (yield* call( + withRefreshApiCall, + postUserDataProcessingRequest({ body: { choice } - }); + }), + action + )) as unknown as SagaCallReturnType; if (E.isRight(response) && response.right.status === 200) { yield* put(upsertUserDataProcessing.success(response.right.value)); @@ -98,10 +106,14 @@ export function* deleteUserDataProcessingSaga( const choice = action.payload; try { - const response: SagaCallReturnType = - yield* call(deleteUserDataProcessingRequest, { + const response = (yield* call( + withRefreshApiCall, + deleteUserDataProcessingRequest({ choice - }); + }), + action + )) as unknown as SagaCallReturnType; + if (E.isRight(response)) { if (response.right.status === 202) { yield* put( From db20fe39ebe20c4ffad9fd4a77b364fed678940d Mon Sep 17 00:00:00 2001 From: Fabio Bombardi <16268789+shadowsheep1@users.noreply.github.com> Date: Fri, 1 Dec 2023 13:26:20 +0100 Subject: [PATCH 34/35] chore: wrap user data processing api with fl --- .../user/__test__/userDataProcessing.test.ts | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/ts/sagas/user/__test__/userDataProcessing.test.ts b/ts/sagas/user/__test__/userDataProcessing.test.ts index 73ea510baf1..2c735cb9bdb 100644 --- a/ts/sagas/user/__test__/userDataProcessing.test.ts +++ b/ts/sagas/user/__test__/userDataProcessing.test.ts @@ -14,6 +14,7 @@ import { loadUserDataProcessingSaga, upsertUserDataProcessingSaga } from "../userDataProcessing"; +import { withRefreshApiCall } from "../../../features/fastLogin/saga/utils"; describe("loadUserDataProcessingSaga", () => { const getUserDataProcessingRequest = jest.fn(); @@ -30,9 +31,13 @@ describe("loadUserDataProcessingSaga", () => { loadAction ) .next() - .call(getUserDataProcessingRequest, { - choice: loadAction.payload - }) + .call( + withRefreshApiCall, + getUserDataProcessingRequest({ + choice: loadAction.payload + }), + loadAction + ) .next(get404Response) .put( loadUserDataProcessing.success({ @@ -58,9 +63,13 @@ describe("loadUserDataProcessingSaga", () => { loadAction ) .next() - .call(getUserDataProcessingRequest, { - choice: loadAction.payload - }) + .call( + withRefreshApiCall, + getUserDataProcessingRequest({ + choice: loadAction.payload + }), + loadAction + ) .next(get200Response) .put( loadUserDataProcessing.success({ @@ -83,9 +92,13 @@ describe("loadUserDataProcessingSaga", () => { loadAction ) .next() - .call(getUserDataProcessingRequest, { - choice: loadAction.payload - }) + .call( + withRefreshApiCall, + getUserDataProcessingRequest({ + choice: loadAction.payload + }), + loadAction + ) .next(get500Response) .put( loadUserDataProcessing.failure({ @@ -119,9 +132,13 @@ describe("upsertUserDataProcessingSaga", () => { requestAction ) .next() - .call(postUserDataProcessingRequest, { - body: { choice: requestAction.payload } - }) + .call( + withRefreshApiCall, + postUserDataProcessingRequest({ + body: { choice: requestAction.payload } + }), + requestAction + ) .next(post200Response) .put(upsertUserDataProcessing.success(mokedNewStatus)) .next() @@ -141,9 +158,13 @@ describe("upsertUserDataProcessingSaga", () => { requestAction ) .next() - .call(postUserDataProcessingRequest, { - body: { choice: requestAction.payload } - }) + .call( + withRefreshApiCall, + postUserDataProcessingRequest({ + body: { choice: requestAction.payload } + }), + requestAction + ) .next(get500Response) .put( upsertUserDataProcessing.failure({ @@ -177,9 +198,13 @@ describe("deleteUserDataProcessingSaga", () => { requestAction ) .next() - .call(deleteUserDataProcessingRequest, { - choice: requestAction.payload - }) + .call( + withRefreshApiCall, + deleteUserDataProcessingRequest({ + choice: requestAction.payload + }), + requestAction + ) .next(post202Response) .put(deleteUserDataProcessing.success({ choice: requestAction.payload })) .next() @@ -201,9 +226,13 @@ describe("deleteUserDataProcessingSaga", () => { requestActionDownload ) .next() - .call(deleteUserDataProcessingRequest, { - choice - }) + .call( + withRefreshApiCall, + deleteUserDataProcessingRequest({ + choice + }), + requestActionDownload + ) .next(get409Response) .put( deleteUserDataProcessing.failure({ From 5c5a0bfba1d092a355867e3443abe7608fc2d315 Mon Sep 17 00:00:00 2001 From: Alice Di Rico <83651704+Ladirico@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:21:40 +0100 Subject: [PATCH 35/35] fix style on android --- ts/screens/profile/CduEmailInsertScreen.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/ts/screens/profile/CduEmailInsertScreen.tsx b/ts/screens/profile/CduEmailInsertScreen.tsx index f7b74364ac7..074c5898288 100644 --- a/ts/screens/profile/CduEmailInsertScreen.tsx +++ b/ts/screens/profile/CduEmailInsertScreen.tsx @@ -341,9 +341,6 @@ const CduEmailInsertScreen = (props: Props) => {