diff --git a/app/(app)/profil/_layout.tsx b/app/(app)/profil/_layout.tsx index 175f65600..65d73fe27 100644 --- a/app/(app)/profil/_layout.tsx +++ b/app/(app)/profil/_layout.tsx @@ -43,25 +43,7 @@ function CustomRouter() { - - {[1, 2, 3].map((x) => ( - - ))} - - } - > - - + diff --git a/app/(app)/profil/mot-de-passe.tsx b/app/(app)/profil/mot-de-passe.tsx new file mode 100644 index 000000000..8d5d5a30b --- /dev/null +++ b/app/(app)/profil/mot-de-passe.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import ProfilLayout from '@/components/layouts/ProfilLayout' +import * as metatags from '@/config/metatags' +import ChangePassScreen from '@/screens/profil/password/page' +import Head from 'expo-router/head' + +function ChangePasswordScreen() { + return ( + <> + + {metatags.createTitle('Cotisation et dons')} + + + + + + + ) +} + +export default ChangePasswordScreen diff --git a/src/components/layouts/ProfilLayout.tsx b/src/components/layouts/ProfilLayout.tsx index 5945e92ff..f3fb03316 100644 --- a/src/components/layouts/ProfilLayout.tsx +++ b/src/components/layouts/ProfilLayout.tsx @@ -50,6 +50,24 @@ export default function ProfilLayout({ children }: { children: React.ReactNode } ) : ( - children + + {[1, 2, 3].map((x) => ( + + ))} + + } + > + {children} + ) } diff --git a/src/screens/profil/menu/Menu.tsx b/src/screens/profil/menu/Menu.tsx index 0d5377bb6..e5e937490 100644 --- a/src/screens/profil/menu/Menu.tsx +++ b/src/screens/profil/menu/Menu.tsx @@ -37,10 +37,11 @@ export const menuData: Array & { pathname?: Hre children: 'Communications', pathname: '/profil/communications', }, - // { - // icon: KeyRound, - // children: 'Mot de passe', - // }, + { + icon: KeyRound, + children: 'Mot de passe', + pathname: '/profil/mot-de-passe', + }, // { // icon: BadgeCheck, // children: 'Certification du profil', diff --git a/src/screens/profil/password/page.tsx b/src/screens/profil/password/page.tsx new file mode 100644 index 000000000..8f826da12 --- /dev/null +++ b/src/screens/profil/password/page.tsx @@ -0,0 +1,164 @@ +import { useMemo } from 'react' +import { KeyboardAvoidingView, Platform } from 'react-native' +import Input from '@/components/base/Input/Input' +import Text from '@/components/base/Text' +import { VoxButton } from '@/components/Button' +import PageLayout from '@/components/layouts/PageLayout/PageLayout' +import VoxCard from '@/components/VoxCard/VoxCard' +import { ProfilChangePasswordFormError } from '@/services/profile/error' +import { usetPostChangePassword } from '@/services/profile/hook' +import { zodResolver } from '@hookform/resolvers/zod' +import { Controller, useForm } from 'react-hook-form' +import { isWeb, ScrollView, useMedia, XStack, YStack } from 'tamagui' +import * as z from 'zod' + +const ChangePasswordSchema = z + .object({ + old_password: z.string().min(1, "L'ancien mot de passe est requis"), + new_password: z + .string() + .min(8, 'Le mot de passe doit contenir au moins 8 caractères') + .regex(/[A-Z]/, 'Le mot de passe doit contenir au moins une lettre majuscule') + .regex(/[a-z]/, 'Le mot de passe doit contenir au moins une lettre minuscule') + .regex(/[!@#$%^&*()\-=+{}\|:;\"'<>,.?[\]\/\\]/, 'Le mot de passe doit contenir au moins un charactère spécial'), + new_password_confirmation: z.string().min(1, 'La confirmation du mot de passe est requise'), + }) + .superRefine((data, ctx) => { + if (data.new_password !== data.new_password_confirmation) { + return ctx.addIssue({ + path: ['new_password_confirmation'], + code: z.ZodIssueCode.custom, + message: 'Les mots de passe ne correspondent pas', + }) + } + }) + +export default function ChangePasswordScreen() { + const media = useMedia() + const scrollViewContainerStyle = useMemo( + () => ({ + pt: media.gtSm ? '$8' : undefined, + pl: media.gtSm ? '$8' : undefined, + pr: media.gtSm ? '$8' : undefined, + pb: isWeb ? '$10' : '$12', + }), + [media], + ) + const { control, formState, handleSubmit, reset, setError } = useForm({ + resolver: zodResolver(ChangePasswordSchema), + mode: 'all', + defaultValues: { + old_password: '', + new_password: '', + new_password_confirmation: '', + }, + }) + const { isDirty, isValid } = formState + const { isPending, mutateAsync } = usetPostChangePassword() + + const onSubmit = handleSubmit((data) => { + mutateAsync(data) + .then(() => { + reset() + }) + .catch((e) => { + if (e instanceof ProfilChangePasswordFormError) { + e.violations.forEach((violation) => { + setError(violation.propertyPath, { message: violation.message }) + }) + } + }) + }) + + return ( + + + + + + Modifier mon mot de passe + Vous devez renseigner votre mot de passe actuel pour changer de mot de passe. + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + Afin de garantir un minimum de sécurité sur les accès votre mot de passe doit contenir au moins : + + + + 8 caractères minimum + + + + Une lettre majuscule + + + + Une lettre minuscule + + + + Un caractère spécial + + + + + + reset()}> + Annuler + + + Enregister + + + + + + + + ) +} diff --git a/src/services/profile/api.ts b/src/services/profile/api.ts index 0c3d9c42f..ba6e03da7 100644 --- a/src/services/profile/api.ts +++ b/src/services/profile/api.ts @@ -1,4 +1,9 @@ -import { profileElectDeclarationFormErrorThrower, profileElectPaymentFormErrorThrower, profileFormErrorThrower } from '@/services/profile/error' +import { + profilChangePasswordFormErrorThrower, + profileElectDeclarationFormErrorThrower, + profileElectPaymentFormErrorThrower, + profileFormErrorThrower, +} from '@/services/profile/error' import * as schemas from '@/services/profile/schema' import type * as Types from '@/services/profile/schema' import { api } from '@/utils/api' @@ -130,3 +135,11 @@ export const getTaxReceiptFile = (taxReceiptUuid: string) => { }, })() } +export const postChangePassword = api({ + method: 'POST', + path: '/api/v3/profile/me/password-change', + requestSchema: schemas.RestChangePasswordRequestSchema, + responseSchema: schemas.RestChangePasswordResponseSchema, + errorThrowers: [profilChangePasswordFormErrorThrower], + type: 'private', +}) diff --git a/src/services/profile/error.ts b/src/services/profile/error.ts index 98880352b..cacceecaa 100644 --- a/src/services/profile/error.ts +++ b/src/services/profile/error.ts @@ -1,9 +1,8 @@ import { z } from 'zod' import { createFormErrorResponseSchema, createFormErrorThrower } from '../common/errors/form-errors' -import { propertyPathDeclarationPaymentSchema, propertyPathElectPaymentSchema, propertyPathSchema } from './schema' +import { propertyPathChangePasswordSchema, propertyPathDeclarationPaymentSchema, propertyPathElectPaymentSchema, propertyPathSchema } from './schema' const profileFormErrorSchema = createFormErrorResponseSchema(propertyPathSchema) - export class ProfileFormError extends Error { violations: z.infer['violations'] constructor(public errors: z.infer) { @@ -11,11 +10,9 @@ export class ProfileFormError extends Error { this.violations = errors.violations } } - export const profileFormErrorThrower = createFormErrorThrower(ProfileFormError, profileFormErrorSchema) const profileElectPaymentFormErrorSchema = createFormErrorResponseSchema(propertyPathElectPaymentSchema) - export class profileElectPaymentFormError extends Error { violations: z.infer['violations'] constructor(public errors: z.infer) { @@ -23,11 +20,9 @@ export class profileElectPaymentFormError extends Error { this.violations = errors.violations } } - export const profileElectPaymentFormErrorThrower = createFormErrorThrower(profileElectPaymentFormError, profileElectPaymentFormErrorSchema) const profileElectDeclarationFormErrorSchema = createFormErrorResponseSchema(propertyPathDeclarationPaymentSchema) - export class profileElectDeclarationFormError extends Error { violations: z.infer['violations'] constructor(public errors: z.infer) { @@ -35,5 +30,14 @@ export class profileElectDeclarationFormError extends Error { this.violations = errors.violations } } - export const profileElectDeclarationFormErrorThrower = createFormErrorThrower(profileElectDeclarationFormError, profileElectDeclarationFormErrorSchema) + +const profilChangePasswordFormErrorSchema = createFormErrorResponseSchema(propertyPathChangePasswordSchema) +export class ProfilChangePasswordFormError extends Error { + violations: z.infer['violations'] + constructor(public errors: z.infer) { + super('FormError') + this.violations = errors.violations + } +} +export const profilChangePasswordFormErrorThrower = createFormErrorThrower(ProfilChangePasswordFormError, profilChangePasswordFormErrorSchema) diff --git a/src/services/profile/hook.ts b/src/services/profile/hook.ts index 1dc16ed28..c5017068a 100644 --- a/src/services/profile/hook.ts +++ b/src/services/profile/hook.ts @@ -248,3 +248,17 @@ export const useGetTaxReceiptFile = () => { }, }) } + +export const usetPostChangePassword = () => { + const toast = useToastController() + return useMutation({ + mutationFn: api.postChangePassword, + onSuccess: () => { + toast.show('Succès', { message: 'Mot de passe modifié', type: 'success' }) + }, + onError: (e) => { + toast.show('Erreur', { message: 'Impossible de modifier le mot de passe', type: 'error' }) + return e + }, + }) +} diff --git a/src/services/profile/schema.ts b/src/services/profile/schema.ts index 44e1561e6..f930d9643 100644 --- a/src/services/profile/schema.ts +++ b/src/services/profile/schema.ts @@ -297,3 +297,15 @@ export const RestTaxReceiptsResponseSchema = z.array( export const RestTaxReceiptFileRequestSchema = z.void() export const RestTaxReceiptFileResponseSchema = z.any() + +export const RestChangePasswordRequestSchema = z.object({ + old_password: z.string(), + new_password: z.string(), + new_password_confirmation: z.string(), +}) + +export type RestChangePasswordRequest = z.infer + +export const RestChangePasswordResponseSchema = z.any() + +export const propertyPathChangePasswordSchema = z.enum(['old_password', 'new_password', 'new_password_confirmation'])