Skip to content

Commit

Permalink
(PC-32700) feat(NC): add New Caledonian phone number check in SetPhon…
Browse files Browse the repository at this point in the history
…eNumber (#7266)

* (PC-32700) feat(NC): add New Caledonia phone number check

* (PC-32700) feat(NC): use isPhoneNumberValid in SetPhoneNumberWithoutValidation

* (PC-32700) feat(NC): fix Sonar issues for isPhoneNumberValid

* Return server error message

* Explicite onSuccess and onError

* Use libphonenumber-js for number validation

* Fix tests

* Stop using "as"

---------

Co-authored-by: Maxime Le Duc <[email protected]>
  • Loading branch information
lbeneston-pass and mleduc-pass authored Nov 28, 2024
1 parent 94a7b12 commit d220ba3
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ export const COUNTRIES: Country[] = [
name: 'Wallis-et-Futuna',
callingCode: '681',
},
{
id: 'NC',
name: 'Nouvelle-Calédonie',
callingCode: '687',
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const SetPhoneNumber = () => {
const [country, setCountry] = useState<Country>(INITIAL_COUNTRY)
const { navigate } = useNavigation<UseNavigationType>()
const { goBack } = useGoBack(...homeNavConfig)
const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber)
const isContinueButtonEnabled = isPhoneNumberValid(phoneNumber, country.id)
const saveStep = useSaveStep()

const { remainingAttempts, isLastAttempt } = usePhoneValidationRemainingAttempts()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ 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'
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'

Expand All @@ -28,8 +29,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()
Expand All @@ -41,7 +70,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()
Expand All @@ -60,12 +89,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',
Expand All @@ -75,7 +104,7 @@ describe('SetPhoneNumberWithoutValidation', () => {
unmount()
})

test('Store phone number', async () => {
it('should store phone number', async () => {
const { unmount } = renderSetPhoneNumberWithoutValidation()

await submitWithPhoneNumber('0612345678')
Expand All @@ -93,7 +122,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()

Expand All @@ -106,16 +135,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(<SetPhoneNumberWithoutValidation />))
}
Expand All @@ -125,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,9 +30,16 @@ import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs'
import { invalidateStepperInfoQuery } from '../helpers/invalidateStepperQuery'

import { formatPhoneNumberWithPrefix } from './helpers/formatPhoneNumber'
import { isPhoneNumberValid } from './helpers/isPhoneNumberValid'

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.id) : false
}),
countryId: yup.string().required(),
})

Expand All @@ -51,12 +59,10 @@ export const SetPhoneNumberWithoutValidation = () => {
mode: 'onChange',
})

const disableSubmit = !formState.isValid

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) {
Expand All @@ -76,10 +82,10 @@ export const SetPhoneNumberWithoutValidation = () => {
invalidateStepperInfoQuery()
navigateForwardToStepper()
},
() => {
setError('phoneNumber', { message: 'Une erreur est survenue' })
}
)
onError: (error) => {
isApiError(error) && setError('phoneNumber', { message: error.message })
},
})

const onSubmit = async ({ phoneNumber, countryId }: FormValues) => {
const country = findCountry(countryId)
Expand Down Expand Up @@ -117,7 +123,7 @@ export const SetPhoneNumberWithoutValidation = () => {
<TextInput
autoComplete="off" // disable autofill on android
autoCapitalize="none"
isError={false}
isError={!!fieldState.error}
keyboardType="number-pad"
label="Numéro de téléphone"
value={field.value}
Expand Down Expand Up @@ -157,7 +163,7 @@ export const SetPhoneNumberWithoutValidation = () => {
}
fixedBottomChildren={
<ButtonPrimary
disabled={disableSubmit}
disabled={!formState.isValid}
type="submit"
wording="Continuer"
onPress={submit}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
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 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) {
if (!isCountryCode(country)) return false

const phoneNumber = parsePhoneNumberFromString(number, country)
return phoneNumber?.isValid() ?? false
}
14 changes: 8 additions & 6 deletions src/features/profile/api/useUpdateProfileMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ 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) {
client.setQueryData([QueryKeys.USER_PROFILE], (old: UserProfileResponse | undefined) => ({
...(old ?? {}),
...response,
}))
onSuccessCallback(response, variables)
onSuccess?.(response, variables)
},
onError: onErrorCallback,
onError,
})
}
10 changes: 5 additions & 5 deletions src/features/profile/pages/ChangeCity/ChangeCity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const useSubmitChangeStatus = () => {
const { user } = useAuthContext()
const { navigate } = useNavigation<UseNavigationType>()
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 ?? '',
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/features/subscription/helpers/useThematicSubscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit d220ba3

Please sign in to comment.