From 01a20798f6dc8fc7ab95ce6175b2f636717de0c5 Mon Sep 17 00:00:00 2001 From: LucasBeneston Date: Fri, 22 Nov 2024 15:00:27 +0100 Subject: [PATCH 1/8] (PC-32700) feat(NC): add New Caledonia phone number check --- .../components/countryPicker/constants.ts | 5 ++ .../pages/phoneValidation/SetPhoneNumber.tsx | 2 +- .../helpers/isPhoneNumberValid.native.test.ts | 52 ++++++++++++++----- .../helpers/isPhoneNumberValid.ts | 41 +++++++++++++-- 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/features/identityCheck/components/countryPicker/constants.ts b/src/features/identityCheck/components/countryPicker/constants.ts index b6711931401..b6308944a84 100644 --- a/src/features/identityCheck/components/countryPicker/constants.ts +++ b/src/features/identityCheck/components/countryPicker/constants.ts @@ -53,4 +53,9 @@ export const COUNTRIES: Country[] = [ name: 'Wallis-et-Futuna', callingCode: '681', }, + { + id: 'NC', + name: 'Nouvelle-Calédonie', + callingCode: '687', + }, ] diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx index 9977db226cc..f5d2ffae491 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx @@ -46,7 +46,7 @@ export const SetPhoneNumber = () => { const [country, setCountry] = useState(INITIAL_COUNTRY) const { navigate } = useNavigation() const { goBack } = useGoBack(...homeNavConfig) - const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber) + const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber, country.callingCode) const saveStep = useSaveStep() const { remainingAttempts, isLastAttempt } = usePhoneValidationRemainingAttempts() diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts index 038711378c7..9ab190cc975 100644 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts +++ b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts @@ -1,19 +1,45 @@ import { isPhoneNumberValid } from 'features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid' +const METROPOLITAN_FRANCE_CALLING_CODE = '33' +const NEW_CALEDONIA_CALLING_CODE = '687' + describe('isPhoneNumberValid', () => { it.each` - phoneNumber | isValid - ${'111111111'} | ${true} - ${'0111111111'} | ${true} - ${'011111111'} | ${false} - ${'1111111'} | ${false} - ${'1.11.11.11.11'} | ${true} - ${'01.11.11.11.11'} | ${true} - ${'0-11-11-11-11'} | ${false} - ${'1-11-11-11'} | ${false} - `('should return $isValid if phone number is $phoneNumber', ({ phoneNumber, isValid }) => { - const result = isPhoneNumberValid(phoneNumber) + phoneNumber | countryCallingCode | isValid + ${'012345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'012345'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} + ${'012-345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'012-345'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} + ${'123456'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'123456'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} + ${'1234567'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'12345678'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'123456789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'123-456-789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'0123456789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'12-34-56'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'12-34-56'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} + ${'12-34-56-78'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'1-23-45-67-89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'01-23-45-67-89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'12.34.56'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'12.34.56'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} + ${'12.34.56.78'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'1.23.45.67.89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'01.23.45.67.89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} + ${'12345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'12345678910'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'011111111'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'0-11-11-11-11'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'1-11-11-11'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'+33123456'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + ${'06+123'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} + `( + 'should return $isValid if phone number is $phoneNumber and countryCallingCode is $countryCallingCode', + ({ phoneNumber, countryCallingCode, isValid }) => { + const result = isPhoneNumberValid(phoneNumber, countryCallingCode) - expect(result).toBe(isValid) - }) + expect(result).toBe(isValid) + } + ) }) diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts index d0ee78adb9e..bdf70be8aed 100644 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts +++ b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts @@ -1,4 +1,39 @@ -export function isPhoneNumberValid(number: string) { - // 9 digits, 10 if the first is a "0" that can be separated by whitespace, "." or "-". - return Boolean(number.match(/^(?:0)?\s*[1-9](?:[\s.-]*\d{2}){4}$/)) +import { Country } from 'features/identityCheck/components/countryPicker/types' + +const callingCodeWithSixDigitsAfter = ['687', '681'] + +export function isPhoneNumberValid(number: string, countryCallingCode: Country['callingCode']) { + const cleanedNumber = number.replace(/[-.]/g, '') + + const isNotOnlyDigits = !/^\d+$/.test(cleanedNumber) + if (isNotOnlyDigits) return false + + const isCountryWithSixDigitsAfterCallingCode = + callingCodeWithSixDigitsAfter.includes(countryCallingCode) + const isNotCountryWithSixDigitsAfterCallingCode = !isCountryWithSixDigitsAfterCallingCode + + const startsWithZero = cleanedNumber.startsWith('0') + const startsWithOneToNine = /^[1-9]/.test(cleanedNumber) + + switch (cleanedNumber.length) { + case 6: + return isCountryWithSixDigitsAfterCallingCode && Boolean(cleanedNumber.match(/^\d{6}$/)) + case 7: + case 8: + return false + case 9: + return ( + isNotCountryWithSixDigitsAfterCallingCode && + startsWithOneToNine && + Boolean(cleanedNumber.match(/^[1-9]\d{8}$/)) + ) + case 10: + return ( + isNotCountryWithSixDigitsAfterCallingCode && + startsWithZero && + Boolean(cleanedNumber.match(/^0\d{9}$/)) + ) + default: + return false + } } From 4e539c7d9f66905797087d9f3e11eda68eddd862 Mon Sep 17 00:00:00 2001 From: LucasBeneston Date: Fri, 22 Nov 2024 15:40:21 +0100 Subject: [PATCH 2/8] (PC-32700) feat(NC): use isPhoneNumberValid in SetPhoneNumberWithoutValidation --- ...oneNumberWithoutValidation.native.test.tsx | 52 +++++++++++++------ .../SetPhoneNumberWithoutValidation.tsx | 15 ++++-- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx index 02d31a2fae0..3bbce82517d 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx @@ -6,7 +6,7 @@ import { UserProfileResponse } from 'api/gen' import { initialSubscriptionState } from 'features/identityCheck/context/reducer' import * as SubscriptionContextProvider from 'features/identityCheck/context/SubscriptionContextProvider' import { reactQueryProviderHOC } from 'tests/reactQueryProviderHOC' -import { act, fireEvent, render, screen, waitFor } from 'tests/utils' +import { act, fireEvent, render, screen } from 'tests/utils' import { SetPhoneNumberWithoutValidation } from './SetPhoneNumberWithoutValidation' @@ -28,8 +28,36 @@ describe('SetPhoneNumberWithoutValidation', () => { expect(screen).toMatchSnapshot() }) + describe('when form validation', () => { + it('should disable the button when phone number is invalid', async () => { + givenStoredPhoneNumber('', { callingCode: '33', countryCode: 'FR' }) + + renderSetPhoneNumberWithoutValidation() + + await fillPhoneNumberInput('123') + + const button = screen.getByText('Continuer') + await act(() => { + expect(button).toBeDisabled() + }) + }) + + it('should enable the button when phone number is valid', async () => { + givenStoredPhoneNumber('', { callingCode: '33', countryCode: 'FR' }) + + renderSetPhoneNumberWithoutValidation() + + await fillPhoneNumberInput('0612345678') + + const button = screen.getByText('Continuer') + await act(() => { + expect(button).toBeEnabled() + }) + }) + }) + describe('when user already given his phone number', () => { - test('Use the phone number already given', () => { + it('should use the phone number already given', () => { givenStoredPhoneNumber('0612345678', { callingCode: '33', countryCode: 'FR' }) const { unmount } = renderSetPhoneNumberWithoutValidation() @@ -41,7 +69,7 @@ describe('SetPhoneNumberWithoutValidation', () => { unmount() // to avoid act warning https://github.com/orgs/react-hook-form/discussions/3108#discussioncomment-8514714 }) - test('Use the country already given', () => { + it('should use the country already given', () => { givenStoredPhoneNumber('0612345678', { callingCode: '596', countryCode: 'MQ' }) const { unmount } = renderSetPhoneNumberWithoutValidation() @@ -60,12 +88,12 @@ describe('SetPhoneNumberWithoutValidation', () => { updatePhoneNumberWillSucceed() }) - test('Redirect to steppers when update phone number is succeed', async () => { + it('should redirect to steppers when update phone number is succeed', async () => { const { unmount } = renderSetPhoneNumberWithoutValidation() await submitWithPhoneNumber('0612345678') - await waitFor(() => { + await act(() => { expect(dispatch).toHaveBeenCalledWith({ payload: { index: 1, routes: [{ name: 'TabNavigator' }, { name: 'Stepper' }] }, type: 'RESET', @@ -75,7 +103,7 @@ describe('SetPhoneNumberWithoutValidation', () => { unmount() }) - test('Store phone number', async () => { + it('should store phone number', async () => { const { unmount } = renderSetPhoneNumberWithoutValidation() await submitWithPhoneNumber('0612345678') @@ -93,7 +121,7 @@ describe('SetPhoneNumberWithoutValidation', () => { }) describe('When failure', () => { - test('Show error message when update phone number is failed', async () => { + it('should show error message when update phone number is failed', async () => { updatePhoneNumberWillFail() const { unmount } = renderSetPhoneNumberWithoutValidation() @@ -106,16 +134,6 @@ describe('SetPhoneNumberWithoutValidation', () => { }) }) - test('User can NOT send form when form is invalid', async () => { - renderSetPhoneNumberWithoutValidation() - - await fillPhoneNumberInput('') - - const button = screen.getByText('Continuer') - - expect(button).toBeDisabled() - }) - function renderSetPhoneNumberWithoutValidation() { return render(reactQueryProviderHOC()) } diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx index e8966bcdf7a..118b7582f6c 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx @@ -29,9 +29,16 @@ import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' import { invalidateStepperInfoQuery } from '../helpers/invalidateStepperQuery' import { formatPhoneNumberWithPrefix } from './helpers/formatPhoneNumber' +import { isPhoneNumberValid } from './helpers/isPhoneNumberValid' // Assure-toi que l'import est correct const schema = yup.object({ - phoneNumber: yup.string().required('Le numéro de téléphone est requis'), + phoneNumber: yup + .string() + .required('Le numéro de téléphone est requis') + .test('is-valid-phone', '', (value, { parent }) => { + const country = findCountry(parent.countryId) + return country ? isPhoneNumberValid(value ?? '', country.callingCode) : false + }), countryId: yup.string().required(), }) @@ -51,8 +58,6 @@ export const SetPhoneNumberWithoutValidation = () => { mode: 'onChange', }) - const disableSubmit = !formState.isValid - const { navigateForwardToStepper } = useNavigateForwardToStepper() const saveStep = useSaveStep() const { mutate: updateProfile } = useUpdateProfileMutation( @@ -117,7 +122,7 @@ export const SetPhoneNumberWithoutValidation = () => { { } fixedBottomChildren={ Date: Fri, 22 Nov 2024 16:02:56 +0100 Subject: [PATCH 3/8] (PC-32700) feat(NC): fix Sonar issues for isPhoneNumberValid --- .../phoneValidation/helpers/isPhoneNumberValid.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts index bdf70be8aed..7304b55391d 100644 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts +++ b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts @@ -15,9 +15,13 @@ export function isPhoneNumberValid(number: string, countryCallingCode: Country[' const startsWithZero = cleanedNumber.startsWith('0') const startsWithOneToNine = /^[1-9]/.test(cleanedNumber) + const sixDigitRegex = /^\d{6}$/ + const nineDigitRegex = /^[1-9]\d{8}$/ + const tenDigitRegex = /^0\d{9}$/ + switch (cleanedNumber.length) { case 6: - return isCountryWithSixDigitsAfterCallingCode && Boolean(cleanedNumber.match(/^\d{6}$/)) + return isCountryWithSixDigitsAfterCallingCode && !!sixDigitRegex.exec(cleanedNumber) case 7: case 8: return false @@ -25,13 +29,13 @@ export function isPhoneNumberValid(number: string, countryCallingCode: Country[' return ( isNotCountryWithSixDigitsAfterCallingCode && startsWithOneToNine && - Boolean(cleanedNumber.match(/^[1-9]\d{8}$/)) + !!nineDigitRegex.exec(cleanedNumber) ) case 10: return ( isNotCountryWithSixDigitsAfterCallingCode && startsWithZero && - Boolean(cleanedNumber.match(/^0\d{9}$/)) + !!tenDigitRegex.exec(cleanedNumber) ) default: return false From 86440cbfd17408b1e44b54a0046945ce7998c739 Mon Sep 17 00:00:00 2001 From: Maxime Le Duc Date: Wed, 27 Nov 2024 15:07:19 +0100 Subject: [PATCH 4/8] Return server error message --- .../phoneValidation/SetPhoneNumberWithoutValidation.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx index 118b7582f6c..1bb1a5d57c2 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx @@ -81,8 +81,10 @@ export const SetPhoneNumberWithoutValidation = () => { invalidateStepperInfoQuery() navigateForwardToStepper() }, - () => { - setError('phoneNumber', { message: 'Une erreur est survenue' }) + (e) => { + if (e instanceof Error) { + setError('phoneNumber', { message: e.message }) + } } ) From e53449e788238082f3cdd6940f9cf38efdf0a604 Mon Sep 17 00:00:00 2001 From: Maxime Le Duc Date: Wed, 27 Nov 2024 15:19:30 +0100 Subject: [PATCH 5/8] Explicite onSuccess and onError --- .../SetPhoneNumberWithoutValidation.tsx | 14 ++++++-------- .../profile/api/useUpdateProfileMutation.ts | 14 ++++++++------ .../profile/pages/ChangeCity/ChangeCity.tsx | 10 +++++----- .../pages/ChangeStatus/useSubmitChangeStatus.tsx | 11 ++++++----- .../NotificationSettings/NotificationsSettings.tsx | 11 ++++++----- .../helpers/useThematicSubscription.tsx | 10 +++++----- .../subscription/page/OnboardingSubscription.tsx | 10 +++++----- 7 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx index 1bb1a5d57c2..24c2a9c7877 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx @@ -60,8 +60,8 @@ export const SetPhoneNumberWithoutValidation = () => { const { navigateForwardToStepper } = useNavigateForwardToStepper() const saveStep = useSaveStep() - const { mutate: updateProfile } = useUpdateProfileMutation( - () => { + const { mutate: updateProfile } = useUpdateProfileMutation({ + onSuccess: () => { const { phoneNumber, countryId } = getValues() const country = findCountry(countryId) if (!country) { @@ -81,12 +81,10 @@ export const SetPhoneNumberWithoutValidation = () => { invalidateStepperInfoQuery() navigateForwardToStepper() }, - (e) => { - if (e instanceof Error) { - setError('phoneNumber', { message: e.message }) - } - } - ) + onError: (error) => { + if (error instanceof Error) setError('phoneNumber', { message: error.message }) + }, + }) const onSubmit = async ({ phoneNumber, countryId }: FormValues) => { const country = findCountry(countryId) diff --git a/src/features/profile/api/useUpdateProfileMutation.ts b/src/features/profile/api/useUpdateProfileMutation.ts index ae69d1bc52e..00fae4022a8 100644 --- a/src/features/profile/api/useUpdateProfileMutation.ts +++ b/src/features/profile/api/useUpdateProfileMutation.ts @@ -4,10 +4,12 @@ import { api } from 'api/api' import { UserProfileResponse, UserProfilePatchRequest } from 'api/gen' import { QueryKeys } from 'libs/queryKeys' -export function useUpdateProfileMutation( - onSuccessCallback: (data: UserProfileResponse, body: UserProfilePatchRequest) => void, - onErrorCallback: (error: unknown) => void -) { +type Options = { + onSuccess?: (data: UserProfileResponse, body: UserProfilePatchRequest) => void + onError?: (error: unknown) => void +} + +export function useUpdateProfileMutation({ onError, onSuccess }: Options) { const client = useQueryClient() return useMutation((body: UserProfilePatchRequest) => api.patchNativeV1Profile(body), { onSuccess(response: UserProfileResponse, variables) { @@ -15,8 +17,8 @@ export function useUpdateProfileMutation( ...(old ?? {}), ...response, })) - onSuccessCallback(response, variables) + onSuccess?.(response, variables) }, - onError: onErrorCallback, + onError, }) } diff --git a/src/features/profile/pages/ChangeCity/ChangeCity.tsx b/src/features/profile/pages/ChangeCity/ChangeCity.tsx index 8c30eb9c903..5cc5e8d2b5d 100644 --- a/src/features/profile/pages/ChangeCity/ChangeCity.tsx +++ b/src/features/profile/pages/ChangeCity/ChangeCity.tsx @@ -31,8 +31,8 @@ export const ChangeCity = () => { resolver: yupResolver(cityResolver), defaultValues: { city: storedCity ?? undefined }, }) - const { mutate: updateProfile } = useUpdateProfileMutation( - (_, variables) => { + const { mutate: updateProfile } = useUpdateProfileMutation({ + onSuccess: (_, variables) => { analytics.logUpdatePostalCode({ newCity: variables.city ?? '', oldCity: user?.city ?? '', @@ -44,13 +44,13 @@ export const ChangeCity = () => { timeout: SNACK_BAR_TIME_OUT, }) }, - () => { + onError: () => { showErrorSnackBar({ message: 'Une erreur est survenue', timeout: SNACK_BAR_TIME_OUT, }) - } - ) + }, + }) const onSubmit = ({ city }: CityForm) => { setCity(city) diff --git a/src/features/profile/pages/ChangeStatus/useSubmitChangeStatus.tsx b/src/features/profile/pages/ChangeStatus/useSubmitChangeStatus.tsx index 87581074605..5a9a0e25fd4 100644 --- a/src/features/profile/pages/ChangeStatus/useSubmitChangeStatus.tsx +++ b/src/features/profile/pages/ChangeStatus/useSubmitChangeStatus.tsx @@ -19,8 +19,8 @@ export const useSubmitChangeStatus = () => { const { user } = useAuthContext() const { navigate } = useNavigation() const { showSuccessSnackBar, showErrorSnackBar } = useSnackBarContext() - const { mutate: patchProfile, isLoading } = useUpdateProfileMutation( - (_, variables) => { + const { mutate: patchProfile, isLoading } = useUpdateProfileMutation({ + onSuccess: (_, variables) => { analytics.logUpdateStatus({ oldStatus: user?.activityId ?? '', newStatus: variables.activityId ?? '', @@ -30,13 +30,14 @@ export const useSubmitChangeStatus = () => { timeout: SNACK_BAR_TIME_OUT, }) }, - () => { + + onError: () => { showErrorSnackBar({ message: 'Une erreur est survenue', timeout: SNACK_BAR_TIME_OUT, }) - } - ) + }, + }) const { control, handleSubmit, diff --git a/src/features/profile/pages/NotificationSettings/NotificationsSettings.tsx b/src/features/profile/pages/NotificationSettings/NotificationsSettings.tsx index a84fb443e46..81c6f13956d 100644 --- a/src/features/profile/pages/NotificationSettings/NotificationsSettings.tsx +++ b/src/features/profile/pages/NotificationSettings/NotificationsSettings.tsx @@ -64,22 +64,23 @@ export const NotificationsSettings = () => { const { pushPermission } = usePushPermission(updatePushPermissionFromSettings) - const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation( - () => { + const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation({ + onSuccess: () => { showSuccessSnackBar({ message: 'Tes modifications ont été enregistrées\u00a0!', timeout: SNACK_BAR_TIME_OUT, }) analytics.logNotificationToggle(!!state.allowEmails, state.allowPush) }, - () => { + + onError: () => { showErrorSnackBar({ message: 'Une erreur est survenue', timeout: SNACK_BAR_TIME_OUT, }) dispatch({ type: 'reset', initialState }) - } - ) + }, + }) const areNotificationsEnabled = Platform.OS === 'web' ? state.allowEmails : state.allowEmails || state.allowPush diff --git a/src/features/subscription/helpers/useThematicSubscription.tsx b/src/features/subscription/helpers/useThematicSubscription.tsx index 89323bf53d8..223675315c7 100644 --- a/src/features/subscription/helpers/useThematicSubscription.tsx +++ b/src/features/subscription/helpers/useThematicSubscription.tsx @@ -53,8 +53,8 @@ export const useThematicSubscription = ({ const isSubscribeButtonActive = isAtLeastOneNotificationTypeActivated && isThemeSubscribed - const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation( - async () => { + const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation({ + onSuccess: async () => { analytics.logNotificationToggle(!!state.allowEmails, !!state.allowPush) const analyticsParams = homeId ? { from: 'thematicHome', entryId: homeId } @@ -72,14 +72,14 @@ export const useThematicSubscription = ({ } as SubscriptionAnalyticsParams) } }, - () => { + onError: () => { showErrorSnackBar({ message: 'Une erreur est survenue, veuillez réessayer', timeout: SNACK_BAR_TIME_OUT, }) setState(initialState) - } - ) + }, + }) if (!thematic) { return { diff --git a/src/features/subscription/page/OnboardingSubscription.tsx b/src/features/subscription/page/OnboardingSubscription.tsx index bae78d2d3d9..ab27e176298 100644 --- a/src/features/subscription/page/OnboardingSubscription.tsx +++ b/src/features/subscription/page/OnboardingSubscription.tsx @@ -65,8 +65,8 @@ export const OnboardingSubscription = () => { initialSubscribedThemes ) - const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation( - () => { + const { mutate: updateProfile, isLoading: isUpdatingProfile } = useUpdateProfileMutation({ + onSuccess: () => { analytics.logSubscriptionUpdate({ type: 'update', from: 'home' }) showSuccessSnackBar({ message: 'Thèmes suivis\u00a0! Tu peux gérer tes alertes depuis ton profil.', @@ -74,13 +74,13 @@ export const OnboardingSubscription = () => { }) replace(...homeNavConfig) }, - () => { + onError: () => { showErrorSnackBar({ message: 'Une erreur est survenue, tu peux réessayer plus tard.', timeout: SNACK_BAR_TIME_OUT, }) - } - ) + }, + }) const isThemeChecked = (theme: SubscriptionTheme) => subscribedThemes.includes(theme) From 566896d6d92cb1e789ad28a846eb040c6a301a19 Mon Sep 17 00:00:00 2001 From: Maxime Le Duc Date: Wed, 27 Nov 2024 15:45:05 +0100 Subject: [PATCH 6/8] Use libphonenumber-js for number validation --- .../SetPhoneNumberWithoutValidation.tsx | 7 +-- .../helpers/isPhoneNumberValid.native.test.ts | 45 ------------------- .../helpers/isPhoneNumberValid.ts | 45 ++----------------- 3 files changed, 8 insertions(+), 89 deletions(-) delete mode 100644 src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx index 24c2a9c7877..843149e58b3 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.tsx @@ -4,6 +4,7 @@ import { Controller, useForm } from 'react-hook-form' import { v4 as uuidv4 } from 'uuid' import * as yup from 'yup' +import { isApiError } from 'api/apiHelpers' import { COUNTRIES, METROPOLITAN_FRANCE, @@ -29,7 +30,7 @@ import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' import { invalidateStepperInfoQuery } from '../helpers/invalidateStepperQuery' import { formatPhoneNumberWithPrefix } from './helpers/formatPhoneNumber' -import { isPhoneNumberValid } from './helpers/isPhoneNumberValid' // Assure-toi que l'import est correct +import { isPhoneNumberValid } from './helpers/isPhoneNumberValid' const schema = yup.object({ phoneNumber: yup @@ -37,7 +38,7 @@ const schema = yup.object({ .required('Le numéro de téléphone est requis') .test('is-valid-phone', '', (value, { parent }) => { const country = findCountry(parent.countryId) - return country ? isPhoneNumberValid(value ?? '', country.callingCode) : false + return country ? isPhoneNumberValid(value ?? '', country.id) : false }), countryId: yup.string().required(), }) @@ -82,7 +83,7 @@ export const SetPhoneNumberWithoutValidation = () => { navigateForwardToStepper() }, onError: (error) => { - if (error instanceof Error) setError('phoneNumber', { message: error.message }) + isApiError(error) && setError('phoneNumber', { message: error.message }) }, }) diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts deleted file mode 100644 index 9ab190cc975..00000000000 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.native.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { isPhoneNumberValid } from 'features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid' - -const METROPOLITAN_FRANCE_CALLING_CODE = '33' -const NEW_CALEDONIA_CALLING_CODE = '687' - -describe('isPhoneNumberValid', () => { - it.each` - phoneNumber | countryCallingCode | isValid - ${'012345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'012345'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} - ${'012-345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'012-345'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} - ${'123456'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'123456'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} - ${'1234567'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'12345678'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'123456789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'123-456-789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'0123456789'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'12-34-56'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'12-34-56'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} - ${'12-34-56-78'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'1-23-45-67-89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'01-23-45-67-89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'12.34.56'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'12.34.56'} | ${NEW_CALEDONIA_CALLING_CODE} | ${true} - ${'12.34.56.78'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'1.23.45.67.89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'01.23.45.67.89'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${true} - ${'12345'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'12345678910'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'011111111'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'0-11-11-11-11'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'1-11-11-11'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'+33123456'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - ${'06+123'} | ${METROPOLITAN_FRANCE_CALLING_CODE} | ${false} - `( - 'should return $isValid if phone number is $phoneNumber and countryCallingCode is $countryCallingCode', - ({ phoneNumber, countryCallingCode, isValid }) => { - const result = isPhoneNumberValid(phoneNumber, countryCallingCode) - - expect(result).toBe(isValid) - } - ) -}) diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts index 7304b55391d..d7968108504 100644 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts +++ b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts @@ -1,43 +1,6 @@ -import { Country } from 'features/identityCheck/components/countryPicker/types' +import parsePhoneNumberFromString, { CountryCode } from 'libphonenumber-js' -const callingCodeWithSixDigitsAfter = ['687', '681'] - -export function isPhoneNumberValid(number: string, countryCallingCode: Country['callingCode']) { - const cleanedNumber = number.replace(/[-.]/g, '') - - const isNotOnlyDigits = !/^\d+$/.test(cleanedNumber) - if (isNotOnlyDigits) return false - - const isCountryWithSixDigitsAfterCallingCode = - callingCodeWithSixDigitsAfter.includes(countryCallingCode) - const isNotCountryWithSixDigitsAfterCallingCode = !isCountryWithSixDigitsAfterCallingCode - - const startsWithZero = cleanedNumber.startsWith('0') - const startsWithOneToNine = /^[1-9]/.test(cleanedNumber) - - const sixDigitRegex = /^\d{6}$/ - const nineDigitRegex = /^[1-9]\d{8}$/ - const tenDigitRegex = /^0\d{9}$/ - - switch (cleanedNumber.length) { - case 6: - return isCountryWithSixDigitsAfterCallingCode && !!sixDigitRegex.exec(cleanedNumber) - case 7: - case 8: - return false - case 9: - return ( - isNotCountryWithSixDigitsAfterCallingCode && - startsWithOneToNine && - !!nineDigitRegex.exec(cleanedNumber) - ) - case 10: - return ( - isNotCountryWithSixDigitsAfterCallingCode && - startsWithZero && - !!tenDigitRegex.exec(cleanedNumber) - ) - default: - return false - } +export function isPhoneNumberValid(number: string, country: string) { + const phoneNumber = parsePhoneNumberFromString(number, country as CountryCode) + return phoneNumber?.isValid() ?? false } From 4da7deca07be3e771fd5839b2f033971b8a8af28 Mon Sep 17 00:00:00 2001 From: Maxime Le Duc Date: Wed, 27 Nov 2024 16:18:55 +0100 Subject: [PATCH 7/8] Fix tests --- .../identityCheck/pages/phoneValidation/SetPhoneNumber.tsx | 2 +- .../SetPhoneNumberWithoutValidation.native.test.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx index f5d2ffae491..e5b79ab96c3 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumber.tsx @@ -46,7 +46,7 @@ export const SetPhoneNumber = () => { const [country, setCountry] = useState(INITIAL_COUNTRY) const { navigate } = useNavigation() const { goBack } = useGoBack(...homeNavConfig) - const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber, country.callingCode) + const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber, country.id) const saveStep = useSaveStep() const { remainingAttempts, isLastAttempt } = usePhoneValidationRemainingAttempts() diff --git a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx index 3bbce82517d..089bbde6656 100644 --- a/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx +++ b/src/features/identityCheck/pages/phoneValidation/SetPhoneNumberWithoutValidation.native.test.tsx @@ -2,6 +2,7 @@ import React from 'react' import { dispatch } from '__mocks__/@react-navigation/native' import * as API from 'api/api' +import { ApiError } from 'api/ApiError' import { UserProfileResponse } from 'api/gen' import { initialSubscriptionState } from 'features/identityCheck/context/reducer' import * as SubscriptionContextProvider from 'features/identityCheck/context/SubscriptionContextProvider' @@ -143,7 +144,7 @@ describe('SetPhoneNumberWithoutValidation', () => { } function updatePhoneNumberWillFail() { - patchProfile.mockRejectedValueOnce(new Error('Une erreur est survenue')) + patchProfile.mockRejectedValueOnce(new ApiError(500, undefined, 'Une erreur est survenue')) } async function fillPhoneNumberInput(phoneNumber: string) { From 79cf79f58a8a01f89f6139ae7fd3f084673bba2d Mon Sep 17 00:00:00 2001 From: Maxime Le Duc Date: Wed, 27 Nov 2024 16:47:58 +0100 Subject: [PATCH 8/8] Stop using "as" --- .../phoneValidation/helpers/isPhoneNumberValid.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts index d7968108504..2d058c0190a 100644 --- a/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts +++ b/src/features/identityCheck/pages/phoneValidation/helpers/isPhoneNumberValid.ts @@ -1,6 +1,13 @@ -import parsePhoneNumberFromString, { CountryCode } from 'libphonenumber-js' +import parsePhoneNumberFromString, { CountryCode, getCountries } from 'libphonenumber-js' + +function isCountryCode(code: string): code is CountryCode { + const countries: string[] = getCountries() + return countries.includes(code) +} export function isPhoneNumberValid(number: string, country: string) { - const phoneNumber = parsePhoneNumberFromString(number, country as CountryCode) + if (!isCountryCode(country)) return false + + const phoneNumber = parsePhoneNumberFromString(number, country) return phoneNumber?.isValid() ?? false }