@evervault/react-native 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { TurboModuleRegistry, TextInput, Platform, StyleSheet } from 'react-native';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
  import * as React from 'react';
4
- import React__default, { createContext, useMemo, useCallback, useContext, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
4
+ import React__default, { createContext, useMemo, useCallback, useContext, forwardRef, useRef, useImperativeHandle, useEffect, useState } from 'react';
5
5
  import { WebView } from 'react-native-webview';
6
6
 
7
7
  const NativeEvervault = TurboModuleRegistry.get("NativeEvervault");
@@ -7164,51 +7164,6 @@ const r=(t,r,o)=>{if(t&&"reportValidity"in t){const s=get(o,r);t.setCustomValidi
7164
7164
 
7165
7165
  function n(r,e){for(var n={};r.length;){var s=r[0],t=s.code,i=s.message,a=s.path.join(".");if(!n[a])if("unionErrors"in s){var u=s.unionErrors[0].errors[0];n[a]={message:u.message,type:u.code};}else n[a]={message:i,type:t};if("unionErrors"in s&&s.unionErrors.forEach(function(e){return e.errors.forEach(function(e){return r.push(e)})}),e){var c=n[a].types,f=c&&c[s.code];n[a]=appendErrors(a,e,n,t,f?[].concat(f,s.message):s.message);}r.shift();}return n}function s(o$1,s,t){return void 0===t&&(t={}),function(i,a,u){try{return Promise.resolve(function(e,n){try{var a=Promise.resolve(o$1["sync"===t.mode?"parse":"parseAsync"](i,s)).then(function(e){return u.shouldUseNativeValidation&&o({},u),{errors:{},values:t.raw?Object.assign({},i):e}});}catch(r){return n(r)}return a&&a.then?a.then(void 0,n):a}(0,function(r){if(function(r){return Array.isArray(null==r?void 0:r.errors)}(r))return {values:{},errors:s$1(n(r.errors,!u.shouldUseNativeValidation&&"all"===u.criteriaMode),u)};throw r}))}catch(r){return Promise.reject(r)}}}
7166
7166
 
7167
- const DEFAULT_ACCEPTED_BRANDS = [];
7168
- const Card$1 = forwardRef(function Card({ children, defaultValues, onChange, acceptedBrands = DEFAULT_ACCEPTED_BRANDS, validationMode = "all", }, ref) {
7169
- const evervault = useEvervault();
7170
- const resolver = useMemo(() => {
7171
- const schema = getCardFormSchema(acceptedBrands);
7172
- return s(schema);
7173
- }, [acceptedBrands]);
7174
- const methods = useForm({
7175
- defaultValues,
7176
- resolver,
7177
- mode: validationMode,
7178
- shouldUseNativeValidation: false,
7179
- });
7180
- useEffect(() => {
7181
- if (!onChange)
7182
- return;
7183
- let abortController;
7184
- function handleChange(values) {
7185
- if (abortController) {
7186
- abortController.abort();
7187
- }
7188
- abortController = new AbortController();
7189
- const signal = abortController.signal;
7190
- requestAnimationFrame(async () => {
7191
- const payload = await formatPayload(values, {
7192
- encrypt: evervault.encrypt,
7193
- form: methods,
7194
- });
7195
- if (signal.aborted)
7196
- return;
7197
- onChange?.(payload);
7198
- });
7199
- }
7200
- handleChange(methods.getValues());
7201
- const subscription = methods.watch(handleChange);
7202
- return () => subscription.unsubscribe();
7203
- }, [evervault.encrypt, onChange]);
7204
- useImperativeHandle(ref, useCallback(() => ({
7205
- reset() {
7206
- methods.reset();
7207
- },
7208
- }), []));
7209
- return jsx(FormProvider, { ...methods, children: children });
7210
- });
7211
-
7212
7167
  // Taken from https://github.com/gregberge/react-merge-refs
7213
7168
  function mergeRefs(...refs) {
7214
7169
  return (value) => {
@@ -7408,6 +7363,9 @@ var MaskInput = /*#__PURE__*/React.forwardRef(function (props, ref) {
7408
7363
  }));
7409
7364
  });
7410
7365
 
7366
+ const EvervaultInputContext = createContext({
7367
+ validationMode: "all",
7368
+ });
7411
7369
  function useForwardedInputRef(ref) {
7412
7370
  const inputRef = useRef(null);
7413
7371
  useImperativeHandle(ref, useCallback(() => ({
@@ -7444,6 +7402,7 @@ function mask(format) {
7444
7402
  });
7445
7403
  }
7446
7404
  const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props }, ref) {
7405
+ const { validationMode } = useContext(EvervaultInputContext);
7447
7406
  const inputRef = useForwardedInputRef(ref);
7448
7407
  const methods = useFormContext();
7449
7408
  return (jsx(Controller, { control: methods.control, name: name, shouldUnregister: true, render: ({ field, fieldState }) => (jsx(MaskInput
@@ -7453,22 +7412,86 @@ const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props
7453
7412
  id: field.name, ...props,
7454
7413
  // Strict props
7455
7414
  ref: mergeRefs(inputRef, field.ref), editable: !field.disabled && (props.editable ?? true), onBlur: (evt) => {
7415
+ const shouldValidate = validationMode === "onBlur" ||
7416
+ validationMode === "onTouched" ||
7417
+ validationMode === "all";
7456
7418
  methods.setValue(field.name, field.value, {
7457
7419
  shouldDirty: true,
7458
7420
  shouldTouch: true,
7459
- shouldValidate: true,
7421
+ shouldValidate,
7460
7422
  });
7461
7423
  props.onBlur?.(evt);
7462
7424
  }, mask: mask, maskAutoComplete: !!mask, value: field.value, onChangeText: (masked, unmasked) => {
7425
+ const shouldValidate = (validationMode === "onTouched" && fieldState.isTouched) ||
7426
+ ((validationMode === "onChange" || validationMode === "all") &&
7427
+ (!!fieldState.error || fieldState.isTouched));
7463
7428
  methods.setValue(field.name, unmasked, {
7464
7429
  shouldDirty: true,
7465
- shouldValidate: !!fieldState.error || fieldState.isTouched,
7430
+ shouldValidate,
7466
7431
  });
7467
7432
  },
7468
7433
  // Remove unwanted props
7469
7434
  defaultValue: undefined, onChange: undefined })) }));
7470
7435
  });
7471
7436
 
7437
+ const DEFAULT_ACCEPTED_BRANDS = [];
7438
+ const Card$1 = forwardRef(function Card({ children, defaultValues, onChange, onError, acceptedBrands = DEFAULT_ACCEPTED_BRANDS, validationMode = "all", }, ref) {
7439
+ const evervault = useEvervault();
7440
+ const resolver = useMemo(() => {
7441
+ const schema = getCardFormSchema(acceptedBrands);
7442
+ return s(schema);
7443
+ }, [acceptedBrands]);
7444
+ const methods = useForm({
7445
+ defaultValues,
7446
+ resolver,
7447
+ mode: validationMode,
7448
+ shouldUseNativeValidation: false,
7449
+ });
7450
+ const inputContext = useMemo(() => ({
7451
+ validationMode,
7452
+ }), [validationMode]);
7453
+ // Use refs to prevent closures from being captured
7454
+ const onChangeRef = useRef(onChange);
7455
+ onChangeRef.current = onChange;
7456
+ const onErrorRef = useRef(onError);
7457
+ onErrorRef.current = onError;
7458
+ useEffect(() => {
7459
+ if (!onChange)
7460
+ return;
7461
+ let abortController;
7462
+ function handleChange(values) {
7463
+ if (abortController) {
7464
+ abortController.abort();
7465
+ }
7466
+ abortController = new AbortController();
7467
+ const signal = abortController.signal;
7468
+ requestAnimationFrame(async () => {
7469
+ try {
7470
+ const payload = await formatPayload(values, {
7471
+ encrypt: evervault.encrypt,
7472
+ form: methods,
7473
+ });
7474
+ if (signal.aborted)
7475
+ return;
7476
+ onChangeRef.current?.(payload);
7477
+ }
7478
+ catch (error) {
7479
+ onErrorRef.current?.(error);
7480
+ }
7481
+ });
7482
+ }
7483
+ handleChange(methods.getValues());
7484
+ const subscription = methods.watch(handleChange);
7485
+ return () => subscription.unsubscribe();
7486
+ }, [evervault.encrypt]);
7487
+ useImperativeHandle(ref, useCallback(() => ({
7488
+ reset() {
7489
+ methods.reset();
7490
+ },
7491
+ }), []));
7492
+ return (jsx(FormProvider, { ...methods, children: jsx(EvervaultInputContext.Provider, { value: inputContext, children: children }) }));
7493
+ });
7494
+
7472
7495
  const CardHolder = forwardRef(function CardHolder(props, ref) {
7473
7496
  return (jsx(EvervaultInput, { placeholder: "Johnny Appleseed", ...props, ref: ref, name: "name", inputMode: "text", autoComplete: Platform.select({
7474
7497
  ios: "cc-name",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@evervault/react-native",
3
3
  "description": "Evervault SDK for React Native",
4
- "version": "2.0.0",
4
+ "version": "2.2.0",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./build/index.cjs.js",
7
7
  "module": "./build/index.esm.js",
@@ -76,6 +76,7 @@
76
76
  "@evervault/card-validator": "1.3.0"
77
77
  },
78
78
  "scripts": {
79
+ "prebuild": "pnpm codegen",
79
80
  "build": "rollup -c",
80
81
  "watch": "rollup -c --watch",
81
82
  "codegen": "rimraf build android/app && react-native codegen",
package/src/Card/Root.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  useEffect,
6
6
  useImperativeHandle,
7
7
  useMemo,
8
+ useRef,
8
9
  } from "react";
9
10
  import { CardBrandName, CardConfig, CardPayload } from "./types";
10
11
  import { DeepPartial, FormProvider, useForm } from "react-hook-form";
@@ -12,6 +13,8 @@ import { CardFormValues, getCardFormSchema } from "./schema";
12
13
  import { zodResolver } from "@hookform/resolvers/zod";
13
14
  import { useEvervault } from "../useEvervault";
14
15
  import { formatPayload } from "./utils";
16
+ import { EvervaultInputContext, EvervaultInputContextValue } from "../Input";
17
+ import { EvervaultContextValue } from "../context";
15
18
 
16
19
  const DEFAULT_ACCEPTED_BRANDS: CardBrandName[] = [];
17
20
 
@@ -31,6 +34,11 @@ export interface CardProps extends PropsWithChildren, CardConfig {
31
34
  */
32
35
  onChange?(payload: CardPayload): void;
33
36
 
37
+ /**
38
+ * Triggered when a native error occurs.
39
+ */
40
+ onError?(error: Error): void;
41
+
34
42
  /**
35
43
  * The validation mode to use for the form.
36
44
  *
@@ -56,6 +64,7 @@ export const Card = forwardRef<Card, CardProps>(function Card(
56
64
  children,
57
65
  defaultValues,
58
66
  onChange,
67
+ onError,
59
68
  acceptedBrands = DEFAULT_ACCEPTED_BRANDS,
60
69
  validationMode = "all",
61
70
  },
@@ -75,6 +84,19 @@ export const Card = forwardRef<Card, CardProps>(function Card(
75
84
  shouldUseNativeValidation: false,
76
85
  });
77
86
 
87
+ const inputContext = useMemo<EvervaultInputContextValue>(
88
+ () => ({
89
+ validationMode,
90
+ }),
91
+ [validationMode]
92
+ );
93
+
94
+ // Use refs to prevent closures from being captured
95
+ const onChangeRef = useRef<typeof onChange>(onChange);
96
+ onChangeRef.current = onChange;
97
+ const onErrorRef = useRef<typeof onError>(onError);
98
+ onErrorRef.current = onError;
99
+
78
100
  useEffect(() => {
79
101
  if (!onChange) return;
80
102
 
@@ -88,19 +110,23 @@ export const Card = forwardRef<Card, CardProps>(function Card(
88
110
  const signal = abortController.signal;
89
111
 
90
112
  requestAnimationFrame(async () => {
91
- const payload = await formatPayload(values, {
92
- encrypt: evervault.encrypt,
93
- form: methods,
94
- });
95
- if (signal.aborted) return;
96
- onChange?.(payload);
113
+ try {
114
+ const payload = await formatPayload(values, {
115
+ encrypt: evervault.encrypt,
116
+ form: methods,
117
+ });
118
+ if (signal.aborted) return;
119
+ onChangeRef.current?.(payload);
120
+ } catch (error) {
121
+ onErrorRef.current?.(error as Error);
122
+ }
97
123
  });
98
124
  }
99
125
 
100
126
  handleChange(methods.getValues());
101
127
  const subscription = methods.watch(handleChange);
102
128
  return () => subscription.unsubscribe();
103
- }, [evervault.encrypt, onChange]);
129
+ }, [evervault.encrypt]);
104
130
 
105
131
  useImperativeHandle(
106
132
  ref,
@@ -114,5 +140,11 @@ export const Card = forwardRef<Card, CardProps>(function Card(
114
140
  )
115
141
  );
116
142
 
117
- return <FormProvider {...methods}>{children}</FormProvider>;
143
+ return (
144
+ <FormProvider {...methods}>
145
+ <EvervaultInputContext.Provider value={inputContext}>
146
+ {children}
147
+ </EvervaultInputContext.Provider>
148
+ </FormProvider>
149
+ );
118
150
  });
@@ -4,10 +4,11 @@ import {
4
4
  screen,
5
5
  userEvent,
6
6
  } from "@testing-library/react-native";
7
- import { EvervaultInput, mask } from "./Input";
8
- import { FormProvider, useForm, useFormContext } from "react-hook-form";
7
+ import { EvervaultInput, EvervaultInputContext, mask } from "./Input";
8
+ import { FieldErrors, FormProvider, Resolver, useForm } from "react-hook-form";
9
9
  import { PropsWithChildren } from "react";
10
- import { Text, View } from "react-native";
10
+ import { zodResolver } from "@hookform/resolvers/zod";
11
+ import { z } from "zod";
11
12
 
12
13
  describe("mask", () => {
13
14
  it("should convert a mask to an array of regex", () => {
@@ -31,21 +32,38 @@ describe("mask", () => {
31
32
  describe("EvervaultInput", () => {
32
33
  const methodMocks = {
33
34
  setValue: vi.fn(),
35
+ setError: vi.fn(),
34
36
  };
35
37
 
36
- function Form({ children }: PropsWithChildren) {
37
- const methods = useForm();
38
+ interface FormProps {
39
+ resolver?: Resolver<any>;
40
+ }
41
+
42
+ function Form({ children, resolver }: PropsWithChildren<FormProps>) {
43
+ const methods = useForm({
44
+ resolver,
45
+ });
38
46
  const setValue = (...args: Parameters<typeof methods.setValue>) => {
39
47
  methodMocks.setValue(...args);
40
48
  methods.setValue(...args);
41
49
  };
50
+ const setError = (...args: Parameters<typeof methods.setError>) => {
51
+ methodMocks.setError(...args);
52
+ methods.setError(...args);
53
+ };
42
54
  return (
43
- <FormProvider {...methods} setValue={setValue}>
55
+ <FormProvider {...methods} setValue={setValue} setError={setError}>
44
56
  {children}
45
57
  </FormProvider>
46
58
  );
47
59
  }
48
60
 
61
+ function createForm(options: FormProps) {
62
+ return function (props: PropsWithChildren<FormProps>) {
63
+ return <Form {...options} {...props} />;
64
+ };
65
+ }
66
+
49
67
  it("should render", async () => {
50
68
  render(<EvervaultInput testID="phone" name="phone" />, {
51
69
  wrapper: Form,
@@ -135,4 +153,184 @@ describe("EvervaultInput", () => {
135
153
  shouldValidate: true,
136
154
  });
137
155
  });
156
+
157
+ it("only validates the field when blurred if validationMode=onBlur", async () => {
158
+ render(
159
+ <EvervaultInputContext.Provider value={{ validationMode: "onBlur" }}>
160
+ <EvervaultInput testID="phone" name="phone" />
161
+ </EvervaultInputContext.Provider>,
162
+ {
163
+ wrapper: Form,
164
+ }
165
+ );
166
+
167
+ const input = screen.getByTestId("phone");
168
+
169
+ fireEvent(input, "blur");
170
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", undefined, {
171
+ shouldDirty: true,
172
+ shouldTouch: true,
173
+ shouldValidate: true,
174
+ });
175
+
176
+ const user = userEvent.setup();
177
+ await user.type(input, "1234567890", { skipBlur: true });
178
+ expect(input).toHaveProp("value", "1234567890");
179
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
180
+ "phone",
181
+ "1234567890",
182
+ {
183
+ shouldDirty: true,
184
+ shouldValidate: false,
185
+ }
186
+ );
187
+ });
188
+
189
+ it("only validates the field after first touch if validationMode=onTouched", async () => {
190
+ render(
191
+ <EvervaultInputContext.Provider value={{ validationMode: "onTouched" }}>
192
+ <EvervaultInput testID="phone" name="phone" />
193
+ </EvervaultInputContext.Provider>,
194
+ {
195
+ wrapper: Form,
196
+ }
197
+ );
198
+
199
+ const input = screen.getByTestId("phone");
200
+
201
+ const user = userEvent.setup();
202
+ await user.type(input, "1234", { skipBlur: true });
203
+ expect(input).toHaveProp("value", "1234");
204
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
205
+ shouldDirty: true,
206
+ shouldValidate: false,
207
+ });
208
+
209
+ fireEvent(input, "blur");
210
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
211
+ shouldDirty: true,
212
+ shouldTouch: true,
213
+ shouldValidate: true,
214
+ });
215
+
216
+ await user.type(input, "567890", { skipBlur: true });
217
+ expect(input).toHaveProp("value", "1234567890");
218
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
219
+ "phone",
220
+ "1234567890",
221
+ { shouldDirty: true, shouldValidate: true }
222
+ );
223
+ });
224
+
225
+ it("only validates the field when changed if validationMode=onChange and the field is touched", async () => {
226
+ render(
227
+ <EvervaultInputContext.Provider value={{ validationMode: "onChange" }}>
228
+ <EvervaultInput testID="phone" name="phone" />
229
+ </EvervaultInputContext.Provider>,
230
+ {
231
+ wrapper: Form,
232
+ }
233
+ );
234
+
235
+ const input = screen.getByTestId("phone");
236
+
237
+ const user = userEvent.setup();
238
+ await user.type(input, "1234567890", { skipBlur: true });
239
+ expect(input).toHaveProp("value", "1234567890");
240
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
241
+ "phone",
242
+ "1234567890",
243
+ { shouldDirty: true, shouldValidate: false }
244
+ );
245
+
246
+ fireEvent(input, "blur");
247
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
248
+ "phone",
249
+ "1234567890",
250
+ { shouldDirty: true, shouldTouch: true, shouldValidate: false }
251
+ );
252
+
253
+ await user.type(input, "1", { skipBlur: true });
254
+ expect(input).toHaveProp("value", "12345678901");
255
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
256
+ "phone",
257
+ "12345678901",
258
+ { shouldDirty: true, shouldValidate: true }
259
+ );
260
+ });
261
+
262
+ it("only validates the field when changed if validationMode=onChange and the field has errors", async () => {
263
+ render(
264
+ <EvervaultInputContext.Provider value={{ validationMode: "onChange" }}>
265
+ <EvervaultInput testID="phone" name="phone" />
266
+ </EvervaultInputContext.Provider>,
267
+ {
268
+ wrapper: createForm({
269
+ resolver: zodResolver(z.object({ phone: z.string().min(10) })),
270
+ }),
271
+ }
272
+ );
273
+
274
+ const input = screen.getByTestId("phone");
275
+
276
+ const user = userEvent.setup();
277
+ await user.type(input, "1234", { skipBlur: true });
278
+ expect(input).toHaveProp("value", "1234");
279
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
280
+ shouldDirty: true,
281
+ shouldValidate: false,
282
+ });
283
+
284
+ fireEvent(input, "blur");
285
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
286
+ shouldDirty: true,
287
+ shouldTouch: true,
288
+ shouldValidate: false,
289
+ });
290
+
291
+ await user.type(input, "5", { skipBlur: true });
292
+ expect(input).toHaveProp("value", "12345");
293
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "12345", {
294
+ shouldDirty: true,
295
+ shouldValidate: true,
296
+ });
297
+ });
298
+
299
+ it("validates on blur, touch, and change if validationMode=all", async () => {
300
+ render(
301
+ <EvervaultInputContext.Provider value={{ validationMode: "all" }}>
302
+ <EvervaultInput testID="phone" name="phone" />
303
+ </EvervaultInputContext.Provider>,
304
+ {
305
+ wrapper: createForm({
306
+ resolver: zodResolver(z.object({ phone: z.string().min(10) })),
307
+ }),
308
+ }
309
+ );
310
+
311
+ const input = screen.getByTestId("phone");
312
+
313
+ const user = userEvent.setup();
314
+ await user.type(input, "1234", { skipBlur: true });
315
+ expect(input).toHaveProp("value", "1234");
316
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
317
+ shouldDirty: true,
318
+ shouldValidate: false,
319
+ });
320
+
321
+ fireEvent(input, "blur");
322
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith("phone", "1234", {
323
+ shouldDirty: true,
324
+ shouldTouch: true,
325
+ shouldValidate: true,
326
+ });
327
+
328
+ await user.type(input, "567890", { skipBlur: true });
329
+ expect(input).toHaveProp("value", "1234567890");
330
+ expect(methodMocks.setValue).toHaveBeenLastCalledWith(
331
+ "phone",
332
+ "1234567890",
333
+ { shouldDirty: true, shouldValidate: true }
334
+ );
335
+ });
138
336
  });
package/src/Input.tsx CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
+ createContext,
2
3
  ForwardedRef,
3
4
  forwardRef,
4
5
  ReactNode,
5
6
  Ref,
6
7
  RefObject,
7
8
  useCallback,
9
+ useContext,
8
10
  useImperativeHandle,
9
11
  useRef,
10
12
  } from "react";
@@ -13,6 +15,14 @@ import { mergeRefs } from "./utils";
13
15
  import { Controller, useFormContext } from "react-hook-form";
14
16
  import MaskInput, { Mask, MaskArray } from "react-native-mask-input";
15
17
 
18
+ export interface EvervaultInputContextValue {
19
+ validationMode: "onChange" | "onBlur" | "onTouched" | "all";
20
+ }
21
+
22
+ export const EvervaultInputContext = createContext<EvervaultInputContextValue>({
23
+ validationMode: "all",
24
+ });
25
+
16
26
  export type EvervaultInput = Pick<
17
27
  TextInput,
18
28
  | "isFocused"
@@ -90,6 +100,8 @@ export const EvervaultInput = forwardRef<
90
100
  EvervaultInput,
91
101
  EvervaultInputProps<Record<string, unknown>>
92
102
  >(function EvervaultInput({ name, mask, ...props }, ref) {
103
+ const { validationMode } = useContext(EvervaultInputContext);
104
+
93
105
  const inputRef = useForwardedInputRef(ref);
94
106
 
95
107
  const methods = useFormContext();
@@ -108,10 +120,14 @@ export const EvervaultInput = forwardRef<
108
120
  ref={mergeRefs(inputRef, field.ref)}
109
121
  editable={!field.disabled && (props.editable ?? true)}
110
122
  onBlur={(evt) => {
123
+ const shouldValidate =
124
+ validationMode === "onBlur" ||
125
+ validationMode === "onTouched" ||
126
+ validationMode === "all";
111
127
  methods.setValue(field.name, field.value, {
112
128
  shouldDirty: true,
113
129
  shouldTouch: true,
114
- shouldValidate: true,
130
+ shouldValidate,
115
131
  });
116
132
  props.onBlur?.(evt);
117
133
  }}
@@ -119,9 +135,13 @@ export const EvervaultInput = forwardRef<
119
135
  maskAutoComplete={!!mask}
120
136
  value={field.value}
121
137
  onChangeText={(masked, unmasked) => {
138
+ const shouldValidate =
139
+ (validationMode === "onTouched" && fieldState.isTouched) ||
140
+ ((validationMode === "onChange" || validationMode === "all") &&
141
+ (!!fieldState.error || fieldState.isTouched));
122
142
  methods.setValue(field.name, unmasked, {
123
143
  shouldDirty: true,
124
- shouldValidate: !!fieldState.error || fieldState.isTouched,
144
+ shouldValidate,
125
145
  });
126
146
  }}
127
147
  // Remove unwanted props