Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PC-32700) feat(NC): add New Caledonian phone number check in SetPhoneNumber #7266

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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