Skip to content

Commit

Permalink
fix: check new types for validation (#14)
Browse files Browse the repository at this point in the history
* fix: check new types for validation

* fix: typo

* fix: typo

* fix: fixed submit and validation in the forms

* fix: fixed button wrapepr styels

* fix: fixed lint

* fix: types for business type picker

* fix: moved condition teh the funtion

* fix: allow undefined in isRequiredFulfilled funtion
  • Loading branch information
barttom authored Jan 30, 2024
1 parent 1d3a102 commit dca6b06
Show file tree
Hide file tree
Showing 29 changed files with 138 additions and 96 deletions.
8 changes: 4 additions & 4 deletions src/components/Form/FormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import FormContext from './FormContext';
import FormWrapper from './FormWrapper';
import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types';
import type {BaseInputProps, FormProps, InputRefs, OnyxDraftFormValuesFields, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types';

// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web.
// 200ms delay was chosen as a result of empirical testing.
Expand Down Expand Up @@ -48,13 +48,13 @@ type FormProviderOnyxProps = {
network: OnyxEntry<Network>;
};

type FormProviderProps<TFormID extends OnyxFormKey = OnyxFormKey> = FormProviderOnyxProps &
type FormProviderProps<TFormID extends OnyxFormKeyWithoutDraft = OnyxFormKeyWithoutDraft> = FormProviderOnyxProps &
FormProps<TFormID> & {
/** Children to render. */
children: ((props: {inputValues: OnyxFormValues<TFormID>}) => ReactNode) | ReactNode;

/** Callback to validate the form */
validate?: (values: OnyxFormValuesFields<TFormID>) => Errors;
validate?: (values: OnyxFormValuesFields<TFormID> & OnyxDraftFormValuesFields<`${TFormID}Draft`>) => Errors;

/** Should validate function be called when input loose focus */
shouldValidateOnBlur?: boolean;
Expand Down Expand Up @@ -355,4 +355,4 @@ export default withOnyx<FormProviderProps, FormProviderOnyxProps>({
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
key: (props) => `${props.formID}Draft` as any,
},
})(forwardRef(FormProvider)) as <TFormID extends OnyxFormKey>(props: Omit<FormProviderProps<TFormID>, keyof FormProviderOnyxProps>) => ReactNode;
})(forwardRef(FormProvider)) as <TFormID extends OnyxFormKeyWithoutDraft>(props: Omit<FormProviderProps<TFormID>, keyof FormProviderOnyxProps>) => ReactNode;
5 changes: 1 addition & 4 deletions src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useCallback, useMemo, useRef} from 'react';
import type {RefObject} from 'react';
import type {StyleProp, View, ViewStyle} from 'react-native';
import type {View} from 'react-native';
import {Keyboard, ScrollView} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
Expand All @@ -25,9 +25,6 @@ type FormWrapperOnyxProps = {
type FormWrapperProps = ChildrenProps &
FormWrapperOnyxProps &
FormProps & {
/** Submit button styles */
submitButtonStyles?: StyleProp<ViewStyle>;

/** Server side errors keyed by microtime */
errors: Errors;

Expand Down
39 changes: 36 additions & 3 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type AmountTextInput from '@components/AmountTextInput';
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
import type Picker from '@components/Picker';
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
import type StatePicker from '@components/StatePicker';
import type TextInput from '@components/TextInput';
import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type Form from '@src/types/onyx/Form';
import type {BaseForm, FormValueType} from '@src/types/onyx/Form';
Expand All @@ -17,7 +19,15 @@ import type {BaseForm, FormValueType} from '@src/types/onyx/Form';
* TODO: Add remaining inputs here once these components are migrated to Typescript:
* CountrySelector | StatePicker | DatePicker | EmojiPickerButtonDropdown | RoomNameInput | ValuePicker
*/
type ValidInputs = typeof TextInput | typeof AmountTextInput | typeof SingleChoiceQuestion | typeof CheckboxWithLabel | typeof Picker | typeof AddressSearch;
type ValidInputs =
| typeof TextInput
| typeof AmountTextInput
| typeof SingleChoiceQuestion
| typeof CheckboxWithLabel
| typeof Picker
| typeof AddressSearch
| typeof BusinessTypePicker
| typeof StatePicker;

type ValueTypeKey = 'string' | 'boolean' | 'date';

Expand Down Expand Up @@ -51,16 +61,25 @@ type InputWrapperProps<TInput extends ValidInputs> = Omit<BaseInputProps, 'ref'>
type ExcludeDraft<T> = T extends `${string}Draft` ? never : T;
type OnyxFormKeyWithoutDraft = ExcludeDraft<OnyxFormKey>;

type DraftOnly<T> = T extends `${string}Draft` ? T : never;
type OnyxFormKeyOnlyDraft = DraftOnly<OnyxFormKey>;

type OnyxDraftFormValues<TOnyxKey extends OnyxFormKeyOnlyDraft & keyof OnyxValues = OnyxFormKeyOnlyDraft> = OnyxValues[TOnyxKey];
type OnyxDraftFormValuesFields<TOnyxKey extends OnyxFormKeyOnlyDraft & keyof OnyxValues = OnyxFormKeyOnlyDraft> = Omit<OnyxFormValues<TOnyxKey>, keyof BaseForm>;

type OnyxFormValues<TOnyxKey extends OnyxFormKey & keyof OnyxValues = OnyxFormKey> = OnyxValues[TOnyxKey];
type OnyxFormValuesFields<TOnyxKey extends OnyxFormKey & keyof OnyxValues = OnyxFormKey> = Omit<OnyxFormValues<TOnyxKey>, keyof BaseForm>;

type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {
type FormProps<TFormID extends OnyxFormKeyWithoutDraft = OnyxFormKeyWithoutDraft> = {
/** A unique Onyx key identifying the form */
formID: TFormID;

/** Text to be displayed in the submit button */
submitButtonText: string;

/** Submit button styles */
submitButtonStyles?: StyleProp<ViewStyle>;

/** Controls the submit button's visibility */
isSubmitButtonVisible?: boolean;

Expand Down Expand Up @@ -90,4 +109,18 @@ type RegisterInput = <TInputProps extends BaseInputProps>(inputID: keyof Form, i

type InputRefs = Record<string, MutableRefObject<BaseInputProps>>;

export type {InputWrapperProps, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft};
export type {
InputWrapperProps,
FormProps,
RegisterInput,
ValidInputs,
BaseInputProps,
ValueTypeKey,
OnyxFormValues,
OnyxFormValuesFields,
InputRefs,
OnyxFormKeyWithoutDraft,
OnyxFormKeyOnlyDraft,
OnyxDraftFormValues,
OnyxDraftFormValuesFields,
};
6 changes: 3 additions & 3 deletions src/hooks/useReimbursementAccountStepFormSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {useCallback} from 'react';
import * as FormActions from '@userActions/FormActions';
import type {OnyxFormKeyWithoutDraft} from '@userActions/FormActions';
import ONYXKEYS from '@src/ONYXKEYS';
import type {FormValues} from '@src/types/onyx/Form';
import type {ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';
import type {SubStepProps} from './useSubStep/types';

type UseReimbursementAccountStepFormSubmitParams = Pick<SubStepProps, 'isEditing' | 'onNext'> & {
formId?: OnyxFormKeyWithoutDraft;
fieldIds: Array<keyof FormValues>;
fieldIds: Array<keyof ReimbursementAccountDraftValues>;
};

export default function useReimbursementAccountStepFormSubmit({
Expand All @@ -17,7 +17,7 @@ export default function useReimbursementAccountStepFormSubmit({
fieldIds,
}: UseReimbursementAccountStepFormSubmitParams) {
return useCallback(
(values: FormValues) => {
(values: ReimbursementAccountDraftValues) => {
if (isEditing) {
const stepValues = fieldIds.reduce(
(acc, key) => ({
Expand Down
6 changes: 5 additions & 1 deletion src/libs/ErrorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ type OnyxDataWithErrors = {
errors?: Errors | null;
};

function getLatestErrorMessage<TOnyxData extends OnyxDataWithErrors>(onyxData: TOnyxData): string {
function getLatestErrorMessage<TOnyxData extends OnyxDataWithErrors>(onyxData: TOnyxData | null): string {
if (!onyxData) {
return '';
}

const errors = onyxData.errors ?? {};

if (Object.keys(errors).length === 0) {
Expand Down
12 changes: 8 additions & 4 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {URL_REGEX_WITH_REQUIRED_PROTOCOL} from 'expensify-common/lib/Url';
import isDate from 'lodash/isDate';
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
import type {OnyxFormValuesFields} from '@components/Form/types';
import CONST from '@src/CONST';
import type {Report} from '@src/types/onyx';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import * as CardUtils from './CardUtils';
import DateUtils from './DateUtils';
import * as LoginUtils from './LoginUtils';
Expand Down Expand Up @@ -73,7 +73,11 @@ function isValidPastDate(date: string | Date): boolean {
/**
* Used to validate a value that is "required".
*/
function isRequiredFulfilled(value: string | Date | unknown[] | Record<string, unknown>): boolean {
function isRequiredFulfilled(value?: string | boolean | Date | unknown[] | Record<string, unknown>): boolean {
if (!value) {
return false;
}

if (typeof value === 'string') {
return !StringUtils.isEmptyString(value);
}
Expand All @@ -92,11 +96,11 @@ type GetFieldRequiredErrorsReturn<K extends string[]> = {[P in K[number]]: strin
/**
* Used to add requiredField error to the fields passed.
*/
function getFieldRequiredErrors<T extends OnyxCommon.Errors, K extends string[]>(values: T, requiredFields: K): GetFieldRequiredErrorsReturn<K> {
function getFieldRequiredErrors<T extends OnyxFormValuesFields, K extends string[]>(values: T, requiredFields: K): GetFieldRequiredErrorsReturn<K> {
const errors: GetFieldRequiredErrorsReturn<K> = {} as GetFieldRequiredErrorsReturn<K>;

requiredFields.forEach((fieldKey: K[number]) => {
if (isRequiredFulfilled(values[fieldKey])) {
if (isRequiredFulfilled(values[fieldKey as keyof OnyxFormValuesFields])) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions src/pages/ReimbursementAccount/BankInfo/substeps/Manual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ExampleCheckImage from '@pages/ReimbursementAccount/ExampleCheck';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccount} from '@src/types/onyx';
import type {ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

type ManualOnyxProps = {
/** Reimbursement account from ONYX */
Expand Down Expand Up @@ -40,7 +41,7 @@ function Manual({reimbursementAccount, onNext}: ManualProps) {
* @returns {Object}
*/
const validate = useCallback(
(values: FormValues) => {
(values: ReimbursementAccountDraftValues) => {
const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
const routingNumber = values.routingNumber?.trim();

Expand Down Expand Up @@ -71,7 +72,6 @@ function Manual({reimbursementAccount, onNext}: ManualProps) {
validate={validate}
submitButtonText={translate('common.next')}
style={[styles.mh5, styles.flexGrow1]}
shouldSaveDraft
>
<Text style={[styles.textHeadline, styles.mb3]}>{translate('bankAccount.manuallyAdd')}</Text>
<Text style={[styles.mb5, styles.textLabel]}>{translate('bankAccount.checkHelpLine')}</Text>
Expand Down
25 changes: 12 additions & 13 deletions src/pages/ReimbursementAccount/BankInfo/substeps/Plaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PlaidData, ReimbursementAccount, ReimbursementAccountFormDraft} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type {ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

type PlaidOnyxProps = {
/** Reimbursement account from ONYX */
Expand All @@ -28,27 +29,24 @@ type PlaidOnyxProps = {

type PlaidProps = PlaidOnyxProps & SubStepProps;

type ValuesType = {
selectedPlaidAccountID: string;
};

const BANK_INFO_STEP_KEYS = CONST.BANK_ACCOUNT.BANK_INFO_STEP.INPUT_KEY;

const validate = (values: ReimbursementAccountDraftValues): Errors => {
const errorFields: Errors = {};

if (!values.selectedPlaidAccountID) {
errorFields.selectedPlaidAccountID = 'bankAccount.error.youNeedToSelectAnOption';
}

return errorFields;
};

function Plaid({reimbursementAccount, reimbursementAccountDraft, onNext, plaidData}: PlaidProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const isFocused = useIsFocused();
const selectedPlaidAccountID = reimbursementAccountDraft?.[BANK_INFO_STEP_KEYS.PLAID_ACCOUNT_ID] ?? '';

const validate = useCallback((values: ValuesType): Errors => {
const errorFields: Errors = {};
if (!values.selectedPlaidAccountID) {
errorFields.selectedPlaidAccountID = 'bankAccount.error.youNeedToSelectAnOption';
}

return errorFields;
}, []);

useEffect(() => {
const plaidBankAccounts = plaidData?.bankAccounts ?? [];
if (isFocused || plaidBankAccounts.length) {
Expand Down Expand Up @@ -88,6 +86,7 @@ function Plaid({reimbursementAccount, reimbursementAccountDraft, onNext, plaidDa
style={[styles.mh5, styles.flexGrow1]}
>
<InputWrapper
// @ts-expect-error TODO: Remove this once AddPlaidBankAccount (https://github.com/Expensify/App/issues/25119) is migrated to TypeScript
InputComponent={AddPlaidBankAccount}
text={translate('bankAccount.plaidBodyCopy')}
onSelect={(plaidAccountID: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import AddressForm from '@pages/ReimbursementAccount/AddressForm';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccountFormDraft} from '@src/types/onyx';
import type {BeneficialOwnerDraftData} from '@src/types/onyx/ReimbursementAccountDraft';
import type {BeneficialOwnerDraftData, ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

const BENEFICIAL_OWNER_INFO_KEY = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA;
const BENEFICIAL_OWNER_PREFIX = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA.PREFIX;
Expand Down Expand Up @@ -44,7 +44,7 @@ function AddressUBO({reimbursementAccountDraft, onNext, isEditing, beneficialOwn
zipCode: reimbursementAccountDraft?.[inputKeys.zipCode] ?? '',
};

const validate = (values: FormValues) => {
const validate = (values: ReimbursementAccountDraftValues) => {
const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields);

if (values[inputKeys.street] && !ValidationUtils.isValidAddress(values[inputKeys.street])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccountFormDraft} from '@src/types/onyx';
import type {BeneficialOwnerDraftData} from '@src/types/onyx/ReimbursementAccountDraft';
import type {BeneficialOwnerDraftData, ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

const DOB = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA.DOB;
const BENEFICIAL_OWNER_PREFIX = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA.PREFIX;
Expand All @@ -38,7 +38,7 @@ function DateOfBirthUBO({reimbursementAccountDraft, onNext, isEditing, beneficia
const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE);
const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT);

const validate = (values: FormValues) => {
const validate = (values: ReimbursementAccountDraftValues) => {
const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields);

if (values[dobInputID]) {
Expand Down Expand Up @@ -68,7 +68,8 @@ function DateOfBirthUBO({reimbursementAccountDraft, onNext, isEditing, beneficia
submitButtonStyles={[styles.pb5, styles.mb0]}
>
<Text style={[styles.textHeadline, styles.mb3]}>{translate('beneficialOwnerInfoStep.enterTheDateOfBirthOfTheOwner')}</Text>
<InputWrapper
{/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */}
<InputWrapper<unknown>
InputComponent={DatePicker}
inputID={dobInputID}
label={translate('common.dob')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccountFormDraft} from '@src/types/onyx';
import type {BeneficialOwnerDraftData} from '@src/types/onyx/ReimbursementAccountDraft';
import type {BeneficialOwnerDraftData, ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

const SSN_LAST_4 = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA.SSN_LAST_4;
const BENEFICIAL_OWNER_PREFIX = CONST.BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA.PREFIX;
Expand All @@ -34,7 +34,7 @@ function SocialSecurityNumberUBO({reimbursementAccountDraft, onNext, isEditing,
const defaultSsnLast4 = reimbursementAccountDraft?.[ssnLast4InputID] ?? '';
const stepFields = [ssnLast4InputID];

const validate = (values: FormValues) => {
const validate = (values: ReimbursementAccountDraftValues) => {
const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields);
if (values[ssnLast4InputID] && !ValidationUtils.isValidSSNLastFour(values[ssnLast4InputID])) {
errors[ssnLast4InputID] = 'bankAccount.error.ssnLast4';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import AddressForm from '@pages/ReimbursementAccount/AddressForm';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccount} from '@src/types/onyx';
import type {FormValues} from '@src/types/onyx/Form';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

type AddressBusinessOnyxProps = {
/** Reimbursement account from ONYX */
Expand All @@ -33,7 +33,7 @@ const INPUT_KEYS = {

const STEP_FIELDS = [COMPANY_BUSINESS_INFO_KEY.STREET, COMPANY_BUSINESS_INFO_KEY.CITY, COMPANY_BUSINESS_INFO_KEY.STATE, COMPANY_BUSINESS_INFO_KEY.ZIP_CODE];

const validate = (values: FormValues): OnyxCommon.Errors => {
const validate = (values: ReimbursementAccountDraftValues): OnyxCommon.Errors => {
const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);

if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReimbursementAccount, ReimbursementAccountFormDraft} from '@src/types/onyx';
import type {FormValues} from '@src/types/onyx/Form';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {ReimbursementAccountDraftValues} from '@src/types/onyx/ReimbursementAccountDraft';

type ConfirmationBusinessOnyxProps = {
/** Reimbursement account from ONYX */
Expand All @@ -37,7 +37,7 @@ type States = keyof typeof COMMON_CONST.STATES;

const BUSINESS_INFO_STEP_KEYS = CONST.BANK_ACCOUNT.BUSINESS_INFO_STEP.INPUT_KEY;

const validate = (values: FormValues): OnyxCommon.Errors => {
const validate = (values: ReimbursementAccountDraftValues): OnyxCommon.Errors => {
const errors = ValidationUtils.getFieldRequiredErrors(values, [BUSINESS_INFO_STEP_KEYS.HAS_NO_CONNECTION_TO_CANNABIS]);

if (!values.hasNoConnectionToCannabis) {
Expand Down
Loading

0 comments on commit dca6b06

Please sign in to comment.