diff --git a/src/components/CardExpirationDateElement.hook.ts b/src/components/CardExpirationDateElement.hook.ts index 3a1995b..7af1610 100644 --- a/src/components/CardExpirationDateElement.hook.ts +++ b/src/components/CardExpirationDateElement.hook.ts @@ -36,14 +36,6 @@ const useCardExpirationDateElement = ({ const mask = useMask({ type }); - useBtRef({ - btRef, - elementRef, - id, - setElementValue, - type, - }); - const { _onChange, _onBlur, _onFocus } = useUserEventHandlers({ setElementValue, element: { @@ -56,6 +48,15 @@ const useCardExpirationDateElement = ({ onFocus, }); + useBtRef({ + btRef, + elementRef, + id, + setElementValue, + type, + onChange: _onChange, + }); + return { _onBlur, _onChange, diff --git a/src/components/CardNumberElement.hook.ts b/src/components/CardNumberElement.hook.ts index 56e92aa..eedc4ac 100644 --- a/src/components/CardNumberElement.hook.ts +++ b/src/components/CardNumberElement.hook.ts @@ -81,13 +81,6 @@ export const useCardNumberElement = ({ id, }); - useBtRef({ - btRef, - elementRef, - id, - setElementValue, - }); - const { _onChange, _onBlur, _onFocus } = useUserEventHandlers({ setElementValue, transform: [' ', ''], @@ -106,6 +99,14 @@ export const useCardNumberElement = ({ onFocus, }); + useBtRef({ + btRef, + elementRef, + id, + setElementValue, + onChange: _onChange, + }); + return { elementRef, elementValue, diff --git a/src/components/CardVerificationCodeElement.hook.ts b/src/components/CardVerificationCodeElement.hook.ts index a5300a5..e7df55b 100644 --- a/src/components/CardVerificationCodeElement.hook.ts +++ b/src/components/CardVerificationCodeElement.hook.ts @@ -40,13 +40,6 @@ export const useCardVerificationCodeElement = ({ type, }); - useBtRef({ - btRef, - elementRef, - id, - setElementValue, - }); - const { _onChange, _onBlur, _onFocus } = useUserEventHandlers({ setElementValue, element: { @@ -59,6 +52,14 @@ export const useCardVerificationCodeElement = ({ onFocus, }); + useBtRef({ + btRef, + elementRef, + id, + setElementValue, + onChange: _onChange, + }); + return { elementRef, elementValue, diff --git a/src/components/TextElement.hook.ts b/src/components/TextElement.hook.ts index 7c98f7f..f622701 100644 --- a/src/components/TextElement.hook.ts +++ b/src/components/TextElement.hook.ts @@ -36,13 +36,6 @@ export const useTextElement = ({ useCleanupStateBeforeUnmount(id); - useBtRef({ - btRef, - elementRef, - id, - setElementValue, - }); - const { _onChange, _onBlur, _onFocus } = useUserEventHandlers({ setElementValue, element: { @@ -56,6 +49,14 @@ export const useTextElement = ({ transform, }); + useBtRef({ + btRef, + elementRef, + id, + setElementValue, + onChange: _onChange, + }); + return { elementRef, _onChange, diff --git a/src/components/shared/useBtRef.ts b/src/components/shared/useBtRef.ts index fac3816..fdbce17 100644 --- a/src/components/shared/useBtRef.ts +++ b/src/components/shared/useBtRef.ts @@ -1,4 +1,3 @@ -import { compose } from 'ramda'; import type { Dispatch, ForwardedRef, RefObject, SetStateAction } from 'react'; import { useEffect } from 'react'; import type { TextInput } from 'react-native'; @@ -18,6 +17,7 @@ type UseBtRefProps = { id: string; setElementValue: Dispatch>; type?: ElementType; + onChange?: (value: string) => void; }; type CreateBtRefArgs = Omit & { @@ -99,9 +99,18 @@ export const useBtRef = ({ id, type, setElementValue, + onChange, }: UseBtRefProps) => { useEffect(() => { - const valueSetter = compose(setElementValue, valueFormatter); + const valueSetter: ValueSetter = (val) => { + const formattedValue = valueFormatter(val); + + setElementValue(formattedValue); + + if (onChange) { + onChange(formattedValue); + } + }; const newBtRef = createBtRef({ id, diff --git a/tests/components/CardNumberElement.test.tsx b/tests/components/CardNumberElement.test.tsx index 7c69e45..4124820 100644 --- a/tests/components/CardNumberElement.test.tsx +++ b/tests/components/CardNumberElement.test.tsx @@ -932,4 +932,257 @@ describe('CardNumberElement', () => { }); }); }); + + describe('setValue behavior', () => { + test('setValue triggers onChange with valid card number', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + render( + + ); + + onChange.mockClear(); + + const validCardRef = { + id: ref.current.id, + format: () => '4242424242424242', + }; + + ref.current.setValue(validCardRef); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + brand: 'visa', + cardBin: '42424242', + cardLast4: '4242', + complete: true, + cvcLength: 3, + empty: false, + maskSatisfied: true, + valid: true, + }) + ); + }); + }); + + test('setValue triggers onChange with invalid card number', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + render( + + ); + + onChange.mockClear(); + + const invalidCardRef = { + id: ref.current.id, + format: () => '4242424242424241', + }; + + ref.current.setValue(invalidCardRef); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + brand: 'visa', + complete: false, + empty: false, + errors: [{ targetId: 'cardNumber', type: 'invalid' }], + maskSatisfied: true, + valid: false, + }) + ); + }); + }); + + test('setValue triggers onChange with incomplete card number', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + render( + + ); + + onChange.mockClear(); + + const incompleteCardRef = { + id: ref.current.id, + format: () => '4242', + }; + + ref.current.setValue(incompleteCardRef); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + brand: 'visa', + cardBin: undefined, + cardLast4: undefined, + cvcLength: 3, + complete: false, + empty: false, + errors: [{ targetId: 'cardNumber', type: 'incomplete' }], + maskSatisfied: false, + valid: false, + }) + ); + }); + }); + + test('setValue triggers onChange with empty value', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + render( + + ); + + onChange.mockClear(); + + const emptyCardRef = { + id: ref.current.id, + format: () => '', + }; + + ref.current.setValue(emptyCardRef); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + brand: 'unknown', + complete: false, + empty: true, + maskSatisfied: false, + valid: false, + }) + ); + }); + }); + + test('setValue does not trigger onChange when onChange is not provided', async () => { + const ref = { + current: null as any, + }; + + const { getByPlaceholderText } = render( + + ); + + const validCardRef = { + id: ref.current.id, + format: () => '4242424242424242', + }; + + // Should not throw error even without onChange + expect(() => { + ref.current.setValue(validCardRef); + }).not.toThrow(); + + const el = getByPlaceholderText('Card Number'); + await waitFor(() => { + expect(el.props.value).toBe('4242 4242 4242 4242'); + }); + }); + + test('setValue updates element value correctly', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + const { getByPlaceholderText } = render( + + ); + + const validCardRef = { + id: ref.current.id, + format: () => '4242424242424242', + }; + + ref.current.setValue(validCardRef); + + const el = getByPlaceholderText('Card Number'); + await waitFor(() => { + expect(el.props.value).toBe('4242 4242 4242 4242'); + }); + }); + + test('setValue with skipLuhnValidation validates correctly', async () => { + const onChange = jest.fn(); + const ref = { + current: null as any, + }; + + render( + + ); + + onChange.mockClear(); + + const invalidLuhnCardRef = { + id: ref.current.id, + format: () => '4242424242424241', + }; + + ref.current.setValue(invalidLuhnCardRef); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + brand: 'visa', + complete: true, + empty: false, + valid: true, + errors: undefined, + }) + ); + }); + }); + }); });