feat: ✨ Managing notifications from user profile
jump to
@@ -46,6 +46,9 @@ );
if (!emailTemplate) throw new Error(`No locale found for ${notifType} in ${lang}`); + strapi.log.debug( + `Send email notification of type ${notifType} to ${to} (lang: ${lang})` + ); await strapi.plugins["email"].services.email.send({ to, ...emailTemplate,
@@ -19,13 +19,14 @@ },
} ); const { user, event, payload = {} } = notification; - await strapi - .service("api::email.email") - .sendEmailNotif(user.email, notification.type, user.lang, { - user, - event, - ...(payload as object), - }); + if (user.notificationEnabled) + await strapi + .service("api::email.email") + .sendEmailNotif(user.email, notification.type, user.lang, { + user, + event, + ...(payload as object), + }); } }, };
@@ -100,13 +100,14 @@ "default": false
}, "lang": { "type": "enumeration", - "enum": [ - "fr", - "en" - ], + "enum": ["fr", "en"], "default": "fr" }, "newsletterConsent": { + "type": "boolean", + "default": false + }, + "notificationEnabled": { "type": "boolean", "default": true },
@@ -772,7 +772,8 @@ lastName: Attribute.String;
onboardingUser: Attribute.Boolean & Attribute.DefaultTo<false>; onboardingCreator: Attribute.Boolean & Attribute.DefaultTo<false>; lang: Attribute.Enumeration<['fr', 'en']> & Attribute.DefaultTo<'fr'>; - newsletterConsent: Attribute.Boolean & Attribute.DefaultTo<true>; + newsletterConsent: Attribute.Boolean & Attribute.DefaultTo<false>; + notificationEnabled: Attribute.Boolean & Attribute.DefaultTo<true>; notifications: Attribute.Relation< 'plugin::users-permissions.user', 'oneToMany',
@@ -0,0 +1,21 @@
+import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; + +const ContentSwitch = ({ + isEditing, + checked, + onChange, + trueLabel, + falseLabel, + t, +}) => { + return isEditing ? ( + <Switch checked={checked} onChange={onChange} /> + ) : ( + <Typography variant="h6"> + {checked ? t(trueLabel) : t(falseLabel)} + </Typography> + ); +}; + +export default ContentSwitch;
@@ -0,0 +1,51 @@
+import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import {useTranslation} from 'react-i18next'; +import ContentSwitch from './ContentSwitch'; + +interface Props { + toggleNotification: () => void; + toggleNewsletter: () => void; + notificationChecked: boolean; + newsletterChecked: boolean; + isEditing: boolean; +} + +const ManagingNotificationsField = ({ + isEditing, + toggleNotification, + toggleNewsletter, + notificationChecked, + newsletterChecked, +}: Props) => { + const {t} = useTranslation(); + + return ( + <Box padding={2}> + <Box display="flex" alignItems="center" justifyContent="space-between"> + <Typography variant="h6">{t('profile.notification.label')}</Typography> + <ContentSwitch + isEditing={isEditing} + checked={notificationChecked} + onChange={toggleNotification} + trueLabel="profile.notification.value.yes" + falseLabel="profile.notification.value.no" + t={t} + /> + </Box> + <Box display="flex" alignItems="center" justifyContent="space-between"> + <Typography variant="h6">{t('profile.newsletter.label')}</Typography> + <ContentSwitch + isEditing={isEditing} + checked={newsletterChecked} + onChange={toggleNewsletter} + trueLabel="profile.newsletter.value.yes" + falseLabel="profile.newsletter.value.no" + t={t} + /> + </Box> + </Box> + ); +}; + +export default ManagingNotificationsField;
@@ -1,17 +1,26 @@
-import {useState} from 'react'; +import {useReducer, useState} from 'react'; import Container from '@mui/material/Container'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import CardActions from '@mui/material/CardActions'; import Button from '@mui/material/Button'; -import { useTheme } from '@mui/material/styles'; +import {useTheme} from '@mui/material/styles'; import {useTranslation} from 'react-i18next'; import EditPassword from './EditPassword'; import ProfileField from './ProfileField'; import useToastStore from '../../stores/useToastStore'; -import {useUpdateMeMutation} from '../../generated/graphql'; +import { + UsersPermissionsUser, + useUpdateMeMutation, +} from '../../generated/graphql'; +import ManagingNotificationsField from './ManagingNotificationsField'; -const Profile = ({profile, logout}) => { +interface Props { + profile: UsersPermissionsUser; + logout: () => void; +} + +const Profile = ({profile, logout}: Props) => { const {t} = useTranslation(); const theme = useTheme(); const addToast = useToastStore(s => s.addToast);@@ -22,6 +31,15 @@ const [isEditingPassword, setIsEditingPassword] = useState(false);
const [firstName, setFirstName] = useState(profile.firstName); const [lastName, setLastName] = useState(profile.lastName); const [email, setEmail] = useState(profile.email); + const [newsletterConsent, toggleNewsletter] = useReducer( + i => !i, + profile.newsletterConsent + ); + const [notificationEnabled, toggleNotification] = useReducer( + i => !i, + profile.notificationEnabled + ); + const [oldPassword, setOldPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [errorPassword, setErrorPassword] = useState('');@@ -55,7 +73,13 @@ const onSave = async () => {
try { await updateProfile({ variables: { - userUpdate: {firstName, lastName, email}, + userUpdate: { + firstName, + lastName, + email, + newsletterConsent, + notificationEnabled, + }, }, }); setIsEditing(false);@@ -122,10 +146,18 @@ isEditing={isEditing}
disabled={!isStrapiUser} /> </CardContent> + <ManagingNotificationsField + isEditing={isEditing} + notificationChecked={notificationEnabled} + newsletterChecked={newsletterConsent} + toggleNotification={toggleNotification} + toggleNewsletter={toggleNewsletter} + /> + <CardActions sx={{justifyContent: 'flex-end'}}> {!isEditing && ( <> - <Button type="button" onClick={() => logout()}> + <Button type="button" onClick={logout}> {t('profile.actions.logout')} </Button> <Button
@@ -1854,6 +1854,7 @@ firstName?: Maybe<Scalars['String']['output']>;
lang?: Maybe<Enum_Userspermissionsuser_Lang>; lastName?: Maybe<Scalars['String']['output']>; newsletterConsent?: Maybe<Scalars['Boolean']['output']>; + notificationEnabled?: Maybe<Scalars['Boolean']['output']>; notifications?: Maybe<NotificationRelationResponseCollection>; onboardingCreator?: Maybe<Scalars['Boolean']['output']>; onboardingUser?: Maybe<Scalars['Boolean']['output']>;@@ -1916,6 +1917,7 @@ lang?: InputMaybe<StringFilterInput>;
lastName?: InputMaybe<StringFilterInput>; newsletterConsent?: InputMaybe<BooleanFilterInput>; not?: InputMaybe<UsersPermissionsUserFiltersInput>; + notificationEnabled?: InputMaybe<BooleanFilterInput>; notifications?: InputMaybe<NotificationFiltersInput>; onboardingCreator?: InputMaybe<BooleanFilterInput>; onboardingUser?: InputMaybe<BooleanFilterInput>;@@ -1940,6 +1942,7 @@ firstName?: InputMaybe<Scalars['String']['input']>;
lang?: InputMaybe<Enum_Userspermissionsuser_Lang>; lastName?: InputMaybe<Scalars['String']['input']>; newsletterConsent?: InputMaybe<Scalars['Boolean']['input']>; + notificationEnabled?: InputMaybe<Scalars['Boolean']['input']>; notifications?: InputMaybe<Array<InputMaybe<Scalars['ID']['input']>>>; oldPassword?: InputMaybe<Scalars['String']['input']>; onboardingCreator?: InputMaybe<Scalars['Boolean']['input']>;@@ -2159,19 +2162,19 @@
export type DeleteTravelMutation = { __typename?: 'Mutation', deleteTravel?: { __typename?: 'TravelEntityResponse', data?: { __typename?: 'TravelEntity', id?: string | null } | null } | null }; -export type UserFieldsFragment = { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null }; +export type UserFieldsFragment = { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null }; export type ProfileQueryVariables = Exact<{ [key: string]: never; }>; -export type ProfileQuery = { __typename?: 'Query', me?: { __typename?: 'UsersPermissionsMe', id: string, username: string, profile?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null }; +export type ProfileQuery = { __typename?: 'Query', me?: { __typename?: 'UsersPermissionsMe', id: string, username: string, profile?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null }; export type UpdateMeMutationVariables = Exact<{ userUpdate: UsersPermissionsUserInput; }>; -export type UpdateMeMutation = { __typename?: 'Mutation', updateMe: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null } }; +export type UpdateMeMutation = { __typename?: 'Mutation', updateMe: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null } }; export type VehicleFieldsFragment = { __typename?: 'VehicleEntity', id?: string | null, attributes?: { __typename?: 'Vehicle', name: string, seats?: number | null, phone_number?: string | null } | null };@@ -2332,6 +2335,8 @@ firstName
lang onboardingUser onboardingCreator + newsletterConsent + notificationEnabled provider events(pagination: {limit: 500}) { data {
@@ -7,6 +7,8 @@ firstName
lang onboardingUser onboardingCreator + newsletterConsent + notificationEnabled provider events(pagination: {limit: 500}) { data {
@@ -148,6 +148,12 @@ "profile.actions.logout": "Logout",
"profile.actions.save": "Save", "profile.actions.save_new_password": "Update", "profile.current_password": "Current password", + "profile.notification.label": "Notifications", + "profile.notification.value.yes": "Activated", + "profile.notification.value.no": "Disabled", + "profile.newsletter.label": "Receive our newsletter", + "profile.newsletter.value.yes": "Yes", + "profile.newsletter.value.no": "No", "profile.email": "Email", "profile.errors.password_nomatch": "Wrong password", "profile.firstName": "First name",
@@ -148,6 +148,12 @@ "profile.actions.logout": "Se déconnecter",
"profile.actions.save": "Enregistrer", "profile.actions.save_new_password": "Mettre à jour", "profile.current_password": "Mot de passe actuel", + "profile.notification.label": "Notifications", + "profile.notification.value.yes": "Activées", + "profile.notification.value.no": "Désactivées", + "profile.newsletter.label": "Recevoir la newsletter", + "profile.newsletter.value.yes": "Oui", + "profile.newsletter.value.no": "Non", "profile.email": "Email", "profile.errors.password_nomatch": "Mot de passe erroné", "profile.firstName": "Prénom",