Skip to content

Commit

Permalink
feat(profil): change password (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
OverGlass authored Oct 8, 2024
1 parent b28005f commit a8e3dca
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 32 deletions.
20 changes: 1 addition & 19 deletions app/(app)/profil/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,7 @@ function CustomRouter() {
</XStack>
</PageLayout.SideBarLeft>
<PageLayout.MainSingleColumn>
<BoundarySuspenseWrapper
fallback={
<YStack
gap={16}
$gtSm={{
pt: '$8',
pl: '$8',
pr: '$8',
}}
pb={isWeb ? '$10' : '$12'}
>
{[1, 2, 3].map((x) => (
<Skeleton key={x} />
))}
</YStack>
}
>
<Slot />
</BoundarySuspenseWrapper>
<Slot />
</PageLayout.MainSingleColumn>
<PageLayout.SideBarRight />
</PageLayout>
Expand Down
21 changes: 21 additions & 0 deletions app/(app)/profil/mot-de-passe.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<title>{metatags.createTitle('Cotisation et dons')}</title>
</Head>

<ProfilLayout>
<ChangePassScreen />
</ProfilLayout>
</>
)
}

export default ChangePasswordScreen
20 changes: 19 additions & 1 deletion src/components/layouts/ProfilLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ export default function ProfilLayout({ children }: { children: React.ReactNode }
<PageLayout.SideBarRight />
</PageLayout>
) : (
children
<BoundarySuspenseWrapper
fallback={
<YStack
gap={16}
$gtSm={{
pt: '$8',
pl: '$8',
pr: '$8',
}}
pb={isWeb ? '$10' : '$12'}
>
{[1, 2, 3].map((x) => (
<Skeleton key={x} />
))}
</YStack>
}
>
{children}
</BoundarySuspenseWrapper>
)
}
9 changes: 5 additions & 4 deletions src/screens/profil/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ export const menuData: Array<ComponentProps<typeof Menu.Item> & { 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',
Expand Down
164 changes: 164 additions & 0 deletions src/screens/profil/password/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageLayout.MainSingleColumn position="relative">
<KeyboardAvoidingView behavior={Platform.OS === 'android' ? 'height' : 'padding'} style={{ flex: 1 }} keyboardVerticalOffset={100}>
<ScrollView contentContainerStyle={scrollViewContainerStyle}>
<VoxCard>
<VoxCard.Content>
<VoxCard.Title>Modifier mon mot de passe</VoxCard.Title>
<Text.P> Vous devez renseigner votre mot de passe actuel pour changer de mot de passe.</Text.P>
<Controller
control={control}
name="old_password"
render={({ field, fieldState }) => (
<Input
color="gray"
onChange={field.onChange}
onBlur={field.onBlur}
value={field.value}
placeholder="Mot de passe actuel"
type="password"
error={fieldState.error?.message}
/>
)}
/>

<Controller
control={control}
name="new_password"
render={({ field, fieldState }) => (
<Input
color="gray"
onChange={field.onChange}
onBlur={field.onBlur}
value={field.value}
type="password"
placeholder="Nouveau mot de passe"
error={fieldState.error?.message}
/>
)}
/>

<Controller
control={control}
name="new_password_confirmation"
render={({ field, fieldState }) => (
<Input
color="gray"
onChange={field.onChange}
onBlur={field.onBlur}
value={field.value}
placeholder="Confirmer le nouveau mot de passe"
type="password"
error={fieldState.error?.message}
/>
)}
/>
<YStack>
<Text.P>Afin de garantir un minimum de sécurité sur les accès votre mot de passe doit contenir au moins :</Text.P>
<YStack pl={12}>
<XStack gap={6}>
<Text.P></Text.P>
<Text.P>8 caractères minimum</Text.P>
</XStack>
<XStack gap={6}>
<Text.P></Text.P>
<Text.P>Une lettre majuscule</Text.P>
</XStack>
<XStack gap={6}>
<Text.P></Text.P>
<Text.P>Une lettre minuscule</Text.P>
</XStack>
<XStack gap={6}>
<Text.P></Text.P>
<Text.P>Un caractère spécial</Text.P>
</XStack>
</YStack>
</YStack>

<XStack justifyContent="flex-end" gap="$2">
<VoxButton variant="outlined" display={isDirty ? 'flex' : 'none'} onPress={() => reset()}>
Annuler
</VoxButton>
<VoxButton variant="outlined" theme="blue" onPress={onSubmit} loading={isPending} disabled={!isDirty || !isValid}>
Enregister
</VoxButton>
</XStack>
</VoxCard.Content>
</VoxCard>
</ScrollView>
</KeyboardAvoidingView>
</PageLayout.MainSingleColumn>
)
}
15 changes: 14 additions & 1 deletion src/services/profile/api.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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',
})
18 changes: 11 additions & 7 deletions src/services/profile/error.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
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<typeof profileFormErrorSchema>['violations']
constructor(public errors: z.infer<typeof profileFormErrorSchema>) {
super('FormError')
this.violations = errors.violations
}
}

export const profileFormErrorThrower = createFormErrorThrower(ProfileFormError, profileFormErrorSchema)

const profileElectPaymentFormErrorSchema = createFormErrorResponseSchema(propertyPathElectPaymentSchema)

export class profileElectPaymentFormError extends Error {
violations: z.infer<typeof profileElectPaymentFormErrorSchema>['violations']
constructor(public errors: z.infer<typeof profileElectPaymentFormErrorSchema>) {
super('FormError')
this.violations = errors.violations
}
}

export const profileElectPaymentFormErrorThrower = createFormErrorThrower(profileElectPaymentFormError, profileElectPaymentFormErrorSchema)

const profileElectDeclarationFormErrorSchema = createFormErrorResponseSchema(propertyPathDeclarationPaymentSchema)

export class profileElectDeclarationFormError extends Error {
violations: z.infer<typeof profileElectDeclarationFormErrorSchema>['violations']
constructor(public errors: z.infer<typeof profileElectDeclarationFormErrorSchema>) {
super('FormError')
this.violations = errors.violations
}
}

export const profileElectDeclarationFormErrorThrower = createFormErrorThrower(profileElectDeclarationFormError, profileElectDeclarationFormErrorSchema)

const profilChangePasswordFormErrorSchema = createFormErrorResponseSchema(propertyPathChangePasswordSchema)
export class ProfilChangePasswordFormError extends Error {
violations: z.infer<typeof profilChangePasswordFormErrorSchema>['violations']
constructor(public errors: z.infer<typeof profilChangePasswordFormErrorSchema>) {
super('FormError')
this.violations = errors.violations
}
}
export const profilChangePasswordFormErrorThrower = createFormErrorThrower(ProfilChangePasswordFormError, profilChangePasswordFormErrorSchema)
14 changes: 14 additions & 0 deletions src/services/profile/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
})
}
12 changes: 12 additions & 0 deletions src/services/profile/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof RestChangePasswordRequestSchema>

export const RestChangePasswordResponseSchema = z.any()

export const propertyPathChangePasswordSchema = z.enum(['old_password', 'new_password', 'new_password_confirmation'])

0 comments on commit a8e3dca

Please sign in to comment.