Skip to content

Commit 97cdd3c

Browse files
committed
feat: add a new parameter to trigger validation on setValue
1 parent 8f7b6ac commit 97cdd3c

5 files changed

Lines changed: 252 additions & 19 deletions

File tree

src/BaseElementTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type InputBtDateRefReveal = {
2424
month: InputBTRef;
2525
};
2626

27-
type ValueSetter = (val: InputBtDateRefReveal | InputBTRef | undefined) => void;
27+
type ValueSetter = (val: InputBtDateRefReveal | InputBTRef | undefined, validate?: boolean) => void;
2828

2929
type BTRef = CommonBTRefFunctions & InputBTRef;
3030

src/ElementValues.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const _elementState: Record<string, {
2020
*/
2121
const _elementErrors: Record<string, string | undefined> = {};
2222

23+
/**
24+
* Store for onChange handlers so they can be triggered programmatically
25+
*/
26+
const _elementOnChangeHandlers: Record<string, ((value: string) => void) | undefined> = {};
27+
2328
// Legacy exports for backward compatibility - these now access the unified store
2429
const _elementValues: Record<string, PrimitiveType> = new Proxy({}, {
2530
get: (_, id: string) => _elementState[id]?.value,
@@ -108,4 +113,4 @@ const _elementMetadata: Record<string, {
108113
},
109114
});
110115

111-
export { _elementErrors, _elementValues, _elementMetadata, _elementRawValues, _elementState };
116+
export { _elementErrors, _elementValues, _elementMetadata, _elementRawValues, _elementState, _elementOnChangeHandlers };

src/components/shared/useBtRef.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { compose } from 'ramda';
21
import type { Dispatch, ForwardedRef, RefObject, SetStateAction } from 'react';
32
import { useEffect } from 'react';
43
import type { TextInput } from 'react-native';
@@ -10,7 +9,7 @@ import type {
109
ValueSetter,
1110
} from '../../BaseElementTypes';
1211
import { ElementType } from '../../BaseElementTypes';
13-
import { _elementValues } from '../../ElementValues';
12+
import { _elementValues, _elementOnChangeHandlers, _elementRawValues } from '../../ElementValues';
1413

1514
type UseBtRefProps = {
1615
btRef?: ForwardedRef<BTDateRef | BTRef>;
@@ -101,7 +100,15 @@ export const useBtRef = ({
101100
setElementValue,
102101
}: UseBtRefProps) => {
103102
useEffect(() => {
104-
const valueSetter = compose(setElementValue, valueFormatter);
103+
const valueSetter: ValueSetter = (val, validate = false) => {
104+
const formattedValue = valueFormatter(val);
105+
setElementValue(formattedValue);
106+
107+
if (validate && _elementOnChangeHandlers[id]) {
108+
_elementRawValues[id] = formattedValue;
109+
_elementOnChangeHandlers[id]!(formattedValue);
110+
}
111+
};
105112

106113
const newBtRef = createBtRef({
107114
id,
@@ -111,5 +118,5 @@ export const useBtRef = ({
111118
});
112119

113120
updateRef(btRef!, newBtRef);
114-
}, [btRef, elementRef, id]);
121+
}, [btRef, elementRef, id, type, setElementValue]);
115122
};

src/components/shared/useUserEventHandlers.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Dispatch, SetStateAction } from 'react';
22
import { useEffect } from 'react';
3-
import { _elementValues, _elementMetadata, _elementRawValues } from '../../ElementValues';
3+
import { _elementValues, _elementMetadata, _elementRawValues, _elementOnChangeHandlers } from '../../ElementValues';
44
import { useElementEvent } from './useElementEvent';
55
import type { ElementType, EventConsumers } from '../../BaseElementTypes';
66
import type { TransformType } from './useTransform';
@@ -59,21 +59,31 @@ export const useUserEventHandlers = ({
5959
};
6060
}, [element.binInfo, element.selectedNetwork, onChange, createEvent, element.id]);
6161

62-
return {
63-
_onChange: (_elementValue: string) => {
64-
_elementRawValues[element.id] = _elementValue;
65-
_elementValues[element.id] = transformation.apply(_elementValue);
62+
const _onChange = (_elementValue: string) => {
63+
_elementRawValues[element.id] = _elementValue;
64+
_elementValues[element.id] = transformation.apply(_elementValue);
6665

67-
setElementValue(() => {
68-
if (onChange) {
69-
const event = createEvent(_elementValue);
66+
setElementValue(() => {
67+
if (onChange) {
68+
const event = createEvent(_elementValue);
7069

71-
onChange(event);
72-
}
70+
onChange(event);
71+
}
7372

74-
return _elementValue;
75-
});
76-
},
73+
return _elementValue;
74+
});
75+
};
76+
77+
useEffect(() => {
78+
_elementOnChangeHandlers[element.id] = _onChange;
79+
80+
return () => {
81+
delete _elementOnChangeHandlers[element.id];
82+
};
83+
}, [element.id, onChange, createEvent, transformation]);
84+
85+
return {
86+
_onChange,
7787
_onFocus: (_event: NativeSyntheticEvent<TextInputFocusEventData>) => {
7888
const val = _elementValues[element.id] ?? '';
7989

tests/components/CardNumberElement.test.tsx

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,4 +932,215 @@ describe('CardNumberElement', () => {
932932
});
933933
});
934934
});
935+
936+
describe('setValue validate parameter', () => {
937+
test('setValue without validate parameter does not trigger onChange', async () => {
938+
const onChange = jest.fn();
939+
const ref = {
940+
current: null as any,
941+
};
942+
943+
render(
944+
<CardNumberElement
945+
btRef={ref}
946+
onChange={onChange}
947+
placeholder="Card Number"
948+
style={{}}
949+
/>
950+
);
951+
952+
onChange.mockClear();
953+
954+
ref.current.setValue(
955+
{ id: ref.current.id, format: (val: string) => val }
956+
);
957+
958+
await waitFor(() => {
959+
expect(onChange).not.toHaveBeenCalled();
960+
});
961+
});
962+
963+
test('setValue with validate=false does not trigger onChange', async () => {
964+
const onChange = jest.fn();
965+
const ref = {
966+
current: null as any,
967+
};
968+
969+
render(
970+
<CardNumberElement
971+
btRef={ref}
972+
onChange={onChange}
973+
placeholder="Card Number"
974+
style={{}}
975+
/>
976+
);
977+
978+
onChange.mockClear();
979+
980+
ref.current.setValue(
981+
{ id: ref.current.id, format: (val: string) => val },
982+
false
983+
);
984+
985+
await waitFor(() => {
986+
expect(onChange).not.toHaveBeenCalled();
987+
});
988+
});
989+
990+
test('setValue with validate=true triggers onChange with validation', async () => {
991+
const onChange = jest.fn();
992+
const ref = {
993+
current: null as any,
994+
};
995+
996+
render(
997+
<CardNumberElement
998+
btRef={ref}
999+
onChange={onChange}
1000+
placeholder="Card Number"
1001+
style={{}}
1002+
/>
1003+
);
1004+
1005+
onChange.mockClear();
1006+
1007+
const validCardRef = {
1008+
id: ref.current.id,
1009+
format: () => '4242424242424242',
1010+
};
1011+
1012+
ref.current.setValue(validCardRef, true);
1013+
1014+
await waitFor(() => {
1015+
expect(onChange).toHaveBeenCalledWith(
1016+
expect.objectContaining({
1017+
brand: 'visa',
1018+
cardBin: '42424242',
1019+
cardLast4: '4242',
1020+
complete: true,
1021+
cvcLength: 3,
1022+
empty: false,
1023+
maskSatisfied: true,
1024+
valid: true,
1025+
})
1026+
);
1027+
});
1028+
});
1029+
1030+
test('setValue with validate=true validates invalid card number', async () => {
1031+
const onChange = jest.fn();
1032+
const ref = {
1033+
current: null as any,
1034+
};
1035+
1036+
render(
1037+
<CardNumberElement
1038+
btRef={ref}
1039+
onChange={onChange}
1040+
placeholder="Card Number"
1041+
style={{}}
1042+
/>
1043+
);
1044+
1045+
onChange.mockClear();
1046+
1047+
const invalidCardRef = {
1048+
id: ref.current.id,
1049+
format: () => '4242424242424241',
1050+
};
1051+
1052+
ref.current.setValue(invalidCardRef, true);
1053+
1054+
await waitFor(() => {
1055+
expect(onChange).toHaveBeenCalledWith(
1056+
expect.objectContaining({
1057+
brand: 'visa',
1058+
complete: false,
1059+
empty: false,
1060+
errors: [{ targetId: 'cardNumber', type: 'invalid' }],
1061+
maskSatisfied: true,
1062+
valid: false,
1063+
})
1064+
);
1065+
});
1066+
});
1067+
1068+
test('setValue with validate=true validates incomplete card number', async () => {
1069+
const onChange = jest.fn();
1070+
const ref = {
1071+
current: null as any,
1072+
};
1073+
1074+
render(
1075+
<CardNumberElement
1076+
btRef={ref}
1077+
onChange={onChange}
1078+
placeholder="Card Number"
1079+
style={{}}
1080+
/>
1081+
);
1082+
1083+
onChange.mockClear();
1084+
1085+
const incompleteCardRef = {
1086+
id: ref.current.id,
1087+
format: () => '4242',
1088+
};
1089+
1090+
ref.current.setValue(incompleteCardRef, true);
1091+
1092+
await waitFor(() => {
1093+
expect(onChange).toHaveBeenCalledWith(
1094+
expect.objectContaining({
1095+
brand: 'visa',
1096+
cardBin: undefined,
1097+
cardLast4: undefined,
1098+
cvcLength: 3,
1099+
complete: false,
1100+
empty: false,
1101+
errors: [{ targetId: 'cardNumber', type: 'incomplete' }],
1102+
maskSatisfied: false,
1103+
valid: false,
1104+
})
1105+
);
1106+
});
1107+
});
1108+
1109+
test('setValue with validate=true and empty value', async () => {
1110+
const onChange = jest.fn();
1111+
const ref = {
1112+
current: null as any,
1113+
};
1114+
1115+
render(
1116+
<CardNumberElement
1117+
btRef={ref}
1118+
onChange={onChange}
1119+
placeholder="Card Number"
1120+
style={{}}
1121+
/>
1122+
);
1123+
1124+
onChange.mockClear();
1125+
1126+
const emptyCardRef = {
1127+
id: ref.current.id,
1128+
format: () => '',
1129+
};
1130+
1131+
ref.current.setValue(emptyCardRef, true);
1132+
1133+
await waitFor(() => {
1134+
expect(onChange).toHaveBeenCalledWith(
1135+
expect.objectContaining({
1136+
brand: 'unknown',
1137+
complete: false,
1138+
empty: true,
1139+
maskSatisfied: false,
1140+
valid: false,
1141+
})
1142+
);
1143+
});
1144+
});
1145+
});
9351146
});

0 commit comments

Comments
 (0)