diff --git a/eslint.config.js b/eslint.config.js index 805e877da6e2..1d021ecf75c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -396,10 +396,6 @@ const config = defineConfig([ { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { - // These rules could indicate potential bugs in the source code. - // After fixing the source code, remove these so they become errors instead of warnings. - '@typescript-eslint/no-base-to-string': 'warn', - // @typescript-eslint/lines-between-class-members was moved to @stylistic/eslint-plugin, so replaced with lines-between-class-members. 'lines-between-class-members': 'error', '@typescript-eslint/lines-between-class-members': 'off', diff --git a/package.json b/package.json index 7947f900ada1..742d617fa81f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=241 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=154 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/scripts/utils/CLI.ts b/scripts/utils/CLI.ts index db005eaa1425..5dd54959d0e1 100644 --- a/scripts/utils/CLI.ts +++ b/scripts/utils/CLI.ts @@ -3,6 +3,7 @@ * You provide a CLIConfig defining your arguments, then the class will handle parsing argv, type validation, error handling, and help messages. */ import type {NonEmptyObject, NonEmptyTuple, ValueOf, Writable} from 'type-fest'; +import SafeString from '@src/utils/SafeString'; /** * A base CLI arg has only a description, which we will use in the help/usage message (built-in to any CLI). @@ -292,7 +293,7 @@ class CLI { if (Object.keys(namedArgs).length > 0) { console.log('Named Arguments:'); for (const [name, spec] of Object.entries(namedArgs)) { - const defaultLabel = spec.default !== undefined ? ` (default: ${String(spec.default)})` : ''; + const defaultLabel = spec.default !== undefined ? ` (default: ${SafeString(spec.default)})` : ''; const supersededLabel = spec.supersedes && spec.supersedes.length > 0 ? ` (supersedes: ${spec.supersedes.join(', ')})` : ''; console.log(` --${name.padEnd(20)} ${spec.description}${defaultLabel}${supersededLabel}`); } @@ -302,7 +303,7 @@ class CLI { if (positionalArgs.length > 0) { console.log('Positional Arguments:'); for (const arg of positionalArgs) { - const defaultLabel = arg.default !== undefined ? ` (default: ${String(arg.default)})` : ''; + const defaultLabel = arg.default !== undefined ? ` (default: ${SafeString(arg.default)})` : ''; console.log(` ${arg.name.padEnd(22)} ${arg.description}${defaultLabel}`); } console.log(''); diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index c20c87b66717..d875186024db 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -30,6 +30,7 @@ import type {ColorValue} from '@styles/utils/types'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; @@ -313,7 +314,7 @@ function AttachmentView({ ); } - imageSource = previewSource?.toString() ?? imageSource; + imageSource = SafeString(previewSource) || imageSource; } return ( diff --git a/src/components/TextPicker/TextSelectorModal.tsx b/src/components/TextPicker/TextSelectorModal.tsx index 5d0b1e3fc249..72c420e147ba 100644 --- a/src/components/TextPicker/TextSelectorModal.tsx +++ b/src/components/TextPicker/TextSelectorModal.tsx @@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; import type {TextSelectorModalProps} from './types'; function TextSelectorModal({ @@ -148,7 +149,7 @@ function TextSelectorModal({ ref={inputCallbackRef} InputComponent={TextInput} value={currentValue} - onValueChange={(changedValue) => setValue(changedValue.toString())} + onValueChange={(changedValue) => setValue(SafeString(changedValue))} // eslint-disable-next-line react/jsx-props-no-spreading {...rest} inputID={rest.inputID} diff --git a/src/libs/Console/index.ts b/src/libs/Console/index.ts index 6c344c3233e3..672683ebb889 100644 --- a/src/libs/Console/index.ts +++ b/src/libs/Console/index.ts @@ -6,6 +6,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Log} from '@src/types/onyx'; +import SafeString from '@src/utils/SafeString'; let shouldStoreLogs = false; @@ -51,7 +52,7 @@ function logMessage(args: unknown[]) { } } - return String(arg); + return SafeString(arg); }) .join(' '); const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message, extraData: ''}; @@ -107,7 +108,7 @@ function createLog(text: string) { if (result !== undefined) { return [ {time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}, - {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result), extraData: ''}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: SafeString(result), extraData: ''}, ]; } return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}]; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 873cda03089c..3fd01df806f3 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -9,6 +9,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type {Beta, Report, ReportAction, ReportActions, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Comment} from '@src/types/onyx/Transaction'; +import SafeString from '@src/utils/SafeString'; import {getLinkedTransactionID} from './ReportActionsUtils'; import {getReasonAndReportActionThatRequiresAttention, reasonForReportToBeInOptionList} from './ReportUtils'; import SidebarUtils from './SidebarUtils'; @@ -108,7 +109,7 @@ function onyxDataToString(data: OnyxEntry) { return stringifyJSON(data as Record); } - return String(data); + return SafeString(data); } type OnyxDataType = 'number' | 'object' | 'string' | 'boolean' | 'undefined'; @@ -166,7 +167,7 @@ function compareStringWithOnyxData(text: string, data: OnyxEntry) { return text === stringifyJSON(data as Record); } - return text === String(data); + return text === SafeString(data); } /** diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index f73b4cadf846..6e0fbacd9d27 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -5,6 +5,7 @@ import ROUTES from '@src/ROUTES'; import type {OnyxInputOrEntry, PersonalDetails, Policy, Report} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {SearchPolicy} from '@src/types/onyx/SearchResults'; +import SafeString from '@src/utils/SafeString'; import type {IOURequestType} from './actions/IOU'; import {getCurrencyUnit} from './CurrencyUtils'; import Navigation from './Navigation/Navigation'; @@ -217,7 +218,7 @@ function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: s email: currentUser?.login ?? '', login: currentUser?.login ?? '', displayName: currentUser.displayName ?? '', - avatarUrl: currentUser.avatar?.toString() ?? '', + avatarUrl: SafeString(currentUser.avatar), accountID: currentUser.accountID, text: currentUser.login, selected: true, diff --git a/src/libs/MergeTransactionUtils.ts b/src/libs/MergeTransactionUtils.ts index 76ad0e4a68a6..21c8f6d76b67 100644 --- a/src/libs/MergeTransactionUtils.ts +++ b/src/libs/MergeTransactionUtils.ts @@ -7,6 +7,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type {MergeTransaction, Transaction} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {Receipt} from '@src/types/onyx/Transaction'; +import SafeString from '@src/utils/SafeString'; import {convertToDisplayString} from './CurrencyUtils'; import getReceiptFilenameFromTransaction from './getReceiptFilenameFromTransaction'; import Parser from './Parser'; @@ -365,19 +366,19 @@ function getDisplayValue(field: MergeFieldKey, transaction: Transaction, transla return convertToDisplayString(Number(fieldValue), getCurrency(transaction)); } if (field === 'description') { - return StringUtils.lineBreaksToSpaces(Parser.htmlToText(fieldValue.toString())); + return StringUtils.lineBreaksToSpaces(Parser.htmlToText(SafeString(fieldValue))); } if (field === 'tag') { - return getCommaSeparatedTagNameWithSanitizedColons(fieldValue.toString()); + return getCommaSeparatedTagNameWithSanitizedColons(SafeString(fieldValue)); } if (field === 'reportID') { - return fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID ? translate('common.none') : getReportName(getReportOrDraftReport(fieldValue.toString())); + return fieldValue === CONST.REPORT.UNREPORTED_REPORT_ID ? translate('common.none') : getReportName(getReportOrDraftReport(SafeString(fieldValue))); } if (field === 'attendees') { return Array.isArray(fieldValue) ? getAttendeesListDisplayString(fieldValue) : ''; } - return String(fieldValue); + return SafeString(fieldValue); } /** * Build merge fields data array from conflict fields for UI display diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 302f836295d6..7c4c1c8abd46 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -8,6 +8,7 @@ import {getTextFromHtml} from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import type {Report, ReportAction} from '@src/types/onyx'; +import SafeString from '@src/utils/SafeString'; import focusApp from './focusApp'; import type {LocalNotificationClickHandler, LocalNotificationData, LocalNotificationModifiedExpensePushParams} from './types'; @@ -64,7 +65,7 @@ function push( const notificationID = Str.guid(); notificationCache[notificationID] = new Notification(title, { body, - icon: String(icon), + icon: SafeString(icon), data, silent: true, tag, diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index c9cf3710eb18..fe0c7c66befe 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -1,4 +1,5 @@ import CONST from '@src/CONST'; +import SafeString from '@src/utils/SafeString'; import {WRITE_COMMANDS} from './API/types'; import Log from './Log'; import type {RequestError} from './Network/SequentialQueue'; @@ -21,7 +22,7 @@ class RequestThrottle { this.requestWaitTime = 0; this.requestRetryCount = 0; if (this.timeoutID) { - Log.info(`[RequestThrottle - ${this.name}] clearing timeoutID: ${String(this.timeoutID)}`); + Log.info(`[RequestThrottle - ${this.name}] clearing timeoutID: ${SafeString(this.timeoutID)}`); clearTimeout(this.timeoutID); this.timeoutID = undefined; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 6e2720623c66..b02b70cdb85e 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -77,6 +77,7 @@ import type { WaypointCollection, } from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import SafeString from '@src/utils/SafeString'; import getDistanceInMeters from './getDistanceInMeters'; type TransactionParams = { @@ -1780,7 +1781,7 @@ function compareDuplicateTransactionFields( // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(report?.policyID); - const areAllFieldsEqualForKey = areAllFieldsEqual(transactions, (item) => keys.map((key) => item?.[key]).join('|')); + const areAllFieldsEqualForKey = areAllFieldsEqual(transactions, (item) => keys.map((key) => SafeString(item?.[key])).join('|')); if (fieldName === 'description') { const allCommentsAreEqual = areAllCommentsEqual(transactions, firstTransaction); const allCommentsAreEmpty = isFirstTransactionCommentEmptyObject && transactions.every((item) => getDescription(item) === ''); diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 44a115746bec..7cd4681d77ef 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -6,6 +6,7 @@ import type {Reservation, ReservationTimeDetails, ReservationType} from '@src/ty import type Transaction from '@src/types/onyx/Transaction'; import type {AirPnr, CarPnr, HotelPnr, Pnr, PnrData, PnrTraveler, RailPnr, TripData} from '@src/types/onyx/TripData'; import type IconAsset from '@src/types/utils/IconAsset'; +import SafeString from '@src/utils/SafeString'; import {getMoneyRequestSpendBreakdown} from './ReportUtils'; function getTripReservationIcon(reservationType?: ReservationType): IconAsset { @@ -82,7 +83,7 @@ function parseDurationToSeconds(duration: string): number { function getSeatByLegAndFlight(travelerInfo: ArrayValues, legIdx: number, flightIdx: number): string | undefined { const seats = travelerInfo.booking?.seats?.filter((seat) => seat.legIdx === legIdx && seat.flightIdx === flightIdx); if (seats && seats.length > 0) { - return seats.join(', '); + return seats.map(SafeString).join(', '); } return ''; } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index a9cd654ad3f8..fc77ce4abb67 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -45,6 +45,7 @@ import type {PaymentInformation} from '@src/types/onyx/LastPaymentMethod'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type Nullable from '@src/types/utils/Nullable'; +import SafeString from '@src/utils/SafeString'; import {setPersonalBankAccountContinueKYCOnSuccess} from './BankAccounts'; import {setOptimisticTransactionThread} from './Report'; import {saveLastSearchParams} from './ReportNavigation'; @@ -640,7 +641,7 @@ function exportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDLi if (Array.isArray(value)) { formData.append(key, value.join(',')); } else { - formData.append(key, String(value)); + formData.append(key, SafeString(value)); } }); diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index 602754bef556..330fa3ec6aab 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -12,11 +12,12 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as BankAccounts from '@userActions/BankAccounts'; +import {answerQuestionsForWallet} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalQuestionDetails} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; +import SafeString from '@src/utils/SafeString'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; @@ -87,7 +88,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { } } - BankAccounts.answerQuestionsForWallet(tempAnswers, idNumber); + answerQuestionsForWallet(tempAnswers, idNumber); setUserAnswers(tempAnswers); } else { // Else, show next question @@ -127,7 +128,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { possibleAnswers={possibleAnswers} currentQuestionIndex={currentQuestionIndex} onValueChange={(value) => { - chooseAnswer(String(value)); + chooseAnswer(SafeString(value)); }} onInputChange={() => {}} /> diff --git a/src/pages/ReimbursementAccount/EnterSignerInfo/utils/getSignerDetailsAndSignerFiles.ts b/src/pages/ReimbursementAccount/EnterSignerInfo/utils/getSignerDetailsAndSignerFiles.ts index 7ce2c8628ac9..d906f2b88f7c 100644 --- a/src/pages/ReimbursementAccount/EnterSignerInfo/utils/getSignerDetailsAndSignerFiles.ts +++ b/src/pages/ReimbursementAccount/EnterSignerInfo/utils/getSignerDetailsAndSignerFiles.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {EnterSignerInfoForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/EnterSignerInfoForm'; import type {FileObject} from '@src/types/utils/Attachment'; +import SafeString from '@src/utils/SafeString'; const signerDetailsFields = [ INPUT_IDS.SIGNER_FULL_NAME, @@ -32,7 +33,7 @@ function getSignerDetailsAndSignerFilesForSignerInfo(enterSignerInfoFormDraft: O if (fieldName === INPUT_IDS.SIGNER_STREET || fieldName === INPUT_IDS.SIGNER_CITY || fieldName === INPUT_IDS.SIGNER_STATE || fieldName === INPUT_IDS.SIGNER_ZIP_CODE) { signerDetails[INPUT_IDS.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS] = signerDetails[INPUT_IDS.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS] - ? `${String(signerDetails[INPUT_IDS.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS])}, ${String(enterSignerInfoFormDraft?.[fieldName])}` + ? `${SafeString(signerDetails[INPUT_IDS.SIGNER_COMPLETE_RESIDENTIAL_ADDRESS])}, ${String(enterSignerInfoFormDraft?.[fieldName])}` : enterSignerInfoFormDraft?.[fieldName]; return; } diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/AccountHolderDetails.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/AccountHolderDetails.tsx index 00dbc65a0b6a..eeec7d97126a 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/AccountHolderDetails.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/AccountHolderDetails.tsx @@ -19,6 +19,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {CorpayFormField} from '@src/types/onyx'; +import SafeString from '@src/utils/SafeString'; const {ACCOUNT_HOLDER_COUNTRY} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; const {COUNTRY, ACCOUNT_HOLDER_NAME} = INPUT_IDS.ADDITIONAL_DATA; @@ -77,7 +78,7 @@ function AccountHolderDetails({onNext, isEditing, corpayFields}: BankInfoSubStep return; } - if (new RegExp(rule.regEx).test(values[fieldID] ? String(values[fieldID]) : '')) { + if (new RegExp(rule.regEx).test(values[fieldID] ? SafeString(values[fieldID]) : '')) { return; } @@ -93,7 +94,7 @@ function AccountHolderDetails({onNext, isEditing, corpayFields}: BankInfoSubStep const inputs = useMemo(() => { return accountHolderDetailsFields?.map((field) => { if (field.valueSet !== undefined) { - return getInputForValueSet(field, String(defaultValues[field.id as keyof typeof defaultValues]), isEditing, styles); + return getInputForValueSet(field, SafeString(defaultValues[field.id as keyof typeof defaultValues]), isEditing, styles); } if (field.id === ACCOUNT_HOLDER_COUNTRY) { @@ -127,7 +128,7 @@ function AccountHolderDetails({onNext, isEditing, corpayFields}: BankInfoSubStep label={field.label} aria-label={field.label} role={CONST.ROLE.PRESENTATION} - defaultValue={String(defaultValues[field.id as keyof typeof defaultValues]) ?? ''} + defaultValue={SafeString(defaultValues[field.id as keyof typeof defaultValues])} shouldSaveDraft={!isEditing} limitSearchesToCountry={defaultValues.accountHolderCountry || defaultBankAccountCountry} renamedInputKeys={{ diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/BankAccountDetails.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/BankAccountDetails.tsx index cf2bc5a1465c..d7888b51cb2d 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/BankAccountDetails.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/BankAccountDetails.tsx @@ -18,6 +18,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; import type {CorpayFormField} from '@src/types/onyx'; +import SafeString from '@src/utils/SafeString'; function getInputComponent(field: CorpayFormField) { if (CONST.CORPAY_FIELDS.SPECIAL_LIST_ADDRESS_KEYS.includes(field.id)) { @@ -67,7 +68,7 @@ function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepPr return; } - if (new RegExp(rule.regEx).test(values[fieldID] ? String(values[fieldID]) : '')) { + if (new RegExp(rule.regEx).test(SafeString(values[fieldID]))) { return; } @@ -89,7 +90,7 @@ function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepPr const inputs = useMemo(() => { return bankAccountDetailsFields?.map((field) => { if (field.valueSet !== undefined) { - return getInputForValueSet(field, String(defaultValues[field.id as keyof typeof defaultValues]), isEditing, styles); + return getInputForValueSet(field, SafeString(defaultValues[field.id as keyof typeof defaultValues]), isEditing, styles); } return ( @@ -104,7 +105,7 @@ function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepPr aria-label={field.label} role={CONST.ROLE.PRESENTATION} shouldSaveDraft={!isEditing} - defaultValue={String(defaultValues[field.id as keyof typeof defaultValues]) ?? ''} + defaultValue={SafeString(defaultValues[field.id as keyof typeof defaultValues])} limitSearchesToCountry={reimbursementAccountDraft?.country} renamedInputKeys={{ street: 'bankAddressLine1', diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/Confirmation.tsx index 6ba0c18dae44..9eba1ffc0ad9 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/Confirmation.tsx @@ -13,6 +13,7 @@ import getInputKeysForBankInfoStep from '@pages/ReimbursementAccount/NonUSD/util import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import SafeString from '@src/utils/SafeString'; const {ACCOUNT_HOLDER_COUNTRY} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) { @@ -27,7 +28,7 @@ function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) { const items = useMemo( () => corpayFields?.formFields?.map((field) => { - let title = values[field.id as keyof typeof values] ? String(values[field.id as keyof typeof values]) : ''; + let title = SafeString(values[field.id as keyof typeof values]); if (field.id === ACCOUNT_HOLDER_COUNTRY) { title = CONST.ALL_COUNTRIES[title as keyof typeof CONST.ALL_COUNTRIES]; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx index 31a70b9aa512..484f8ba1e9ad 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import SafeString from '@src/utils/SafeString'; type NameProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; @@ -28,10 +29,10 @@ function Address({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBein } as const; const defaultValues = { - street: String(reimbursementAccountDraft?.[inputKeys.street] ?? ''), - city: String(reimbursementAccountDraft?.[inputKeys.city] ?? ''), - state: String(reimbursementAccountDraft?.[inputKeys.state] ?? ''), - zipCode: String(reimbursementAccountDraft?.[inputKeys.zipCode] ?? ''), + street: SafeString(reimbursementAccountDraft?.[inputKeys.street]), + city: SafeString(reimbursementAccountDraft?.[inputKeys.city]), + state: SafeString(reimbursementAccountDraft?.[inputKeys.state]), + zipCode: SafeString(reimbursementAccountDraft?.[inputKeys.zipCode]), country: (reimbursementAccountDraft?.[inputKeys.country] ?? '') as Country | '', }; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx index a5e04ddfa5f6..e7044e738152 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx @@ -8,6 +8,7 @@ import getValuesForBeneficialOwner from '@pages/ReimbursementAccount/NonUSD/util import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import SafeString from '@src/utils/SafeString'; type ConfirmationProps = SubStepProps & {ownerBeingModifiedID: string}; @@ -19,7 +20,7 @@ function Confirmation({onNext, onMove, isEditing, ownerBeingModifiedID}: Confirm const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const values = useMemo(() => getValuesForBeneficialOwner(ownerBeingModifiedID, reimbursementAccountDraft), [ownerBeingModifiedID, reimbursementAccountDraft]); const beneficialOwnerCountryInputID = `${PREFIX}_${ownerBeingModifiedID}_${COUNTRY}` as const; - const beneficialOwnerCountry = String(reimbursementAccountDraft?.[beneficialOwnerCountryInputID] ?? ''); + const beneficialOwnerCountry = SafeString(reimbursementAccountDraft?.[beneficialOwnerCountryInputID]); const policyID = reimbursementAccount?.achData?.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const currency = policy?.outputCurrency ?? ''; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx index 5d1872280634..64d5ceca7ae5 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx @@ -6,6 +6,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; type DateOfBirthProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; @@ -16,7 +17,7 @@ function DateOfBirth({onNext, isEditing, onMove, isUserEnteringHisOwnData, owner const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const dobInputID = `${PREFIX}_${ownerBeingModifiedID}_${DOB}` as const; - const dobDefaultValue = String(reimbursementAccountDraft?.[dobInputID] ?? ''); + const dobDefaultValue = SafeString(reimbursementAccountDraft?.[dobInputID]); const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourDOB' : 'ownershipInfoStep.whatsTheOwnersDOB'); const handleSubmit = useReimbursementAccountStepFormSubmit({ diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Documents.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Documents.tsx index d5ec243b8770..a7ee9e266020 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Documents.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Documents.tsx @@ -17,6 +17,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {FileObject} from '@src/types/utils/Attachment'; +import SafeString from '@src/utils/SafeString'; type DocumentsProps = SubStepProps & {ownerBeingModifiedID: string}; @@ -36,7 +37,7 @@ function Documents({onNext, isEditing, ownerBeingModifiedID}: DocumentsProps) { const addressProofInputID = `${PREFIX}_${ownerBeingModifiedID}_${ADDRESS_PROOF}` as const; const codiceFiscaleInputID = `${PREFIX}_${ownerBeingModifiedID}_${CODICE_FISCALE}` as const; const beneficialOwnerCountryInputID = `${PREFIX}_${ownerBeingModifiedID}_${COUNTRY}` as const; - const beneficialOwnerCountry = String(reimbursementAccountDraft?.[beneficialOwnerCountryInputID] ?? ''); + const beneficialOwnerCountry = SafeString(reimbursementAccountDraft?.[beneficialOwnerCountryInputID]); const isDocumentNeededStatus = getNeededDocumentsStatusForBeneficialOwner(currency, countryStepCountryValue, beneficialOwnerCountry); const defaultValues: Record = { [proofOfOwnershipInputID]: Array.isArray(reimbursementAccountDraft?.[proofOfOwnershipInputID]) ? (reimbursementAccountDraft?.[proofOfOwnershipInputID] ?? []) : [], diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx index 633ccda38062..acd13a9e5e17 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx @@ -8,6 +8,7 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import {getFieldRequiredErrors, isValidSSNLastFour} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; type Last4SSNProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; @@ -18,14 +19,14 @@ function Last4SSN({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBei const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const last4SSNInputID = `${PREFIX}_${ownerBeingModifiedID}_${SSN_LAST_4}` as const; - const defaultLast4SSN = String(reimbursementAccountDraft?.[last4SSNInputID] ?? ''); + const defaultLast4SSN = SafeString(reimbursementAccountDraft?.[last4SSNInputID]); const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourLast' : 'ownershipInfoStep.whatAreTheLast'); const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, [last4SSNInputID]); - if (values[last4SSNInputID] && !isValidSSNLastFour(String(values[last4SSNInputID]))) { + if (values[last4SSNInputID] && !isValidSSNLastFour(SafeString(values[last4SSNInputID]))) { errors[last4SSNInputID] = translate('bankAccount.error.ssnLast4'); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx index fea8feaf2589..797997dd85ee 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx @@ -6,6 +6,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; type NameProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; @@ -20,8 +21,8 @@ function Name({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBeingMo const stepFields = useMemo(() => [firstNameInputID, lastNameInputID], [firstNameInputID, lastNameInputID]); const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourName' : 'ownershipInfoStep.whatsTheOwnersName'); const defaultValues = { - firstName: String(reimbursementAccountDraft?.[firstNameInputID] ?? ''), - lastName: String(reimbursementAccountDraft?.[lastNameInputID] ?? ''), + firstName: SafeString(reimbursementAccountDraft?.[firstNameInputID]), + lastName: SafeString(reimbursementAccountDraft?.[lastNameInputID]), }; const handleSubmit = useReimbursementAccountStepFormSubmit({ diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx index 831ae3481986..dd0a77e48833 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx @@ -8,6 +8,7 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import {getFieldRequiredErrors, isValidOwnershipPercentage} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; type OwnershipPercentageProps = SubStepProps & { isUserEnteringHisOwnData: boolean; @@ -23,14 +24,14 @@ function OwnershipPercentage({onNext, isEditing, onMove, isUserEnteringHisOwnDat const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const ownershipPercentageInputID = `${PREFIX}_${ownerBeingModifiedID}_${OWNERSHIP_PERCENTAGE}` as const; - const defaultOwnershipPercentage = String(reimbursementAccountDraft?.[ownershipPercentageInputID] ?? ''); + const defaultOwnershipPercentage = SafeString(reimbursementAccountDraft?.[ownershipPercentageInputID]); const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYoursPercentage' : 'ownershipInfoStep.whatPercentage'); const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, [ownershipPercentageInputID]); - if (values[ownershipPercentageInputID] && !isValidOwnershipPercentage(String(values[ownershipPercentageInputID]), totalOwnedPercentage, ownerBeingModifiedID)) { + if (values[ownershipPercentageInputID] && !isValidOwnershipPercentage(SafeString(values[ownershipPercentageInputID]), totalOwnedPercentage, ownerBeingModifiedID)) { errors[ownershipPercentageInputID] = translate('bankAccount.error.ownershipPercentage'); } diff --git a/src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBankInfoStep.ts b/src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBankInfoStep.ts index 4266b7311a56..e59c09472c55 100644 --- a/src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBankInfoStep.ts +++ b/src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBankInfoStep.ts @@ -1,6 +1,7 @@ import CONST from '@src/CONST'; import type {ReimbursementAccountForm} from '@src/types/form'; import type {CorpayFields, CorpayFormField} from '@src/types/onyx'; +import SafeString from '@src/utils/SafeString'; import type {SubStepValues} from './getBankInfoStepValues'; /** @@ -20,7 +21,7 @@ function getInitialSubStepForBusinessInfoStep(data: SubStepValues 0) { - const strValue = String(value); + const strValue = SafeString(value); return field.validationRules.some((rule) => { if (!rule.regEx) { return false; diff --git a/src/pages/ReimbursementAccount/NonUSD/utils/getOwnerDetailsAndOwnerFilesForBeneficialOwners.ts b/src/pages/ReimbursementAccount/NonUSD/utils/getOwnerDetailsAndOwnerFilesForBeneficialOwners.ts index 965c580cbffa..3e34d79bab2f 100644 --- a/src/pages/ReimbursementAccount/NonUSD/utils/getOwnerDetailsAndOwnerFilesForBeneficialOwners.ts +++ b/src/pages/ReimbursementAccount/NonUSD/utils/getOwnerDetailsAndOwnerFilesForBeneficialOwners.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {BeneficialOwnerDataKey, ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm'; import type {FileObject} from '@src/types/utils/Attachment'; +import SafeString from '@src/utils/SafeString'; const { FIRST_NAME, @@ -42,25 +43,25 @@ function getOwnerDetailsAndOwnerFilesForBeneficialOwners(ownerKeys: string[], re return; } - if (fieldName === SSN_LAST_4 && String(reimbursementAccountDraft?.[ownerDetailsNationalityKey]) !== CONST.COUNTRY.US) { + if (fieldName === SSN_LAST_4 && SafeString(reimbursementAccountDraft?.[ownerDetailsNationalityKey]) !== CONST.COUNTRY.US) { return; } if (fieldName === OWNERSHIP_PERCENTAGE) { - ownerDetails[ownerDetailsKey] = String(reimbursementAccountDraft?.[ownerDetailsKey]); + ownerDetails[ownerDetailsKey] = SafeString(reimbursementAccountDraft?.[ownerDetailsKey]); return; } if (fieldName === FIRST_NAME || fieldName === LAST_NAME) { ownerDetails[ownerDetailsFullNameKey] = ownerDetails[ownerDetailsFullNameKey] - ? `${String(ownerDetails[ownerDetailsFullNameKey])} ${String(reimbursementAccountDraft[ownerDetailsKey])}` + ? `${SafeString(ownerDetails[ownerDetailsFullNameKey])} ${SafeString(reimbursementAccountDraft[ownerDetailsKey])}` : reimbursementAccountDraft[ownerDetailsKey]; return; } if (fieldName === STREET || fieldName === CITY || fieldName === STATE || fieldName === ZIP_CODE) { ownerDetails[ownerDetailsResidentialAddressKey] = ownerDetails[ownerDetailsResidentialAddressKey] - ? `${String(ownerDetails[ownerDetailsResidentialAddressKey])}, ${String(reimbursementAccountDraft[ownerDetailsKey])}` + ? `${SafeString(ownerDetails[ownerDetailsResidentialAddressKey])}, ${SafeString(reimbursementAccountDraft[ownerDetailsKey])}` : reimbursementAccountDraft[ownerDetailsKey]; return; } diff --git a/src/pages/ReimbursementAccount/NonUSD/utils/getSignerDetailsAndSignerFilesForSignerInfo.ts b/src/pages/ReimbursementAccount/NonUSD/utils/getSignerDetailsAndSignerFilesForSignerInfo.ts index 04eb577f5999..f996d85d571a 100644 --- a/src/pages/ReimbursementAccount/NonUSD/utils/getSignerDetailsAndSignerFilesForSignerInfo.ts +++ b/src/pages/ReimbursementAccount/NonUSD/utils/getSignerDetailsAndSignerFilesForSignerInfo.ts @@ -3,6 +3,7 @@ import CONST from '@src/CONST'; import type {ReimbursementAccountForm} from '@src/types/form'; import type {BeneficialOwnerDataKey, SignerInfoStepProps} from '@src/types/form/ReimbursementAccountForm'; import type {FileObject} from '@src/types/utils/Attachment'; +import SafeString from '@src/utils/SafeString'; const {FULL_NAME, EMAIL, JOB_TITLE, DATE_OF_BIRTH, ADDRESS, STREET, CITY, STATE, ZIP_CODE, PROOF_OF_DIRECTORS, ADDRESS_PROOF, COPY_OF_ID, CODICE_FISCALE, DOWNLOADED_PDS_AND_FSG} = CONST.NON_USD_BANK_ACCOUNT.SIGNER_INFO_STEP.SIGNER_INFO_DATA; @@ -36,7 +37,9 @@ function getSignerDetailsAndSignerFilesForSignerInfo(reimbursementAccountDraft: } if (fieldName === STREET || fieldName === CITY || fieldName === STATE || fieldName === ZIP_CODE) { - signerDetails[ADDRESS] = signerDetails[ADDRESS] ? `${String(signerDetails[ADDRESS])}, ${String(reimbursementAccountDraft?.[fieldName])}` : reimbursementAccountDraft?.[fieldName]; + signerDetails[ADDRESS] = signerDetails[ADDRESS] + ? `${SafeString(signerDetails[ADDRESS])}, ${SafeString(reimbursementAccountDraft?.[fieldName])}` + : reimbursementAccountDraft?.[fieldName]; return; } @@ -53,20 +56,20 @@ function getSignerDetailsAndSignerFilesForSignerInfo(reimbursementAccountDraft: if (fieldName === FIRST_NAME || fieldName === LAST_NAME) { signerDetails[FULL_NAME] = signerDetails[FULL_NAME] - ? `${String(signerDetails[FULL_NAME])} ${String(reimbursementAccountDraft?.[beneficialFieldKey])}` - : String(reimbursementAccountDraft?.[beneficialFieldKey]); + ? `${SafeString(signerDetails[FULL_NAME])} ${SafeString(reimbursementAccountDraft?.[beneficialFieldKey])}` + : SafeString(reimbursementAccountDraft?.[beneficialFieldKey]); return; } if (fieldName === DOB) { - signerDetails[DATE_OF_BIRTH] = String(reimbursementAccountDraft?.[beneficialFieldKey]); + signerDetails[DATE_OF_BIRTH] = SafeString(reimbursementAccountDraft?.[beneficialFieldKey]); return; } if (fieldName === BENEFICIAL_STREET || fieldName === BENEFICIAL_CITY || fieldName === BENEFICIAL_STATE || fieldName === BENEFICIAL_ZIP_CODE) { signerDetails[ADDRESS] = signerDetails[ADDRESS] - ? `${String(signerDetails[ADDRESS])}, ${String(reimbursementAccountDraft?.[beneficialFieldKey])}` - : String(reimbursementAccountDraft?.[beneficialFieldKey]); + ? `${SafeString(signerDetails[ADDRESS])}, ${SafeString(reimbursementAccountDraft?.[beneficialFieldKey])}` + : SafeString(reimbursementAccountDraft?.[beneficialFieldKey]); } }); } diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx index 4e6c3a66b624..7fa734a4b4e0 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx @@ -11,6 +11,7 @@ import {updateBeneficialOwnersForBankAccount} from '@userActions/BankAccounts'; import {setDraftValues} from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; import AddressUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO'; import ConfirmationUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/ConfirmationUBO'; import DateOfBirthUBO from './subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO'; @@ -60,7 +61,7 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { const beneficialOwners = beneficialOwnerKeys.map((ownerKey) => beneficialOwnerFields.reduce( (acc, fieldName) => { - acc[fieldName] = reimbursementAccountDraft ? String(reimbursementAccountDraft[`beneficialOwner_${ownerKey}_${fieldName}`]) : undefined; + acc[fieldName] = reimbursementAccountDraft ? SafeString(reimbursementAccountDraft[`beneficialOwner_${ownerKey}_${fieldName}`]) : undefined; return acc; }, {} as Record, diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO.tsx index c8d57f4fccf2..36ed72065885 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/AddressUBO.tsx @@ -6,6 +6,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; 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; @@ -25,10 +26,10 @@ function AddressUBO({onNext, onMove, isEditing, beneficialOwnerBeingModifiedID}: } as const; const defaultValues = { - street: String(reimbursementAccountDraft?.[inputKeys.street] ?? ''), - city: String(reimbursementAccountDraft?.[inputKeys.city] ?? ''), - state: String(reimbursementAccountDraft?.[inputKeys.state] ?? ''), - zipCode: String(reimbursementAccountDraft?.[inputKeys.zipCode] ?? ''), + street: SafeString(reimbursementAccountDraft?.[inputKeys.street]), + city: SafeString(reimbursementAccountDraft?.[inputKeys.city]), + state: SafeString(reimbursementAccountDraft?.[inputKeys.state]), + zipCode: SafeString(reimbursementAccountDraft?.[inputKeys.zipCode]), }; const stepFields = [inputKeys.street, inputKeys.city, inputKeys.state, inputKeys.zipCode]; diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO.tsx index 39ec6ae5afbb..b5ace8d14880 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/DateOfBirthUBO.tsx @@ -6,6 +6,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; 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; @@ -18,7 +19,7 @@ function DateOfBirthUBO({onNext, onMove, isEditing, beneficialOwnerBeingModified const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const dobInputID = `${BENEFICIAL_OWNER_PREFIX}_${beneficialOwnerBeingModifiedID}_${DOB}` as const; - const dobDefaultValue = String(reimbursementAccountDraft?.[dobInputID] ?? ''); + const dobDefaultValue = SafeString(reimbursementAccountDraft?.[dobInputID]); const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: [dobInputID], diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO.tsx index 4dc4506ea218..5ba498f86e99 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO.tsx @@ -7,6 +7,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; const {FIRST_NAME, LAST_NAME} = 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; @@ -22,8 +23,8 @@ function LegalNameUBO({onNext, onMove, isEditing, beneficialOwnerBeingModifiedID const lastNameInputID = `${BENEFICIAL_OWNER_PREFIX}_${beneficialOwnerBeingModifiedID}_${LAST_NAME}` as keyof FormOnyxValues; const stepFields = [firstNameInputID, lastNameInputID]; const defaultValues = { - firstName: String(reimbursementAccountDraft?.[firstNameInputID] ?? ''), - lastName: String(reimbursementAccountDraft?.[lastNameInputID] ?? ''), + firstName: SafeString(reimbursementAccountDraft?.[firstNameInputID]), + lastName: SafeString(reimbursementAccountDraft?.[lastNameInputID]), }; const handleSubmit = useReimbursementAccountStepFormSubmit({ diff --git a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO.tsx b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO.tsx index 0fe43c3a7e4f..21645251de4b 100644 --- a/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO.tsx +++ b/src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/SocialSecurityNumberUBO.tsx @@ -8,6 +8,7 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import {getFieldRequiredErrors, isValidSSNLastFour} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import SafeString from '@src/utils/SafeString'; 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; @@ -20,12 +21,12 @@ function SocialSecurityNumberUBO({onNext, onMove, isEditing, beneficialOwnerBein const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); const ssnLast4InputID = `${BENEFICIAL_OWNER_PREFIX}_${beneficialOwnerBeingModifiedID}_${SSN_LAST_4}` as const; - const defaultSsnLast4 = String(reimbursementAccountDraft?.[ssnLast4InputID] ?? ''); + const defaultSsnLast4 = SafeString(reimbursementAccountDraft?.[ssnLast4InputID]); const stepFields = [ssnLast4InputID]; const validate = (values: FormOnyxValues): FormInputErrors => { const errors = getFieldRequiredErrors(values, stepFields); - if (values[ssnLast4InputID] && !isValidSSNLastFour(String(values[ssnLast4InputID]))) { + if (values[ssnLast4InputID] && !isValidSSNLastFour(SafeString(values[ssnLast4InputID]))) { errors[ssnLast4InputID] = translate('bankAccount.error.ssnLast4'); } return errors; diff --git a/src/pages/ReimbursementAccount/USD/utils/getValuesForBeneficialOwner.ts b/src/pages/ReimbursementAccount/USD/utils/getValuesForBeneficialOwner.ts index bdd62ccff6e0..b4960ee575fe 100644 --- a/src/pages/ReimbursementAccount/USD/utils/getValuesForBeneficialOwner.ts +++ b/src/pages/ReimbursementAccount/USD/utils/getValuesForBeneficialOwner.ts @@ -1,6 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {ReimbursementAccountForm} from '@src/types/form'; +import SafeString from '@src/utils/SafeString'; type BeneficialOwnerValues = { firstName: string; @@ -41,14 +42,14 @@ function getValuesForBeneficialOwner(beneficialOwnerBeingModifiedID: string, rei } as const; return { - firstName: String(reimbursementAccountDraft[INPUT_KEYS.firstName] ?? ''), - lastName: String(reimbursementAccountDraft[INPUT_KEYS.lastName] ?? ''), - dob: String(reimbursementAccountDraft[INPUT_KEYS.dob] ?? ''), - ssnLast4: String(reimbursementAccountDraft[INPUT_KEYS.ssnLast4] ?? ''), - street: String(reimbursementAccountDraft[INPUT_KEYS.street] ?? ''), - city: String(reimbursementAccountDraft[INPUT_KEYS.city] ?? ''), - state: String(reimbursementAccountDraft[INPUT_KEYS.state] ?? ''), - zipCode: String(reimbursementAccountDraft[INPUT_KEYS.zipCode] ?? ''), + firstName: SafeString(reimbursementAccountDraft[INPUT_KEYS.firstName]), + lastName: SafeString(reimbursementAccountDraft[INPUT_KEYS.lastName]), + dob: SafeString(reimbursementAccountDraft[INPUT_KEYS.dob]), + ssnLast4: SafeString(reimbursementAccountDraft[INPUT_KEYS.ssnLast4]), + street: SafeString(reimbursementAccountDraft[INPUT_KEYS.street]), + city: SafeString(reimbursementAccountDraft[INPUT_KEYS.city]), + state: SafeString(reimbursementAccountDraft[INPUT_KEYS.state]), + zipCode: SafeString(reimbursementAccountDraft[INPUT_KEYS.zipCode]), }; } diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index c33aa29b4eab..884f576146a4 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -39,6 +39,7 @@ import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Attendee} from '@src/types/onyx/IOU'; +import SafeString from '@src/utils/SafeString'; type MoneyRequestAttendeesSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ @@ -252,7 +253,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde newSelectedOptions = lodashReject(attendees, isOptionSelected); } else { const iconSource = option.icons?.[0]?.source; - const icon = typeof iconSource === 'function' ? '' : (iconSource?.toString() ?? ''); + const icon = typeof iconSource === 'function' ? '' : SafeString(iconSource); newSelectedOptions = [ ...attendees, { diff --git a/src/pages/iou/request/step/IOURequestStepSubrate.tsx b/src/pages/iou/request/step/IOURequestStepSubrate.tsx index f9a46aa76f5c..dbddcb36cc43 100644 --- a/src/pages/iou/request/step/IOURequestStepSubrate.tsx +++ b/src/pages/iou/request/step/IOURequestStepSubrate.tsx @@ -30,6 +30,7 @@ import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {Subrate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import SafeString from '@src/utils/SafeString'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; @@ -113,7 +114,7 @@ function IOURequestStepSubrate({ const validate = (values: FormOnyxValues): Partial> => { const errors = {}; - const quantityVal = String(values[`quantity${pageIndex}`] ?? ''); + const quantityVal = SafeString(values[`quantity${pageIndex}`]); const subrateVal = values[`subrate${pageIndex}`] ?? ''; const quantityInt = parseInt(quantityVal, 10); if (subrateVal === '' || !validOptions.some(({value}) => value === subrateVal)) { @@ -131,8 +132,8 @@ function IOURequestStepSubrate({ }; const submit = (values: FormOnyxValues) => { - const quantityVal = String(values[`quantity${pageIndex}`] ?? ''); - const subrateVal = String(values[`subrate${pageIndex}`] ?? ''); + const quantityVal = SafeString(values[`quantity${pageIndex}`]); + const subrateVal = SafeString(values[`subrate${pageIndex}`]); const quantityInt = parseInt(quantityVal, 10); const selectedSubrate = allPossibleSubrates.find(({id}) => id === subrateVal); const name = selectedSubrate?.name ?? ''; diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx index fcfb5ede4be7..f0163ca0abe1 100644 --- a/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx +++ b/src/pages/media/AttachmentModalScreen/AttachmentModalContainer/index.native.tsx @@ -7,11 +7,12 @@ import AttachmentStateContextProvider from '@pages/media/AttachmentModalScreen/A import type {AttachmentModalOnCloseOptions} from '@pages/media/AttachmentModalScreen/AttachmentModalBaseContent/types'; import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import type {AttachmentModalScreenType} from '@pages/media/AttachmentModalScreen/types'; +import SafeString from '@src/utils/SafeString'; import type AttachmentModalContainerProps from './types'; function AttachmentModalContainer({contentProps, navigation, onShow, onClose, ExtraContent}: AttachmentModalContainerProps) { const attachmentsContext = useContext(AttachmentModalContext); - const testID = typeof contentProps.source === 'string' ? contentProps.source : (contentProps.source?.toString() ?? ''); + const testID = typeof contentProps.source === 'string' ? contentProps.source : SafeString(contentProps.source); const resetAttachmentModalAndClose = useCallback(() => { attachmentsContext.setCurrentAttachment(undefined); diff --git a/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx index 9e780a856ea8..25a84bdacca2 100644 --- a/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx @@ -19,6 +19,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import SafeString from '@src/utils/SafeString'; function ReportAttachmentModalContent({route, navigation}: AttachmentModalScreenProps) { const {attachmentID, type, source: sourceParam, isAuthTokenRequired, attachmentLink, originalFileName, accountID, reportID, hashKey, headerTitle, onShow, onClose} = route.params; @@ -74,7 +75,7 @@ function ReportAttachmentModalContent({route, navigation}: AttachmentModalScreen reportID, attachmentID: attachment.attachmentID, type, - source: String(attachment.source), + source: SafeString(attachment.source), accountID, isAuthTokenRequired: attachment?.isAuthTokenRequired, originalFileName: attachment?.file?.name, diff --git a/src/utils/SafeString.ts b/src/utils/SafeString.ts new file mode 100644 index 000000000000..27a0452fa3ef --- /dev/null +++ b/src/utils/SafeString.ts @@ -0,0 +1,55 @@ +/** + * SafeString is a utility function that converts a value to a string. + * It handles the problematic case of plain objects by converting them to JSON. + * It helps with eslint rule https://typescript-eslint.io/rules/no-base-to-string + * @param value - The value to convert to a string. + * @returns The string representation of the value. + */ +export default function SafeString(value: unknown): string { + if (value === undefined || value === null) { + return ''; + } + + // Handle primitives explicitly so the final fallback never receives an object. + const valueType = typeof value; + if (valueType === 'string') { + return value as string; + } + if (valueType === 'number' || valueType === 'boolean' || valueType === 'function' || valueType === 'bigint' || valueType === 'symbol') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + const primitive = value as number | boolean | Function | bigint | symbol; + return String(primitive); + } + + if (valueType === 'object') { + if (Array.isArray(value)) { + try { + return JSON.stringify(value); + } catch { + return '[object Array]'; + } + } + + const obj = value as {toString: () => string}; + const hasCustomToString = obj.toString && obj.toString !== Object.prototype.toString; + if (hasCustomToString) { + return obj.toString(); + } + + if (value instanceof Map) { + return '[object Map]'; + } + + if (value instanceof Set) { + return '[object Set]'; + } + + try { + return JSON.stringify(obj); + } catch { + return '[object Object]'; + } + } + // Unreachable fallback + return ''; +} diff --git a/tests/unit/SafeString.test.ts b/tests/unit/SafeString.test.ts new file mode 100644 index 000000000000..50e21215ebcc --- /dev/null +++ b/tests/unit/SafeString.test.ts @@ -0,0 +1,101 @@ +import SafeString from '../../src/utils/SafeString'; + +describe('SafeString', () => { + test('returns empty string for undefined and null', () => { + expect(SafeString(undefined)).toBe(''); + expect(SafeString(null)).toBe(''); + }); + + test('handles strings directly', () => { + expect(SafeString('hello')).toBe('hello'); + expect(SafeString('')).toBe(''); + }); + + test('handles numbers, booleans, functions, bigint, symbol', () => { + expect(SafeString(123)).toBe('123'); + expect(SafeString(0)).toBe('0'); + expect(SafeString(true)).toBe('true'); + expect(SafeString(false)).toBe('false'); + expect(SafeString(() => 1)).toBe(`function () { + return 1; + }`); + expect(SafeString(BigInt(10))).toBe('10'); + const sym = Symbol('x'); + expect(SafeString(sym)).toBe(String(sym)); + }); + + test('handles arrays via JSON, including nested', () => { + expect(SafeString([1, 'a', true])).toBe('[1,"a",true]'); + expect(SafeString([1, {a: 2}])).toBe('[1,{"a":2}]'); + }); + + test('arrays with circular refs fall back to [object Array]', () => { + const arr: unknown[] = [1]; + arr.push(arr); + expect(SafeString(arr)).toBe('[object Array]'); + }); + + test('Plain JavaScript objects stringify to JSON', () => { + expect(SafeString({a: 1, b: 'x'})).toBe('{"a":1,"b":"x"}'); + }); + + test('objects with custom toString use it', () => { + const obj = { + toString() { + return 'custom'; + }, + }; + expect(SafeString(obj)).toBe('custom'); + }); + + test('objects with circular refs fall back to [object Object]', () => { + const obj: {self?: unknown; a?: number} = {a: 1}; + obj.self = obj; + expect(SafeString(obj)).toBe('[object Object]'); + }); + + test('comparisons should fallback the same way as value?.toString() for undefined', () => { + const testValue: unknown = undefined; + const comparisonWithSafeString = SafeString(testValue) || 'fallback'; + const comparisonWithToString = testValue?.toString() ?? 'fallback'; + + expect(comparisonWithSafeString).toBe('fallback'); + expect(comparisonWithToString).toBe('fallback'); + expect(comparisonWithSafeString).toBe(comparisonWithToString); + }); + test('comparisons with nullish coalescing operator should fallback the same way for undefined', () => { + const testValue: unknown = undefined; + const comparisonWithSafeString = SafeString(testValue); + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const comparisonWithToString = String(testValue ?? ''); + + expect(comparisonWithSafeString).toBe(''); + expect(comparisonWithToString).toBe(''); + expect(comparisonWithSafeString).toBe(comparisonWithToString); + }); + + test('returns same results as String() for dates', () => { + const now = new Date(); + expect(SafeString(now)).toBe(String(now)); + }); + + test('returns same results as String() for errors', () => { + const error = new Error('test'); + expect(SafeString(error)).toBe(String(error)); + }); + + test('returns same results as String() for collection objects', () => { + expect(SafeString(new Map())).toBe('[object Map]'); + // eslint-disable-next-line @typescript-eslint/no-base-to-string + expect(SafeString(new Map())).toBe(String(new Map())); + + expect(SafeString(new Set())).toBe('[object Set]'); + // eslint-disable-next-line @typescript-eslint/no-base-to-string + expect(SafeString(new Set())).toBe(String(new Set())); + }); + + test('returns same results as String() for regexes', () => { + const regex = /test/; + expect(SafeString(regex)).toBe(String(regex)); + }); +});