From baefebfe78e018b3a5b83837a4f4d8ef4383c2d7 Mon Sep 17 00:00:00 2001 From: Audrey Lebret Date: Mon, 5 Sep 2022 11:35:28 +0200 Subject: [PATCH] feat(notifs): centre de notifs pour les articles (#1418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(notifs): création de la page * feat(notifs): activer/désactiver notif articles * feat(notifs): accessibilité * feat(notif): retours PR * feat(notifs): retours PR * feat(notifs): retours PR --- front/src/components/menu/menu.component.tsx | 7 ++ .../notificationToggle.component.tsx | 116 ++++++++++++++++++ front/src/constants/Labels.ts | 11 ++ front/src/constants/storageKeys.constants.ts | 2 + front/src/navigation/BottomTabNavigator.tsx | 17 +++ front/src/screens/index.ts | 2 + .../notificationsCenter.component.tsx | 66 ++++++++++ front/src/types.tsx | 1 + front/src/utils/notification.util.test.ts | 105 +++++++++++++++- front/src/utils/notification.util.ts | 93 +++++++++----- front/src/utils/tracker.util.ts | 1 + 11 files changed, 386 insertions(+), 35 deletions(-) create mode 100644 front/src/components/notification/notificationToggle.component.tsx create mode 100644 front/src/screens/notificationsCenter/notificationsCenter.component.tsx diff --git a/front/src/components/menu/menu.component.tsx b/front/src/components/menu/menu.component.tsx index b471c93d9..3797ef4f1 100644 --- a/front/src/components/menu/menu.component.tsx +++ b/front/src/components/menu/menu.component.tsx @@ -80,6 +80,13 @@ const Menu: React.FC = ({ showMenu, setShowMenu }) => { }, title: Labels.timeline.library.nom, }, + { + icon: IcomoonIcons.notification, + onPress: () => { + void RootNavigation.navigate("notificationsCenter"); + }, + title: Labels.menu.notificationsCenter, + }, { icon: IcomoonIcons.email, onPress: () => { diff --git a/front/src/components/notification/notificationToggle.component.tsx b/front/src/components/notification/notificationToggle.component.tsx new file mode 100644 index 000000000..b51e8b2c0 --- /dev/null +++ b/front/src/components/notification/notificationToggle.component.tsx @@ -0,0 +1,116 @@ +import type { FC } from "react"; +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; +import { StyleSheet, Text, View } from "react-native"; + +import { Toggle } from "../../components/baseComponents"; +import { Labels, StorageKeysConstants } from "../../constants"; +import { Colors, FontStyle, FontWeight, Margins } from "../../styles"; +import { StorageUtils } from "../../utils"; +import * as NotificationUtils from "../../utils/notification.util"; +import { NotificationType } from "../../utils/notification.util"; + +interface Props { + title: string; + description: string; + type: NotificationType; +} + +const NotificationToggle: FC = ({ title, description, type }) => { + const [isToggleOn, setToggleOn] = useState(false); + + useEffect(() => { + void initToggle(); + }, []); + + const initToggle = async () => { + // Value in storage + const storageValue = (await StorageUtils.getObjectValue( + StorageKeysConstants.notifToggleArticles + )) as boolean; + setToggleOn(storageValue); + }; + + const onTouchToggle = useCallback(async () => { + const newValue = !isToggleOn; + setToggleOn(newValue); + await StorageUtils.storeObjectValue( + StorageKeysConstants.notifToggleArticles, + newValue + ); + + if (newValue) { + if (type === NotificationType.articles) + void NotificationUtils.updateArticlesNotification(); + } else await NotificationUtils.cancelAllNotificationsByType(type); + }, [isToggleOn, type]); + + return ( + + + + {title} + + {description} + + + + {Labels.buttons.no} + + + + + + {Labels.buttons.yes} + + + + ); +}; + +const styles = StyleSheet.create({ + itemTextBloc: { + flex: 2, + }, + itemTextDescr: { + color: Colors.grey, + fontStyle: FontStyle.italic, + marginTop: Margins.smaller, + }, + itemTextTitle: { + fontWeight: FontWeight.bold, + }, + itemToggle: { + marginHorizontal: Margins.smaller, + }, + itemToggleBloc: { + flex: 1, + flexDirection: "row", + justifyContent: "center", + }, + itemToggleText: { + color: Colors.secondaryGreenDark, + }, + mainContent: { + flexDirection: "row", + marginVertical: Margins.larger, + }, +}); + +export default NotificationToggle; diff --git a/front/src/constants/Labels.ts b/front/src/constants/Labels.ts index 9af0a161c..5937757a1 100644 --- a/front/src/constants/Labels.ts +++ b/front/src/constants/Labels.ts @@ -567,6 +567,7 @@ export default { moodboard: "Suivi d'humeur", myFavorites: "Mes favoris", myProfil: "Mon profil", + notificationsCenter: "Centre de notification", title: "Menu", }, moodboard: { @@ -593,6 +594,16 @@ export default { notification: { openTheApp: "Ouverture de l'app", }, + notificationsCenter: { + article: { + decription: + "Vous précisant qu’il y a tel article non lu sur votre étape.", + title: "Notifications des articles non lus", + }, + description: + "Ici vous pouvez paramétrer les notifications que vous souhaitez recevoir en les activant/désactivant :", + title: "Paramètres de notifications", + }, onboarding: { screenNumber: "Écran n°", slidesText: [ diff --git a/front/src/constants/storageKeys.constants.ts b/front/src/constants/storageKeys.constants.ts index 21a30a4a4..d1e446bd7 100644 --- a/front/src/constants/storageKeys.constants.ts +++ b/front/src/constants/storageKeys.constants.ts @@ -18,6 +18,7 @@ export const cartoSavedCoordinates = "@cartoSavedCoordinates"; export const cartoIsFirstLaunch = "@cartoIsFirstLaunch"; export const notifIdNextStep = "@notifIdNextStep"; export const notifIdsEvents = "@notifIdsEvents"; +export const notifToggleArticles = "@notifToggleArticles"; export const eventsCalcFromBirthday = "@eventsCalcFromBirthday"; export const forceToScheduleEventsNotif = "@forceToScheduleEventsNotif"; export const osCalendarId = "@osCalendarId"; @@ -53,6 +54,7 @@ export const allStorageKeys = [ cartoIsFirstLaunch, notifIdNextStep, notifIdsEvents, + notifToggleArticles, eventsCalcFromBirthday, forceToScheduleEventsNotif, osCalendarId, diff --git a/front/src/navigation/BottomTabNavigator.tsx b/front/src/navigation/BottomTabNavigator.tsx index 8c1d05687..5cd3adf49 100644 --- a/front/src/navigation/BottomTabNavigator.tsx +++ b/front/src/navigation/BottomTabNavigator.tsx @@ -19,6 +19,7 @@ import { TabHomeScreen, TabSearchScreen, } from "../screens"; +import NotificationsCenter from "../screens/notificationsCenter/notificationsCenter.component"; import { Colors } from "../styles"; import type { BottomTabParamList, @@ -135,6 +136,10 @@ const TabHomeNavigator: FC = () => ( + ); @@ -150,6 +155,10 @@ const TabCalendarNavigator: FC = () => ( + ); @@ -162,6 +171,10 @@ const TabEpdsNavigator: FC = () => ( options={{}} /> + ); @@ -178,6 +191,10 @@ const TabSearchNavigator: FC = () => ( component={AroundMeMapAndList} /> + ); diff --git a/front/src/screens/index.ts b/front/src/screens/index.ts index 72e7cbb96..6e44a026a 100644 --- a/front/src/screens/index.ts +++ b/front/src/screens/index.ts @@ -11,6 +11,7 @@ import Parentheque from "./home/parentheque.component"; import TabHomeScreen from "./home/tabHomeScreen.component"; import LoadingScreen from "./loading/loading.component"; import Moodboard from "./moodboard/moodboard.component"; +import NotificationsCenter from "./notificationsCenter/notificationsCenter.component"; import Onboarding from "./onboardingAndProfile/onboarding.component"; import Profile from "./onboardingAndProfile/profile.component"; import AroundMeMapAndList from "./search/aroundMeMapAndList.component"; @@ -27,6 +28,7 @@ export { EventDetails, LoadingScreen, Moodboard, + NotificationsCenter, Onboarding, Parentheque, Profile, diff --git a/front/src/screens/notificationsCenter/notificationsCenter.component.tsx b/front/src/screens/notificationsCenter/notificationsCenter.component.tsx new file mode 100644 index 000000000..9774b8ac6 --- /dev/null +++ b/front/src/screens/notificationsCenter/notificationsCenter.component.tsx @@ -0,0 +1,66 @@ +import type { StackNavigationProp } from "@react-navigation/stack"; +import type { FC } from "react"; +import * as React from "react"; +import { useCallback, useState } from "react"; +import { ScrollView, StyleSheet, View } from "react-native"; + +import { BackButton, TitleH1 } from "../../components/baseComponents"; +import NotificationToggle from "../../components/notification/notificationToggle.component"; +import TrackerHandler from "../../components/tracker/trackerHandler.component"; +import { Labels } from "../../constants"; +import { Colors, Paddings } from "../../styles"; +import type { RootStackParamList } from "../../types"; +import { TrackerUtils } from "../../utils"; +import { NotificationType } from "../../utils/notification.util"; + +interface Props { + navigation: StackNavigationProp; +} + +const NotificationsCenter: FC = ({ navigation }) => { + const [trackerAction, setTrackerAction] = useState(""); + + const goBack = useCallback(() => { + setTrackerAction(Labels.buttons.cancel); + navigation.goBack(); + }, [navigation]); + + return ( + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + flexStart: { + alignItems: "flex-start", + }, + header: { + padding: Paddings.default, + paddingTop: Paddings.default, + }, + mainContainer: { + backgroundColor: Colors.white, + }, +}); + +export default NotificationsCenter; diff --git a/front/src/types.tsx b/front/src/types.tsx index bde7b3597..f6b9301b7 100644 --- a/front/src/types.tsx +++ b/front/src/types.tsx @@ -32,6 +32,7 @@ export type TabHomeParamList = { parentheque: { documents?: Document[] }; article: { id: number; step?: Step }; epdsSurvey: undefined; + notificationsCenter: undefined; }; export type TabCalendarParamList = { diff --git a/front/src/utils/notification.util.test.ts b/front/src/utils/notification.util.test.ts index 60bcba35b..89388816a 100644 --- a/front/src/utils/notification.util.test.ts +++ b/front/src/utils/notification.util.test.ts @@ -6,8 +6,13 @@ import { NotificationConstants, StorageKeysConstants, } from "../constants"; +import type { Step } from "../types"; import { NotificationUtils, StorageUtils } from "."; -import { NotificationType } from "./notification.util"; +import { + getNotificationTrigger, + NotificationType, + saveStepForCongratNotifScheduled, +} from "./notification.util"; describe("Notification utils", () => { describe("Build Articles Notification Content", () => { @@ -95,4 +100,102 @@ describe("Notification utils", () => { expect(true).toEqual(expected); }); }); + + describe("getNotificationTrigger", () => { + afterEach(() => { + void AsyncStorage.clear(); + }); + + it("getNotificationTrigger with 0 article and no trigger", async () => { + const result = await getNotificationTrigger(0, null); + const expected = NotificationConstants.MIN_TRIGGER; + + expect(result).toEqual(expected); + }); + + it("getNotificationTrigger with articles", async () => { + const notifTrigger = { seconds: 20 }; + const result = await getNotificationTrigger(5, notifTrigger); + const expected = { seconds: 20 }; + + expect(result).toEqual(expected); + }); + + it("getNotificationTrigger with articles and no trigger", async () => { + const result = await getNotificationTrigger(5, null); + const expected = new Date( + addDays( + new Date(), + NotificationConstants.NUMBER_OF_DAYS_NOTIF_ARTICLES_REMINDER + ).setHours(NotificationConstants.ARTICLES_NOTIF_TRIGGER_HOUR, 0, 0, 0) + ); + + expect(result).toEqual(expected); + await StorageUtils.getObjectValue( + StorageKeysConstants.triggerForArticlesNotification + ).then((data) => { + expect(data).toEqual(expected.toJSON()); + }); + }); + }); + + describe("saveStepForCongratNotifScheduled", () => { + afterEach(() => { + void AsyncStorage.clear(); + }); + + it("saveStepForCongratNotifScheduled with no article and currentStep is null", async () => { + await saveStepForCongratNotifScheduled(0, null, null); + await StorageUtils.getObjectValue( + StorageKeysConstants.stepsAlreadyCongratulatedForArticles + ).then((data) => { + expect(data).toBeNull(); + }); + }); + + it("saveStepForCongratNotifScheduled with no article, currentStep and no stepsAlreadyCongratulatedForArticles", async () => { + const currentStep: Step = { + active: null, + debut: 0, + description: null, + fin: 90, + id: 6, + nom: "De 0 à 3 mois", + ordre: 6, + }; + const expected = ["6"]; + + await saveStepForCongratNotifScheduled(0, currentStep, null); + await StorageUtils.getObjectValue( + StorageKeysConstants.stepsAlreadyCongratulatedForArticles + ).then((data) => { + expect(data).toEqual(expected); + }); + }); + + it("saveStepForCongratNotifScheduled with no article, currentStep and stepsAlreadyCongratulatedForArticles", async () => { + const currentStep: Step = { + active: null, + debut: 0, + description: null, + fin: 90, + id: 6, + nom: "De 0 à 3 mois", + ordre: 6, + }; + const stepsAlreadyCongratulatedForArticles = ["2", "5"]; + const expected = stepsAlreadyCongratulatedForArticles.length + 1; + + await saveStepForCongratNotifScheduled( + 0, + currentStep, + stepsAlreadyCongratulatedForArticles + ); + await StorageUtils.getObjectValue( + StorageKeysConstants.stepsAlreadyCongratulatedForArticles + ).then((data) => { + expect(data).toEqual(expected); + }); + }); + }); }); diff --git a/front/src/utils/notification.util.ts b/front/src/utils/notification.util.ts index 004096aa6..0784686dc 100644 --- a/front/src/utils/notification.util.ts +++ b/front/src/utils/notification.util.ts @@ -368,43 +368,68 @@ export const updateArticlesNotification = async (): Promise => { await scheduleArticlesNotification(trigger); }; +export const getNotificationTrigger = async ( + nbArticlesToRead: number, + notifTrigger: NotificationTriggerInput | undefined +): Promise => + nbArticlesToRead > 0 + ? notifTrigger ?? getNewTriggerForArticlesNotification() + : NotificationConstants.MIN_TRIGGER; + +// Enregistre les étapes pour lesquelles la notification de félicitations (articles tous lus) a déjà été programmée +export const saveStepForCongratNotifScheduled = async ( + nbArticlesToRead: number, + currentStep: Step | null, + stepsAlreadyCongratulatedForArticles: string[] | null +) => { + if (nbArticlesToRead === 0 && currentStep) { + const currentStepId = currentStep.id.toString(); + const newValue = stepsAlreadyCongratulatedForArticles + ? stepsAlreadyCongratulatedForArticles.push(currentStepId) + : [currentStepId]; + await StorageUtils.storeObjectValue( + StorageKeysConstants.stepsAlreadyCongratulatedForArticles, + newValue + ); + } +}; + export const scheduleArticlesNotification = async ( notifTrigger?: NotificationTriggerInput ): Promise => { - const nbArticlesToRead: number = await countCurrentStepArticlesNotRead(); - if (nbArticlesToRead >= 0) { - const trigger: NotificationTriggerInput = - nbArticlesToRead > 0 - ? notifTrigger ?? (await getNewTriggerForArticlesNotification()) - : NotificationConstants.MIN_TRIGGER; - const content = await buildArticlesNotificationContent(nbArticlesToRead); - - const stepsAlreadyCongratulatedForArticles = - ((await StorageUtils.getObjectValue( - StorageKeysConstants.stepsAlreadyCongratulatedForArticles - )) as string[] | undefined) ?? null; - const currentStep = (await StorageUtils.getObjectValue( - StorageKeysConstants.currentStep - )) as Step | null; - - if (content) { - await cancelAllNotificationsByType(NotificationType.articles); - const hasBeenAlreadyNotified = - stepsAlreadyCongratulatedForArticles?.includes( - currentStep ? currentStep.id.toString() : "" - ); - if (!hasBeenAlreadyNotified) { - await sendNotificationReminder(content, trigger); - - // Enregistre les étapes pour lesquelles la notification de félicitations (articles tous lus) a déjà été programmée - if (nbArticlesToRead === 0 && currentStep) { - const currentStepId = currentStep.id.toString(); - const newValue = stepsAlreadyCongratulatedForArticles - ? stepsAlreadyCongratulatedForArticles.push(currentStepId) - : [currentStepId]; - await StorageUtils.storeObjectValue( - StorageKeysConstants.stepsAlreadyCongratulatedForArticles, - newValue + const isToggleActive = (await StorageUtils.getObjectValue( + StorageKeysConstants.notifToggleArticles + )) as boolean; + + if (isToggleActive) { + const nbArticlesToRead: number = await countCurrentStepArticlesNotRead(); + if (nbArticlesToRead >= 0) { + const trigger: NotificationTriggerInput = await getNotificationTrigger( + nbArticlesToRead, + notifTrigger + ); + const content = await buildArticlesNotificationContent(nbArticlesToRead); + + const stepsAlreadyCongratulatedForArticles = + ((await StorageUtils.getObjectValue( + StorageKeysConstants.stepsAlreadyCongratulatedForArticles + )) as string[] | undefined) ?? null; + const currentStep = (await StorageUtils.getObjectValue( + StorageKeysConstants.currentStep + )) as Step | null; + + if (content) { + await cancelAllNotificationsByType(NotificationType.articles); + const hasBeenAlreadyNotified = + stepsAlreadyCongratulatedForArticles?.includes( + currentStep ? currentStep.id.toString() : "" + ); + if (!hasBeenAlreadyNotified) { + await sendNotificationReminder(content, trigger); + await saveStepForCongratNotifScheduled( + nbArticlesToRead, + currentStep, + stepsAlreadyCongratulatedForArticles ); } } diff --git a/front/src/utils/tracker.util.ts b/front/src/utils/tracker.util.ts index bf0533397..6a7915178 100644 --- a/front/src/utils/tracker.util.ts +++ b/front/src/utils/tracker.util.ts @@ -55,6 +55,7 @@ export enum TrackingEvent { FILTER_ARTICLES = "Filtre (Articles)", RECHERCHER = "Rechercher", MOODBOARD = "Moodboard", + NOTIFICATIONS_CENTER = "Centre de notifications", SETTINGS = "Settings", NOTIFICATIONS_DISABLED = "Notifications désactivées", RESSOURCES = "Ressources",