diff --git a/__snapshots__/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx.native-snap b/__snapshots__/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx.native-snap
index 2c3b2104c20..158dd34ead5 100644
--- a/__snapshots__/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx.native-snap
+++ b/__snapshots__/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx.native-snap
@@ -377,83 +377,80 @@ exports[` should render modal correctly after animation and with en
}
>
-
-
- undefined-SVG-Mock
-
-
-
-
-
- Il te reste 70 € sur ton pass Culture.
+
+ undefined-SVG-Mock
+ >
+
+ Il te reste 70 € sur ton pass Culture.
+
+
+
should render modal correctly after animation and with en
-
-
should render modal correctly after animation and with en
-
-
', () => {
+ it('should not show error when form input is valid', () => {
+ renderPriceInputController({})
+
+ expect(screen.queryByText('error')).not.toBeOnTheScreen()
+ })
+
+ it('should show error when form input is invalid', () => {
+ renderPriceInputController({
+ error: { type: 'custom', message: 'error' },
+ })
+
+ expect(screen.getByText('error')).toBeOnTheScreen()
+ })
+
+ it('should display custom error message when error is set', async () => {
+ renderPriceInputController({
+ error: { type: 'custom', message: 'Prix invalide' },
+ })
+
+ const input = screen.getByPlaceholderText('Prix')
+ await user.type(input, 'abc')
+
+ expect(screen.getByText('Prix invalide')).toBeOnTheScreen()
+ })
+})
+
+const renderPriceInputController = ({
+ error,
+ isDisabled,
+}: {
+ error?: ErrorOption
+ isDisabled?: boolean
+}) => {
+ const PriceForm = () => {
+ const { control, setError } = useForm({
+ defaultValues: { price: '' },
+ })
+
+ error && setError('price', error)
+ return (
+
+ )
+ }
+ render()
+}
diff --git a/src/features/search/components/PriceInputController/PriceInputController.tsx b/src/features/search/components/PriceInputController/PriceInputController.tsx
new file mode 100644
index 00000000000..b47e0bac266
--- /dev/null
+++ b/src/features/search/components/PriceInputController/PriceInputController.tsx
@@ -0,0 +1,64 @@
+import React, { PropsWithChildren, ReactElement } from 'react'
+import { Control, Controller, FieldPath, FieldValues } from 'react-hook-form'
+
+import { InputError } from 'ui/components/inputs/InputError'
+import { TextInput } from 'ui/components/inputs/TextInput'
+import { getSpacing } from 'ui/theme/spacing'
+
+interface Props
+ extends Omit, 'value' | 'onChangeText'> {
+ name: TName
+ control: Control
+ label: string
+ placeholder?: string
+ rightLabel?: string
+ isDisabled?: boolean
+ accessibilityId: string
+}
+
+export const PriceInputController = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ name,
+ control,
+ label,
+ placeholder,
+ rightLabel,
+ isDisabled,
+ accessibilityId,
+ ...textInputProps
+}: PropsWithChildren>): ReactElement => {
+ return (
+ (
+
+ 0}
+ textContentType="none"
+ accessibilityDescribedBy={accessibilityId}
+ keyboardType="numeric"
+ autoComplete="off"
+ autoCapitalize="none"
+ {...textInputProps}
+ />
+
+
+ )}
+ />
+ )
+}
diff --git a/src/features/search/components/sections/Price/Price.native.test.tsx b/src/features/search/components/sections/Price/Price.native.test.tsx
index ac976bf5097..ad37e176633 100644
--- a/src/features/search/components/sections/Price/Price.native.test.tsx
+++ b/src/features/search/components/sections/Price/Price.native.test.tsx
@@ -2,8 +2,10 @@ import React from 'react'
import { Price } from 'features/search/components/sections/Price/Price'
import { initialSearchState } from 'features/search/context/reducer'
+import { DEFAULT_PACIFIC_FRANC_TO_EURO_RATE } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
import { setFeatureFlags } from 'libs/firebase/firestore/featureFlags/__tests__/setFeatureFlags'
-import { fireEvent, render, screen } from 'tests/utils'
+import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
+import { render, screen, userEvent } from 'tests/utils'
let mockSearchState = initialSearchState
@@ -26,6 +28,14 @@ jest.mock('react-native/Libraries/Animated/createAnimatedComponent', () => {
}
})
+const user = userEvent.setup()
+jest.useFakeTimers()
+
+const props = {
+ currency: Currency.EURO,
+ euroToPacificFrancRate: DEFAULT_PACIFIC_FRANC_TO_EURO_RATE,
+}
+
describe('Price component', () => {
beforeEach(() => {
setFeatureFlags()
@@ -33,7 +43,7 @@ describe('Price component', () => {
it('should display the search price description when minimum price selected', async () => {
mockSearchState = { ...initialSearchState, minPrice: '5' }
- render()
+ render()
await screen.findByText('Prix')
@@ -42,7 +52,7 @@ describe('Price component', () => {
it('should display the search price description when maximum price selected', async () => {
mockSearchState = { ...initialSearchState, maxPrice: '10' }
- render()
+ render()
await screen.findByText('Prix')
@@ -51,7 +61,7 @@ describe('Price component', () => {
it('should display the search price description when minimum and maximum prices selected', async () => {
mockSearchState = { ...initialSearchState, minPrice: '5', maxPrice: '10' }
- render()
+ render()
await screen.findByText('Prix')
@@ -60,7 +70,7 @@ describe('Price component', () => {
it('should display the search price description with "Gratuit" when minimum and maximum prices selected and are 0', async () => {
mockSearchState = { ...initialSearchState, minPrice: '0', maxPrice: '0' }
- render()
+ render()
await screen.findByText('Prix')
@@ -68,12 +78,12 @@ describe('Price component', () => {
})
it('should open the categories filter modal when clicking on the category button', async () => {
- render(, {
+ render(, {
theme: { isDesktopViewport: false, isMobileViewport: true },
})
- const searchPriceButton = await screen.findByTestId('FilterRow')
- fireEvent.press(searchPriceButton)
+ const searchPriceButton = await screen.findByTestId('FilterRow')
+ await user.press(searchPriceButton)
const fullscreenModalScrollView = screen.getByTestId('fullscreenModalScrollView')
diff --git a/src/features/search/components/sections/Price/Price.tsx b/src/features/search/components/sections/Price/Price.tsx
index add14999afa..56596b198ad 100644
--- a/src/features/search/components/sections/Price/Price.tsx
+++ b/src/features/search/components/sections/Price/Price.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react'
+import React, { useCallback, useMemo } from 'react'
import { FilterRow } from 'features/search/components/FilterRow/FilterRow'
import { useSearch } from 'features/search/context/SearchWrapper'
@@ -6,20 +6,19 @@ import { FilterBehaviour } from 'features/search/enums'
import { getPriceAsNumber } from 'features/search/helpers/getPriceAsNumber/getPriceAsNumber'
import { getPriceDescription } from 'features/search/helpers/getPriceDescription/getPriceDescription'
import { PriceModal } from 'features/search/pages/modals/PriceModal/PriceModal'
-import { useGetPacificFrancToEuroRate } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
-import { useGetCurrencyToDisplay } from 'shared/currency/useGetCurrencyToDisplay'
+import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
import { useModal } from 'ui/components/modals/useModal'
import { Code } from 'ui/svg/icons/Code'
type Props = {
onClose?: VoidFunction
+ currency: Currency
+ euroToPacificFrancRate: number
}
-export const Price = ({ onClose }: Props) => {
- const currency = useGetCurrencyToDisplay()
- const euroToPacificFrancRate = useGetPacificFrancToEuroRate()
-
+export const Price = ({ onClose, currency, euroToPacificFrancRate }: Props) => {
const { searchState } = useSearch()
+
const {
visible: searchPriceModalVisible,
showModal: showSearchPriceModal,
@@ -33,14 +32,14 @@ export const Price = ({ onClose }: Props) => {
showSearchPriceModal()
}, [showSearchPriceModal])
+ const description = useMemo(
+ () => getPriceDescription(currency, euroToPacificFrancRate, minPrice, maxPrice),
+ [currency, euroToPacificFrancRate, maxPrice, minPrice]
+ )
+
return (
-
+
{
- describe('should match', () => {
- it('a valid integer price', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123')
-
- expect(result).toEqual('123')
- })
-
- it('a valid price with a dot', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123.45')
-
- expect(result).toEqual('123.45')
- })
-
- it('a valid price with a comma', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123,45')
-
- expect(result).toEqual('123,45')
- })
-
- it('a valid price with only one decimal', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123,4')
-
- expect(result).toEqual('123,4')
- })
-
- it('a valid price between spaces', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(' 123,4 ')
-
- expect(result).toEqual('123,4')
- })
-
- it('a valid price with a dot but no decimal', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123.')
-
- expect(result).toEqual('123.')
- })
-
- it('when input less than the initial credit', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('200')
-
- expect(result).toEqual('200')
- })
-
- it('when input equal to the initial credit', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(convertCentsToEuros(MAX_PRICE_IN_CENTS).toString())
-
- expect(result).toEqual(convertCentsToEuros(MAX_PRICE_IN_CENTS).toString())
- })
-
- it('when input is undefined', async () => {
- const result = await makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(undefined)
-
- expect(result).toEqual(undefined)
- })
- })
-
- describe('should invalidate an input', () => {
- const formatErrorMessage = `Format du prix incorrect. Exemple de format attendu\u00a0: 10,00`
- const maxPriceErrorMessage = `Le prix indiqué ne doit pas dépasser ${convertCentsToEuros(MAX_PRICE_IN_CENTS)}\u00a0€`
-
- it('that is not a number', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('azerty')
-
- await expect(result).rejects.toEqual(new ValidationError(formatErrorMessage))
- })
-
- it('that is a number with more than 2 decimals', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123.456')
-
- await expect(result).rejects.toEqual(new ValidationError(formatErrorMessage))
- })
-
- it('that is a number with more than 2 comma or dot', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('1,23.45')
-
- await expect(result).rejects.toEqual(new ValidationError(formatErrorMessage))
- })
-
- it('that is a number with negative number', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('-123.45')
-
- await expect(result).rejects.toEqual(new ValidationError(formatErrorMessage))
- })
-
- it('with euro symbol', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('123.45€')
-
- await expect(result).rejects.toEqual(new ValidationError(formatErrorMessage))
- })
-
- it('when input higher than the initial credit', async () => {
- const result = makePriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate('400')
-
- await expect(result).rejects.toEqual(new ValidationError(maxPriceErrorMessage))
- })
- })
-})
diff --git a/src/features/search/helpers/schema/makePriceSchema/makePriceSchema.ts b/src/features/search/helpers/schema/makePriceSchema/makePriceSchema.ts
deleted file mode 100644
index 86346a7ae4a..00000000000
--- a/src/features/search/helpers/schema/makePriceSchema/makePriceSchema.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { string } from 'yup'
-
-import { formatCurrencyFromCents } from 'libs/parsers/formatCurrencyFromCents'
-import { convertEuroToCents } from 'libs/parsers/pricesConversion'
-import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
-
-// integers separated by a dot or comma with 2 decimals max
-const PRICE_REGEX = /^\d+(?:[,.]\d{0,2})?$/
-const formatPriceError = `Format du prix incorrect. Exemple de format attendu\u00a0: 10,00`
-
-const maxPriceError = (
- initialCreditInEuro: string,
- currency: Currency,
- euroToPacificFrancRate: number
-) => {
- const initialCreditInCents = convertEuroToCents(Number(initialCreditInEuro))
- const maxPrice = formatCurrencyFromCents(initialCreditInCents, currency, euroToPacificFrancRate)
- return `Le prix indiqué ne doit pas dépasser ${maxPrice}`
-}
-
-export const makePriceSchema = (
- initialCreditInEuro: string,
- currency: Currency,
- euroToPacificFrancRate: number
-) =>
- string()
- .trim()
- .test('validPrice', formatPriceError, (value) => {
- if (!value) return true
- return PRICE_REGEX.test(value.trim())
- })
- .test(
- 'validMaxPrice',
- maxPriceError(initialCreditInEuro, currency, euroToPacificFrancRate),
- (value) => {
- if (!value) return true
- return Number(value.trim().replaceAll(',', '.')) <= Number(initialCreditInEuro)
- }
- )
diff --git a/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.native.test.ts b/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.native.test.ts
deleted file mode 100644
index 31300bffaad..00000000000
--- a/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.native.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { ValidationError } from 'yup'
-
-import { MAX_PRICE_IN_CENTS } from 'features/search/helpers/reducer.helpers'
-import {
- minPriceError,
- makeSearchPriceSchema,
-} from 'features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema'
-import { DEFAULT_PACIFIC_FRANC_TO_EURO_RATE } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
-import { convertCentsToEuros } from 'libs/parsers/pricesConversion'
-import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
-
-describe('search price schema', () => {
- const initialValues = {
- isLimitCreditSearch: false,
- isOnlyFreeOffersSearch: false,
- }
-
- describe('should match minimum price', () => {
- it('when input less than maximum price input', async () => {
- const values = { ...initialValues, maxPrice: '20', minPrice: '10' }
- const result = await makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- expect(result).toEqual(values)
- })
-
- it('when input equal than maximum price input', async () => {
- const values = { ...initialValues, maxPrice: '20', minPrice: '20' }
- const result = await makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- expect(result).toEqual(values)
- })
-
- it('when input less than decimal maximum price input', async () => {
- const values = { ...initialValues, maxPrice: '20,15', minPrice: '10' }
- const result = await makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- expect(result).toEqual(values)
- })
-
- it('when input equal than decimal maximum price input', async () => {
- const values = { ...initialValues, maxPrice: '20,15', minPrice: '20,15' }
- const result = await makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- expect(result).toEqual(values)
- })
- })
-
- it('should match decimal minimum price when input less than maximum price input', async () => {
- const values = { ...initialValues, maxPrice: '20', minPrice: '10,50' }
- const result = await makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- expect(result).toEqual(values)
- })
-
- it('should invalidate minimum price when input higher than maximum price', async () => {
- const values = { ...initialValues, maxPrice: '20', minPrice: '21' }
- const result = makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- await expect(result).rejects.toEqual(new ValidationError(minPriceError))
- })
-
- it('should invalidate minimum price when input higher than decimal maximum price', async () => {
- const values = { ...initialValues, maxPrice: '20,15', minPrice: '21' }
- const result = makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- await expect(result).rejects.toEqual(new ValidationError(minPriceError))
- })
-
- it('should invalidate decimal minimum price when input higher than maximum price', async () => {
- const values = { ...initialValues, maxPrice: '20', minPrice: '21,15' }
- const result = makeSearchPriceSchema(
- convertCentsToEuros(MAX_PRICE_IN_CENTS).toString(),
- Currency.EURO,
- DEFAULT_PACIFIC_FRANC_TO_EURO_RATE
- ).validate(values)
-
- await expect(result).rejects.toEqual(new ValidationError(minPriceError))
- })
-})
diff --git a/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.ts b/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.ts
deleted file mode 100644
index dc15a9fdeb2..00000000000
--- a/src/features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { boolean, object } from 'yup'
-
-import { makePriceSchema } from 'features/search/helpers/schema/makePriceSchema/makePriceSchema'
-import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
-
-export const minPriceError = `Le montant minimum ne peut pas dépasser le montant maximum`
-
-export const makeSearchPriceSchema = (
- initialCredit: string,
- currency: Currency,
- euroToPacificFrancRate: number
-) =>
- object().shape({
- minPrice: makePriceSchema(initialCredit.toString(), currency, euroToPacificFrancRate).when(
- ['maxPrice'],
- {
- is: (maxPrice: string) => maxPrice.length > 0,
- then: (schema) =>
- schema.test('validMinPrice', minPriceError, (value, schema) => {
- if (!value) return true
- return (
- Number(value.trim().replaceAll(',', '.')) <=
- Number(schema.parent.maxPrice.trim().replaceAll(',', '.'))
- )
- }),
- }
- ),
- maxPrice: makePriceSchema(String(initialCredit), currency, euroToPacificFrancRate),
- isLimitCreditSearch: boolean(),
- isOnlyFreeOffersSearch: boolean(),
- })
diff --git a/src/features/search/helpers/schema/priceSchema/priceSchema.native.test.ts b/src/features/search/helpers/schema/priceSchema/priceSchema.native.test.ts
new file mode 100644
index 00000000000..38d1e2d5966
--- /dev/null
+++ b/src/features/search/helpers/schema/priceSchema/priceSchema.native.test.ts
@@ -0,0 +1,184 @@
+import { ValidationError } from 'yup'
+
+import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
+
+import { priceSchema } from './priceSchema'
+
+describe('priceSchema', () => {
+ const initialCredit = 100
+ const currency = Currency.EURO
+
+ test('should validate correctly when minPrice and maxPrice are within range', async () => {
+ const validData = {
+ minPrice: '10,00',
+ maxPrice: '50,00',
+ isLimitCreditSearch: true,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(validData)).resolves.toBe(true)
+ })
+
+ test('should invalidate when maxPrice exceeds initialCredit', async () => {
+ const invalidData = {
+ minPrice: '10,00',
+ maxPrice: '150,00',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(invalidData)).resolves.toBe(false)
+ })
+
+ test('should invalidate when minPrice is greater than maxPrice', async () => {
+ const invalidData = {
+ minPrice: '60,00',
+ maxPrice: '50,00',
+ isLimitCreditSearch: true,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(invalidData)).resolves.toBe(false)
+ })
+
+ test('should validate when maxPrice is empty and minPrice is within range', async () => {
+ const validData = {
+ minPrice: '10,00',
+ maxPrice: '',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(validData)).resolves.toBe(true)
+ })
+
+ test('should invalidate when minPrice has incorrect format', async () => {
+ const invalidData = {
+ minPrice: '10,000',
+ maxPrice: '50,00',
+ isLimitCreditSearch: true,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(invalidData)).resolves.toBe(false)
+ })
+
+ test('should invalidate when maxPrice has incorrect format', async () => {
+ const invalidData = {
+ minPrice: '10,00',
+ maxPrice: '50.000',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).isValid(invalidData)).resolves.toBe(false)
+ })
+
+ describe('should fail', () => {
+ it('when maxPrice exceeds initialCredit', async () => {
+ const invalidData = {
+ minPrice: '10,00',
+ maxPrice: '150,00',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(invalidData)).rejects.toEqual(
+ new ValidationError('Le prix indiqué ne doit pas dépasser 100\u00a0€')
+ )
+ })
+
+ it('when maxPrice exceeds initialCredit with other currency', async () => {
+ const invalidData = {
+ minPrice: '10,00',
+ maxPrice: '150,00',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(
+ priceSchema({ initialCredit, currency: Currency.PACIFIC_FRANC_SHORT }).validate(invalidData)
+ ).rejects.toEqual(new ValidationError('Le prix indiqué ne doit pas dépasser 100\u00a0F'))
+ })
+
+ it('when minPrice is greater than maxPrice', async () => {
+ const invalidData = {
+ minPrice: '50,00',
+ maxPrice: '40,00',
+ isLimitCreditSearch: true,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(invalidData)).rejects.toEqual(
+ new ValidationError('Le montant minimum ne peut pas dépasser le montant maximum')
+ )
+ })
+
+ it('when maxPrice has an invalid format', async () => {
+ const invalidData = {
+ minPrice: '10,00',
+ maxPrice: '150.000',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(invalidData)).rejects.toEqual(
+ new ValidationError('Format du prix incorrect. Exemple de format attendu\u00a0: 10,00')
+ )
+ })
+
+ it('when minPrice has an invalid format', async () => {
+ const invalidData = {
+ minPrice: '10,000',
+ maxPrice: '50,00',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(invalidData)).rejects.toEqual(
+ new ValidationError('Format du prix incorrect. Exemple de format attendu\u00a0: 10,00')
+ )
+ })
+ })
+
+ describe('should validate', () => {
+ it('when minPrice and maxPrice are within range', async () => {
+ const validData = {
+ minPrice: '10,00',
+ maxPrice: '50,00',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(validData)).resolves.toEqual(
+ validData
+ )
+ })
+
+ it('when maxPrice is empty and minPrice is within range', async () => {
+ const validData = {
+ minPrice: '10,00',
+ maxPrice: '',
+ isLimitCreditSearch: false,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(validData)).resolves.toEqual(
+ validData
+ )
+ })
+
+ it('when both minPrice and maxPrice are empty', async () => {
+ const validData = {
+ minPrice: '',
+ maxPrice: '',
+ isLimitCreditSearch: true,
+ isOnlyFreeOffersSearch: false,
+ }
+
+ await expect(priceSchema({ initialCredit, currency }).validate(validData)).resolves.toEqual(
+ validData
+ )
+ })
+ })
+})
diff --git a/src/features/search/helpers/schema/priceSchema/priceSchema.ts b/src/features/search/helpers/schema/priceSchema/priceSchema.ts
new file mode 100644
index 00000000000..5d7f3ef2065
--- /dev/null
+++ b/src/features/search/helpers/schema/priceSchema/priceSchema.ts
@@ -0,0 +1,48 @@
+import { boolean, object, string } from 'yup'
+
+import { Currency } from 'shared/currency/useGetCurrencyToDisplay'
+
+type PriceValidationParams = {
+ initialCredit: number
+ currency: Currency
+}
+
+const PRICE_REGEX = /^\d+(?:[,.]\d{0,2})?$/
+const formatPriceError = `Format du prix incorrect. Exemple de format attendu\u00a0: 10,00`
+const minPriceError = `Le montant minimum ne peut pas dépasser le montant maximum`
+
+const maxPriceError = ({ initialCredit, currency }: PriceValidationParams) => {
+ return `Le prix indiqué ne doit pas dépasser ${initialCredit}\u00a0${currency}`
+}
+
+const maxPriceValidationSchema = ({ initialCredit, currency }: PriceValidationParams) =>
+ string()
+ .trim()
+ .test('validPrice', formatPriceError, (value) => {
+ if (!value) return true
+ return PRICE_REGEX.test(value.trim())
+ })
+ .test('validMaxPrice', maxPriceError({ initialCredit, currency }), (value) => {
+ if (!value) return true
+ return Number(value.trim().replaceAll(',', '.')) <= Number(initialCredit)
+ })
+
+const minPriceWithConditions = ({ initialCredit, currency }: PriceValidationParams) =>
+ maxPriceValidationSchema({ initialCredit, currency }).when(['maxPrice'], {
+ is: (maxPrice: string) => !!maxPrice && maxPrice.trim().length > 0, // Vérifie si maxPrice est non vide
+ then: (schema) =>
+ schema.test('validMinPrice', minPriceError, (value, schema) => {
+ if (!value) return true
+ const minPrice = Number(value.trim().replace(',', '.'))
+ const maxPrice = Number(schema.parent.maxPrice.trim().replace(',', '.'))
+ return minPrice <= maxPrice
+ }),
+ })
+
+export const priceSchema = ({ initialCredit, currency }: PriceValidationParams) =>
+ object().shape({
+ minPrice: minPriceWithConditions({ initialCredit, currency }),
+ maxPrice: maxPriceValidationSchema({ initialCredit, currency }),
+ isLimitCreditSearch: boolean(),
+ isOnlyFreeOffersSearch: boolean(),
+ })
diff --git a/src/features/search/pages/SearchFilter/SearchFilter.tsx b/src/features/search/pages/SearchFilter/SearchFilter.tsx
index 8145994b6b6..40d0013e25b 100644
--- a/src/features/search/pages/SearchFilter/SearchFilter.tsx
+++ b/src/features/search/pages/SearchFilter/SearchFilter.tsx
@@ -16,16 +16,21 @@ import { useNavigateToSearch } from 'features/search/helpers/useNavigateToSearch
import { useSync } from 'features/search/helpers/useSync/useSync'
import { LocationFilter } from 'features/search/types'
import { analytics } from 'libs/analytics'
+import { useGetPacificFrancToEuroRate } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
import { useFunctionOnce } from 'libs/hooks'
import { useLocation } from 'libs/location'
import { LocationMode } from 'libs/location/types'
import { SuggestedPlace } from 'libs/place/types'
+import { useGetCurrencyToDisplay } from 'shared/currency/useGetCurrencyToDisplay'
import { Li } from 'ui/components/Li'
import { VerticalUl } from 'ui/components/Ul'
import { SecondaryPageWithBlurHeader } from 'ui/pages/SecondaryPageWithBlurHeader'
import { Spacer } from 'ui/theme'
export const SearchFilter: React.FC = () => {
+ const currency = useGetCurrencyToDisplay()
+ const euroToPacificFrancRate = useGetPacificFrancToEuroRate()
+
const { disabilities, setDisabilities } = useAccessibilityFiltersContext()
const routes = useNavigationState((state) => state?.routes)
const currentRoute = routes?.[routes?.length - 1]?.name
@@ -45,17 +50,13 @@ export const SearchFilter: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const oldSearchState = useMemo(() => searchState, [])
+
const onGoBack = useCallback(() => {
navigateToSearch(oldSearchState, oldAccessibilityFilter)
}, [navigateToSearch, oldSearchState, oldAccessibilityFilter])
const onSearchPress = useCallback(() => {
- navigateToSearch(
- {
- ...searchState,
- },
- disabilities
- )
+ navigateToSearch({ ...searchState }, disabilities)
}, [navigateToSearch, searchState, disabilities])
const onResetPress = useCallback(() => {
@@ -119,14 +120,7 @@ export const SearchFilter: React.FC = () => {
return isBeneficiary && hasRemainingCredit
}, [user?.isBeneficiary, user?.domainsCredit?.all?.remaining])
- const shouldDisplayCloseButton = isMobileViewport
-
- const sectionItems = [Section.Category]
- if (hasDuoOfferToggle) sectionItems.push(Section.OfferDuo)
- sectionItems.push(Section.Venue)
- sectionItems.push(Section.Price)
- sectionItems.push(Section.DateHour)
- sectionItems.push(Section.Accessibility)
+ const onClose = isMobileViewport ? onGoBack : undefined
return (
@@ -135,13 +129,30 @@ export const SearchFilter: React.FC = () => {
onGoBack={onGoBack}
scrollViewProps={{ keyboardShouldPersistTaps: 'always' }}>
- {sectionItems.map((SectionItem, index) => {
- return (
-
-
-
- )
- })}
+
+
+
+ {hasDuoOfferToggle ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
@@ -157,7 +168,7 @@ export const SearchFilter: React.FC = () => {
const SectionWrapper: React.FunctionComponent<{
children: React.JSX.Element
- isFirstSectionItem: boolean
+ isFirstSectionItem?: boolean
}> = ({ children, isFirstSectionItem = false }) => {
return (
diff --git a/src/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx b/src/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx
index efe0f1f40f4..eb878c5811d 100644
--- a/src/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx
+++ b/src/features/search/pages/modals/PriceModal/PriceModal.native.test.tsx
@@ -9,7 +9,7 @@ import { SearchState } from 'features/search/types'
import { beneficiaryUser } from 'fixtures/user'
import { setFeatureFlags } from 'libs/firebase/firestore/featureFlags/__tests__/setFeatureFlags'
import { convertCentsToEuros } from 'libs/parsers/pricesConversion'
-import { fireEvent, render, act, waitFor, screen } from 'tests/utils'
+import { render, waitFor, screen, userEvent } from 'tests/utils'
import { PriceModal, PriceModalProps } from './PriceModal'
@@ -48,6 +48,9 @@ jest.mock('react-native/Libraries/Animated/createAnimatedComponent', () => {
}
})
+const user = userEvent.setup()
+jest.useFakeTimers()
+
describe('', () => {
beforeEach(() => {
setFeatureFlags()
@@ -79,10 +82,10 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- fireEvent.changeText(minPriceInput, '5')
+ await user.type(minPriceInput, '5')
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
expect(minPriceInput.props.value).toStrictEqual('')
})
@@ -91,10 +94,10 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- fireEvent.changeText(maxPriceInput, '20')
+ await user.type(maxPriceInput, '20')
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
expect(maxPriceInput.props.value).toStrictEqual('')
})
@@ -103,7 +106,7 @@ describe('', () => {
renderSearchPrice()
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
@@ -114,7 +117,7 @@ describe('', () => {
renderSearchPrice()
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
@@ -130,7 +133,7 @@ describe('', () => {
const minPriceInput = screen.getByPlaceholderText('0')
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
expect(minPriceInput.props.value).toStrictEqual('')
})
@@ -142,7 +145,7 @@ describe('', () => {
const minPriceInput = screen.getByPlaceholderText('0')
const previousButton = screen.getByTestId('Fermer')
- await act(async () => fireEvent.press(previousButton))
+ await user.press(previousButton)
expect(minPriceInput.props.value).toStrictEqual('5')
})
@@ -154,7 +157,7 @@ describe('', () => {
const maxPriceInput = screen.getByPlaceholderText('80')
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
expect(maxPriceInput.props.value).toStrictEqual('')
})
@@ -166,7 +169,7 @@ describe('', () => {
const maxPriceInput = screen.getByPlaceholderText('80')
const previousButton = screen.getByTestId('Fermer')
- await act(async () => fireEvent.press(previousButton))
+ await user.press(previousButton)
expect(maxPriceInput.props.value).toStrictEqual('15')
})
@@ -176,7 +179,7 @@ describe('', () => {
renderSearchPrice()
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
@@ -188,7 +191,7 @@ describe('', () => {
renderSearchPrice()
const previousButton = screen.getByTestId('Fermer')
- await act(async () => fireEvent.press(previousButton))
+ await user.press(previousButton)
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
@@ -200,7 +203,7 @@ describe('', () => {
renderSearchPrice()
const resetButton = screen.getByText('Réinitialiser')
- await act(async () => fireEvent.press(resetButton))
+ await user.press(resetButton)
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
@@ -212,7 +215,7 @@ describe('', () => {
renderSearchPrice()
const previousButton = screen.getByTestId('Fermer')
- await act(async () => fireEvent.press(previousButton))
+ await user.press(previousButton)
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
@@ -224,7 +227,7 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -235,7 +238,7 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -246,8 +249,8 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- fireEvent.press(toggleLimitCreditSearch)
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
+ await user.press(toggleLimitCreditSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -259,7 +262,7 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -271,8 +274,8 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- fireEvent.press(toggleLimitCreditSearch)
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
+ await user.press(toggleLimitCreditSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -283,12 +286,12 @@ describe('', () => {
renderSearchPrice()
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
expect(toggleLimitCreditSearch.props.accessibilityState.checked).toStrictEqual(true)
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(toggleLimitCreditSearch.props.accessibilityState.checked).toStrictEqual(false)
})
@@ -297,12 +300,12 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(toggleOnlyFreeOffersSearch.props.accessibilityState.checked).toStrictEqual(true)
const toggleLimitCreditSearch = screen.getByTestId('Interrupteur limitCreditSearch')
- await act(async () => fireEvent.press(toggleLimitCreditSearch))
+ await user.press(toggleLimitCreditSearch)
expect(toggleOnlyFreeOffersSearch.props.accessibilityState.checked).toStrictEqual(false)
})
@@ -311,10 +314,10 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- fireEvent.changeText(minPriceInput, '5')
+ await user.type(minPriceInput, '5')
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(minPriceInput.props.value).toStrictEqual('0')
})
@@ -324,8 +327,8 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- fireEvent.press(toggleOnlyFreeOffersSearch)
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
+ await user.press(toggleOnlyFreeOffersSearch)
const minPriceInput = screen.getByPlaceholderText('0')
@@ -337,8 +340,8 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- fireEvent.press(toggleOnlyFreeOffersSearch)
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
+ await user.press(toggleOnlyFreeOffersSearch)
const minPriceInput = screen.getByPlaceholderText('0')
@@ -350,8 +353,8 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- fireEvent.press(toggleOnlyFreeOffersSearch)
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
+ await user.press(toggleOnlyFreeOffersSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -363,8 +366,8 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- fireEvent.press(toggleOnlyFreeOffersSearch)
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
+ await user.press(toggleOnlyFreeOffersSearch)
const maxPriceInput = screen.getByPlaceholderText('80')
@@ -377,7 +380,7 @@ describe('', () => {
const minPriceInput = screen.getByPlaceholderText('0')
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(minPriceInput.props.disabled).toStrictEqual(true)
})
@@ -386,10 +389,10 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- fireEvent.changeText(maxPriceInput, '0')
+ await user.type(maxPriceInput, '5')
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(maxPriceInput.props.value).toStrictEqual('0')
})
@@ -400,7 +403,7 @@ describe('', () => {
const maxPriceInput = screen.getByPlaceholderText('80')
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
expect(maxPriceInput.props.disabled).toStrictEqual(true)
})
@@ -423,7 +426,7 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- await act(async () => fireEvent.changeText(minPriceInput, '10,559'))
+ await user.type(minPriceInput, '10,559')
const inputError = screen.getByText(
`Format du prix incorrect. Exemple de format attendu\u00a0: 10,00`
@@ -436,7 +439,7 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- await act(async () => fireEvent.changeText(maxPriceInput, '10,559'))
+ await user.type(maxPriceInput, '10,559')
const inputError = screen.getByText(
`Format du prix incorrect. Exemple de format attendu\u00a0: 10,00`
@@ -479,7 +482,7 @@ describe('', () => {
expect(searchButton).toBeEnabled()
})
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
expect(mockHideModal).toHaveBeenCalledTimes(1)
})
@@ -492,7 +495,7 @@ describe('', () => {
})
const previousButton = screen.getByTestId('Fermer')
- fireEvent.press(previousButton)
+ await user.press(previousButton)
expect(mockHideModal).toHaveBeenCalledTimes(1)
})
@@ -504,11 +507,11 @@ describe('', () => {
const minPriceInput = screen.getByPlaceholderText('0')
const onlyFreeOffersToggle = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.changeText(minPriceInput, '9999'))
+ await user.type(minPriceInput, '9999')
expect(screen.getByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')).toBeOnTheScreen()
- await act(async () => fireEvent.press(onlyFreeOffersToggle))
+ await user.press(onlyFreeOffersToggle)
expect(
screen.queryByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')
@@ -519,13 +522,12 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- const onlyFreeOffersToggle = screen.getByTestId('Interrupteur onlyFreeOffers')
-
- await act(async () => fireEvent.changeText(maxPriceInput, '9999'))
+ await user.type(maxPriceInput, '9999')
expect(screen.getByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')).toBeOnTheScreen()
- await act(async () => fireEvent.press(onlyFreeOffersToggle))
+ const onlyFreeOffersToggle = screen.getByTestId('Interrupteur onlyFreeOffers')
+ await user.press(onlyFreeOffersToggle)
expect(
screen.queryByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')
@@ -536,13 +538,12 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- const limitCreditSearchToggle = screen.getByTestId('Interrupteur limitCreditSearch')
-
- await act(async () => fireEvent.changeText(maxPriceInput, '9999'))
+ await user.type(maxPriceInput, '9999')
expect(screen.getByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')).toBeOnTheScreen()
- await act(async () => fireEvent.press(limitCreditSearchToggle))
+ const limitCreditSearchToggle = screen.getByTestId('Interrupteur limitCreditSearch')
+ await user.press(limitCreditSearchToggle)
expect(
screen.queryByText('Le prix indiqué ne doit pas dépasser 80\u00a0€')
@@ -686,17 +687,17 @@ describe('', () => {
})
const maxPriceInput = screen.getByPlaceholderText('80')
- fireEvent.changeText(maxPriceInput, '50')
+ await user.type(maxPriceInput, '50')
- await waitFor(() => {
- expect(screen.getByText('Appliquer le filtre')).toBeEnabled()
- })
+ expect(screen.getByText('Appliquer le filtre')).toBeEnabled()
const searchButton = screen.getByText('Appliquer le filtre')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams: SearchState = {
...searchState,
+ defaultMinPrice: '',
+ defaultMaxPrice: '50',
maxPrice: '50',
}
@@ -713,18 +714,20 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- await act(async () => fireEvent.changeText(minPriceInput, '5'))
+ await user.type(minPriceInput, '5')
const maxPriceInput = screen.getByPlaceholderText('80')
- await act(async () => fireEvent.changeText(maxPriceInput, '20'))
+ await user.type(maxPriceInput, '20')
const searchButton = screen.getByText('Rechercher')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams = {
...mockSearchState,
minPrice: '5',
maxPrice: '20',
+ defaultMinPrice: '5',
+ defaultMaxPrice: '20',
}
expect(mockDispatch).toHaveBeenCalledWith({
@@ -737,18 +740,20 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- await act(async () => fireEvent.changeText(minPriceInput, '0'))
+ await user.type(minPriceInput, '0')
const maxPriceInput = screen.getByPlaceholderText('80')
- await act(async () => fireEvent.changeText(maxPriceInput, '0'))
+ await user.type(maxPriceInput, '0')
const searchButton = screen.getByText('Rechercher')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams = {
...mockSearchState,
minPrice: '0',
maxPrice: '0',
+ defaultMinPrice: '0',
+ defaultMaxPrice: '0',
offerIsFree: true,
}
@@ -762,16 +767,18 @@ describe('', () => {
renderSearchPrice()
const toggleOnlyFreeOffersSearch = screen.getByTestId('Interrupteur onlyFreeOffers')
- await act(async () => fireEvent.press(toggleOnlyFreeOffersSearch))
+ await user.press(toggleOnlyFreeOffersSearch)
const searchButton = screen.getByText('Rechercher')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams = {
...mockSearchState,
offerIsFree: true,
minPrice: '0',
maxPrice: '0',
+ defaultMinPrice: '0',
+ defaultMaxPrice: '0',
}
expect(mockDispatch).toHaveBeenCalledWith({
@@ -784,15 +791,17 @@ describe('', () => {
renderSearchPrice()
const maxPriceInput = screen.getByPlaceholderText('80')
- await act(async () => fireEvent.changeText(maxPriceInput, '0'))
+ await user.type(maxPriceInput, '0')
const searchButton = screen.getByText('Rechercher')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams = {
...mockSearchState,
offerIsFree: true,
maxPrice: '0',
+ defaultMaxPrice: '0',
+ defaultMinPrice: '',
}
expect(mockDispatch).toHaveBeenCalledWith({
@@ -805,14 +814,18 @@ describe('', () => {
renderSearchPrice()
const minPriceInput = screen.getByPlaceholderText('0')
- await act(async () => fireEvent.changeText(minPriceInput, '1'))
+ await user.type(minPriceInput, '1')
const searchButton = screen.getByText('Rechercher')
- await act(async () => fireEvent.press(searchButton))
+ await user.press(searchButton)
const expectedSearchParams = {
...mockSearchState,
minPrice: '1',
+ maxPrice: undefined,
+ defaultMinPrice: '1',
+ defaultMaxPrice: '',
+ maxPossiblePrice: '80',
}
expect(mockDispatch).toHaveBeenCalledWith({
@@ -835,7 +848,7 @@ describe('', () => {
})
const closeButton = screen.getByTestId('Fermer')
- fireEvent.press(closeButton)
+ await user.press(closeButton)
expect(mockOnClose).toHaveBeenCalledTimes(1)
})
@@ -848,7 +861,7 @@ describe('', () => {
})
const closeButton = screen.getByTestId('Fermer')
- fireEvent.press(closeButton)
+ await user.press(closeButton)
expect(mockOnClose).not.toHaveBeenCalled()
})
diff --git a/src/features/search/pages/modals/PriceModal/PriceModal.tsx b/src/features/search/pages/modals/PriceModal/PriceModal.tsx
index 3a8c4872299..689901c09d1 100644
--- a/src/features/search/pages/modals/PriceModal/PriceModal.tsx
+++ b/src/features/search/pages/modals/PriceModal/PriceModal.tsx
@@ -1,34 +1,35 @@
import { yupResolver } from '@hookform/resolvers/yup'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
-import { View } from 'react-native'
import { useTheme } from 'styled-components'
+import styled from 'styled-components/native'
import { v4 as uuidv4 } from 'uuid'
import { useAuthContext } from 'features/auth/context/AuthContext'
import { FilterSwitchWithLabel } from 'features/search/components/FilterSwitchWithLabel/FilterSwitchWithLabel'
+import { PriceInputController } from 'features/search/components/PriceInputController/PriceInputController'
import { SearchCustomModalHeader } from 'features/search/components/SearchCustomModalHeader'
import { SearchFixedModalBottom } from 'features/search/components/SearchFixedModalBottom'
import { useSearch } from 'features/search/context/SearchWrapper'
import { FilterBehaviour } from 'features/search/enums'
import { MAX_PRICE_IN_CENTS } from 'features/search/helpers/reducer.helpers'
-import { makeSearchPriceSchema } from 'features/search/helpers/schema/makeSearchPriceSchema/makeSearchPriceSchema'
+import { priceSchema } from 'features/search/helpers/schema/priceSchema/priceSchema'
import { SearchState } from 'features/search/types'
-import { useGetPacificFrancToEuroRate } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
-import { useFormatCurrencyFromCents } from 'libs/parsers/formatCurrencyFromCents'
-import { convertCentsToEuros, RoundingMode } from 'libs/parsers/pricesConversion'
-import { useGetCurrencyToDisplay } from 'shared/currency/useGetCurrencyToDisplay'
+import { DEFAULT_PACIFIC_FRANC_TO_EURO_RATE } from 'libs/firebase/firestore/exchangeRates/useGetPacificFrancToEuroRate'
+import {
+ useFormatCurrencyFromCents,
+ useFormatCurrencyFromCentsWithoutCurrenySymbol,
+} from 'libs/parsers/formatCurrencyFromCents'
+import { Currency, useGetCurrencyToDisplay } from 'shared/currency/useGetCurrencyToDisplay'
import { useAvailableCredit } from 'shared/user/useAvailableCredit'
import { InfoBanner } from 'ui/components/banners/InfoBanner'
import { Form } from 'ui/components/Form'
-import { InputError } from 'ui/components/inputs/InputError'
-import { TextInput } from 'ui/components/inputs/TextInput'
import { useForHeightKeyboardEvents } from 'ui/components/keyboard/useKeyboardEvents'
import { AppModal } from 'ui/components/modals/AppModal'
import { Separator } from 'ui/components/Separator'
import { Close } from 'ui/svg/icons/Close'
import { Error } from 'ui/svg/icons/Error'
-import { getSpacing, Spacer } from 'ui/theme'
+import { Spacer, getSpacing } from 'ui/theme'
type PriceModalFormData = {
minPrice: string
@@ -50,6 +51,10 @@ const titleId = uuidv4()
const minPriceInputId = uuidv4()
const maxPriceInputId = uuidv4()
+const getConversionRate = (currency: Currency) => {
+ return currency === Currency.PACIFIC_FRANC_SHORT ? DEFAULT_PACIFIC_FRANC_TO_EURO_RATE : 1
+}
+
export const PriceModal: FunctionComponent = ({
title,
accessibilityLabel,
@@ -58,27 +63,23 @@ export const PriceModal: FunctionComponent = ({
filterBehaviour,
onClose,
}) => {
- const currency = useGetCurrencyToDisplay()
const currencyFull = useGetCurrencyToDisplay('full')
- const euroToPacificFrancRate = useGetPacificFrancToEuroRate()
+ const currency = useGetCurrencyToDisplay()
+ const conversionRate = getConversionRate(currency)
const { searchState, dispatch } = useSearch()
const { isLoggedIn, user } = useAuthContext()
const availableCredit = useAvailableCredit()?.amount ?? 0
- const formatAvailableCredit = convertCentsToEuros(availableCredit, RoundingMode.FLOORED)
+ const formatAvailableCredit = useFormatCurrencyFromCentsWithoutCurrenySymbol(availableCredit)
const formatAvailableCreditWithCurrency = useFormatCurrencyFromCents(availableCredit)
const bannerTitle = `Il te reste ${formatAvailableCreditWithCurrency} sur ton pass Culture.`
const initialCredit = user?.domainsCredit?.all?.initial ?? MAX_PRICE_IN_CENTS
- const formatInitialCredit = convertCentsToEuros(initialCredit, RoundingMode.FLOORED)
+ const formatInitialCredit = useFormatCurrencyFromCentsWithoutCurrenySymbol(initialCredit)
const formatInitialCreditWithCurrency = useFormatCurrencyFromCents(initialCredit)
- const searchPriceSchema = makeSearchPriceSchema(
- String(formatInitialCredit),
- currency,
- euroToPacificFrancRate
- )
+ const searchPriceSchema = priceSchema({ initialCredit: formatInitialCredit, currency })
const isLimitCreditSearchDefaultValue = Number(searchState?.maxPrice) === formatAvailableCredit
const isLoggedInAndBeneficiary = isLoggedIn && user?.isBeneficiary
@@ -91,28 +92,41 @@ export const PriceModal: FunctionComponent = ({
useForHeightKeyboardEvents(setKeyboardHeight)
function search(values: PriceModalFormData) {
+ const transformedValues = {
+ ...values,
+ minPrice: values.minPrice ? String(Number(values.minPrice) * conversionRate) : '',
+ maxPrice: values.maxPrice ? String(Number(values.maxPrice) * conversionRate) : '',
+ }
+
const offerIsFree =
- values.isOnlyFreeOffersSearch ||
- (values.maxPrice === '0' && (values.minPrice === '' || values.minPrice === '0'))
+ transformedValues.isOnlyFreeOffersSearch ||
+ (transformedValues.maxPrice === '0' &&
+ (transformedValues.minPrice === '' || transformedValues.minPrice === '0'))
+
let additionalSearchState: SearchState = {
...searchState,
priceRange: null,
minPrice: undefined,
maxPrice: undefined,
+ defaultMinPrice: values.minPrice,
+ defaultMaxPrice: values.maxPrice,
offerIsFree,
}
- if (values.minPrice) {
- additionalSearchState = { ...additionalSearchState, minPrice: values.minPrice }
+ if (transformedValues.minPrice) {
+ additionalSearchState = {
+ ...additionalSearchState,
+ minPrice: transformedValues.minPrice,
+ }
}
- if (values.maxPrice) {
+
+ if (transformedValues.maxPrice) {
additionalSearchState = {
...additionalSearchState,
- maxPrice: values.maxPrice,
+ maxPrice: transformedValues.maxPrice,
maxPossiblePrice: undefined,
}
} else {
- // Only the offers that can be reserved by the beneficiary user
additionalSearchState = {
...additionalSearchState,
maxPossiblePrice: String(formatInitialCredit),
@@ -125,17 +139,12 @@ export const PriceModal: FunctionComponent = ({
const initialFormValues = useMemo(() => {
return {
- minPrice: searchState?.minPrice ?? '',
- maxPrice: searchState?.maxPrice ?? '',
+ minPrice: searchState?.defaultMinPrice ?? '',
+ maxPrice: searchState?.defaultMaxPrice ?? '',
isLimitCreditSearch: isLimitCreditSearchDefaultValue,
isOnlyFreeOffersSearch: isOnlyFreeOffersSearchDefaultValue,
}
- }, [
- isLimitCreditSearchDefaultValue,
- isOnlyFreeOffersSearchDefaultValue,
- searchState?.maxPrice,
- searchState?.minPrice,
- ])
+ }, [isLimitCreditSearchDefaultValue, isOnlyFreeOffersSearchDefaultValue, searchState])
const {
handleSubmit,
@@ -169,6 +178,7 @@ export const PriceModal: FunctionComponent = ({
}
const maxPrice = searchState?.maxPrice === '0' ? '' : searchState?.maxPrice ?? ''
const minPrice = searchState?.minPrice === '0' ? '' : searchState?.minPrice ?? ''
+
setValue('maxPrice', maxPrice)
setValue('minPrice', minPrice)
trigger(['minPrice', 'maxPrice'])
@@ -227,7 +237,6 @@ export const PriceModal: FunctionComponent = ({
}, [reset])
const disabled = !isValid || (!isValidating && isSubmitting)
-
const isKeyboardOpen = keyboardHeight > 0
const shouldDisplayBackButton = filterBehaviour === FilterBehaviour.APPLY_WITHOUT_SEARCHING
@@ -262,10 +271,10 @@ export const PriceModal: FunctionComponent = ({
{isLoggedInAndBeneficiary ? (
-
-
+
+
-
+
) : null}
= ({
/>
)}
/>
-
-
-
+
{isLoggedInAndBeneficiary ? (
= ({
label="Limiter la recherche à mon crédit"
testID="limitCreditSearch"
/>
-
-
-
+
)}
/>
) : null}
- (
-
- 0}
- keyboardType="numeric"
- label={`Prix minimum (en\u00a0${currencyFull})`}
- value={value}
- onChangeText={onChange}
- onBlur={onBlur}
- textContentType="none" // disable autofill on iOS
- accessibilityDescribedBy={minPriceInputId}
- testID="Entrée pour le prix minimum"
- placeholder="0"
- disabled={getValues('isOnlyFreeOffersSearch')}
- />
-
-
- )}
+ label={`Prix minimum (en\u00a0${currencyFull})`}
+ placeholder="0"
+ accessibilityId={minPriceInputId}
+ testID="Entrée pour le prix minimum"
+ isDisabled={getValues('isOnlyFreeOffersSearch')}
/>
- (
-
- 0}
- keyboardType="numeric"
- label={`Prix maximum (en\u00a0${currencyFull})`}
- value={value}
- onChangeText={(value) => {
- onChange(value)
- trigger('minPrice')
- }}
- onBlur={onBlur}
- textContentType="none" // disable autofill on iOS
- accessibilityDescribedBy={maxPriceInputId}
- testID="Entrée pour le prix maximum"
- rightLabel={`max\u00a0: ${formatInitialCreditWithCurrency}`}
- placeholder={`${formatInitialCredit}`}
- disabled={getValues('isLimitCreditSearch') || getValues('isOnlyFreeOffersSearch')}
- />
-
-
- )}
+ label={`Prix maximum (en\u00a0${currencyFull})`}
+ placeholder={`${formatInitialCredit}`}
+ rightLabel={`max\u00a0: ${formatInitialCreditWithCurrency}`}
+ accessibilityId={maxPriceInputId}
+ testID="Entrée pour le prix maximum"
+ isDisabled={getValues('isLimitCreditSearch') || getValues('isOnlyFreeOffersSearch')}
/>
{isKeyboardOpen ? : null}
)
}
+
+const StyledSeparator = styled(Separator.Horizontal)({
+ marginVertical: getSpacing(6),
+})
diff --git a/src/features/search/pages/modals/PriceModal/TODO.md b/src/features/search/pages/modals/PriceModal/TODO.md
new file mode 100644
index 00000000000..faf9d129319
--- /dev/null
+++ b/src/features/search/pages/modals/PriceModal/TODO.md
@@ -0,0 +1,24 @@
+# TODO
+
+## Links
+
+- [PC-32821](https://passculture.atlassian.net/browse/PC-32821)
+- [MobTime](https://mobtime.hadrienmp.fr/mob/pass-culture)
+
+---
+
+## Tasks
+
+- [ ] Faire une recherche avec les euros et non une autre devise MAIS faire un affichage dans les différentes devises.
+- [ ] Ajouter un teste dans `PriceModal` ou ailleurs mais avec une autre devise que les euros.
+- [x] Ajouter le symbole de la devise dans le message d'erreur des inputs dans `PriceModal`
+- [x] Fusionner `makePriceSchema` et `makeSearchPriceSchema` ?
+ - [ ] Supprimer `makePriceSchema`
+ - [ ] Supprimer `makeSearchPriceSchema`
+ - [ ] Ecrire un test pour le nouveau `makePriceShema`
+- [ ] Que faire dans `getPriceDescription` qui récupère la valeur de l'input de `PriceModal`
+---
+
+## Tasks for another US
+
+- [ ]
\ No newline at end of file
diff --git a/src/features/search/types.ts b/src/features/search/types.ts
index 048f882292a..16b49ea9f09 100644
--- a/src/features/search/types.ts
+++ b/src/features/search/types.ts
@@ -54,6 +54,8 @@ export interface SearchState {
query: string
minPrice?: string
maxPrice?: string
+ defaultMinPrice?: string
+ defaultMaxPrice?: string
searchId?: string
maxPossiblePrice?: string
isAutocomplete?: boolean
diff --git a/src/libs/parsers/formatCurrencyFromCents.ts b/src/libs/parsers/formatCurrencyFromCents.ts
index 27596f938ef..d1b1b108243 100644
--- a/src/libs/parsers/formatCurrencyFromCents.ts
+++ b/src/libs/parsers/formatCurrencyFromCents.ts
@@ -46,3 +46,14 @@ export const useFormatCurrencyFromCents = (priceInCents: number, options?: Forma
const euroToPacificFrancRate = useGetPacificFrancToEuroRate()
return formatCurrencyFromCents(priceInCents, currency, euroToPacificFrancRate, options)
}
+
+export const useFormatCurrencyFromCentsWithoutCurrenySymbol = (priceInCents: number): number => {
+ const currency = useGetCurrencyToDisplay()
+ const euroToPacificFrancRate = useGetPacificFrancToEuroRate()
+ const priceInEuro = convertCentsToEuros(priceInCents)
+
+ if (currency === Currency.PACIFIC_FRANC_SHORT || currency === Currency.PACIFIC_FRANC_FULL) {
+ return convertEuroToPacificFranc(priceInEuro, euroToPacificFrancRate, RoundUnit.UNITS)
+ }
+ return priceInEuro
+}