Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: check new types for validation #14

Merged
merged 9 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
barttom marked this conversation as resolved.
Show resolved Hide resolved

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
8 changes: 4 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,7 @@ 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 (typeof value === 'string') {
return !StringUtils.isEmptyString(value);
}
Expand All @@ -92,11 +92,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
Loading