From 241f5e71ac4533e1e497c8e715eb5c838711cc9c Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 20 Oct 2023 17:35:17 +0700 Subject: [PATCH 001/418] fix: missing translation for server errors --- src/components/Form.js | 5 +---- src/components/Form/FormWrapper.js | 5 +---- src/components/OptionsSelector/BaseOptionsSelector.js | 2 +- src/components/PDFView/PDFPasswordForm.js | 6 +++--- src/libs/ErrorUtils.ts | 6 ++++-- src/pages/ReimbursementAccount/AddressForm.js | 8 ++++---- src/pages/ReimbursementAccount/IdentityForm.js | 8 ++++---- .../Contacts/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- .../TwoFactorAuthForm/BaseTwoFactorAuthForm.js | 2 +- src/pages/settings/Wallet/ActivatePhysicalCardPage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 3 +-- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 6 +++--- 12 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/components/Form.js b/src/components/Form.js index b4e639dcf964..e4babf275af9 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -207,10 +207,7 @@ function Form(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to revalidate the form on update if the preferred locale changed on another device so that errors get translated }, [props.preferredLocale]); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(props.formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [props.formState]); + const errorMessage = useMemo(() => ErrorUtils.getLatestErrorMessage(props.formState), [props.formState]); /** * @param {String} inputID - The inputID of the input being touched diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index 3d9fd37d6f22..b2754cd9c0cf 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -81,10 +81,7 @@ function FormWrapper(props) { const {onSubmit, children, formState, errors, inputRefs, submitButtonText, footerContent, isSubmitButtonVisible, style, enabledWhenOffline, isSubmitActionDangerous, formID} = props; const formRef = useRef(null); const formContentRef = useRef(null); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [formState]); + const errorMessage = useMemo(() => ErrorUtils.getLatestErrorMessage(formState), [formState]); const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 4ffddd700359..7bf16fdef4f5 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -172,7 +172,7 @@ class BaseOptionsSelector extends Component { updateSearchValue(value) { this.setState({ - errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '', + errorMessage: value.length > this.props.maxLength ? ['common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}] : '', }); this.props.onChangeText(value); diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js index 58a4e64a28a5..e91eacbec71f 100644 --- a/src/components/PDFView/PDFPasswordForm.js +++ b/src/components/PDFView/PDFPasswordForm.js @@ -54,13 +54,13 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat const errorText = useMemo(() => { if (isPasswordInvalid) { - return translate('attachmentView.passwordIncorrect'); + return 'attachmentView.passwordIncorrect'; } if (!_.isEmpty(validationErrorText)) { - return translate(validationErrorText); + return validationErrorText; } return ''; - }, [isPasswordInvalid, translate, validationErrorText]); + }, [isPasswordInvalid, validationErrorText]); useEffect(() => { if (!isFocused) { diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index bf4fc0d810a4..ce14d2eda58d 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -46,7 +46,9 @@ type OnyxDataWithErrors = { errors?: Errors; }; -function getLatestErrorMessage(onyxData: TOnyxData): string { +type TranslationData = [string, Record]; + +function getLatestErrorMessage(onyxData: TOnyxData): TranslationData | string { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -55,7 +57,7 @@ function getLatestErrorMessage(onyxData: T const key = Object.keys(errors).sort().reverse()[0]; - return errors[key]; + return [errors[key], {isTranslated: true}]; } type OnyxDataWithErrorFields = { diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 5ddea09c6f4e..5089fc8167ce 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -103,7 +103,7 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} - errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} + errorText={props.errors.street ? 'bankAccount.error.addressStreet' : ''} hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} @@ -118,7 +118,7 @@ function AddressForm(props) { value={props.values.city} defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} - errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} + errorText={props.errors.city ? 'bankAccount.error.addressCity' : ''} containerStyles={[styles.mt4]} /> @@ -129,7 +129,7 @@ function AddressForm(props) { value={props.values.state} defaultValue={props.defaultValues.state || ''} onInputChange={(value) => props.onFieldChange({state: value})} - errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} + errorText={props.errors.state ? 'bankAccount.error.addressState' : ''} /> props.onFieldChange({zipCode: value})} - errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + errorText={props.errors.zipCode ? 'bankAccount.error.zipCode' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} containerStyles={[styles.mt2]} diff --git a/src/pages/ReimbursementAccount/IdentityForm.js b/src/pages/ReimbursementAccount/IdentityForm.js index 20c6e10ec64d..b86779b109f9 100644 --- a/src/pages/ReimbursementAccount/IdentityForm.js +++ b/src/pages/ReimbursementAccount/IdentityForm.js @@ -131,7 +131,7 @@ const defaultProps = { function IdentityForm(props) { // dob field has multiple validations/errors, we are handling it temporarily like this. - const dobErrorText = (props.errors.dob ? props.translate('bankAccount.error.dob') : '') || (props.errors.dobAge ? props.translate('bankAccount.error.age') : ''); + const dobErrorText = (props.errors.dob ? 'bankAccount.error.dob' : '') || (props.errors.dobAge ? 'bankAccount.error.age' : ''); const identityFormInputKeys = ['firstName', 'lastName', 'dob', 'ssnLast4']; const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); @@ -150,7 +150,7 @@ function IdentityForm(props) { value={props.values.firstName} defaultValue={props.defaultValues.firstName} onChangeText={(value) => props.onFieldChange({firstName: value})} - errorText={props.errors.firstName ? props.translate('bankAccount.error.firstName') : ''} + errorText={props.errors.firstName ? 'bankAccount.error.firstName' : ''} /> @@ -163,7 +163,7 @@ function IdentityForm(props) { value={props.values.lastName} defaultValue={props.defaultValues.lastName} onChangeText={(value) => props.onFieldChange({lastName: value})} - errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} + errorText={props.errors.lastName ? 'bankAccount.error.lastName' : ''} /> @@ -189,7 +189,7 @@ function IdentityForm(props) { keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} defaultValue={props.defaultValues.ssnLast4} onChangeText={(value) => props.onFieldChange({ssnLast4: value})} - errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} + errorText={props.errors.ssnLast4 ? 'bankAccount.error.ssnLast4' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} /> diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index 0175f2ceac1f..dc139c03000f 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -117,7 +117,7 @@ function ActivatePhysicalCardPage({ activateCardCodeInputRef.current.blur(); if (lastFourDigits.replace(CONST.MAGIC_CODE_EMPTY_CHAR, '').length !== LAST_FOUR_DIGITS_LENGTH) { - setFormError(translate('activateCardPage.error.thatDidntMatch')); + setFormError('activateCardPage.error.thatDidntMatch'); return; } diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 3576f92be31f..38e428451f2c 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -197,7 +197,6 @@ function LoginForm(props) { }, })); - const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]); const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]); const hasError = !_.isEmpty(serverErrorText); @@ -222,7 +221,7 @@ function LoginForm(props) { autoCapitalize="none" autoCorrect={false} keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} - errorText={formErrorText} + errorText={formError} hasError={hasError} maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index dc100fffe4f1..43b54454ba0f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -312,7 +312,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? props.translate(formError.recoveryCode) : ''} + errorText={formError.recoveryCode ? formError.recoveryCode : ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -328,7 +328,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''} + errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ''} hasError={hasError} autoFocus /> @@ -357,7 +357,7 @@ function BaseValidateCodeForm(props) { value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? props.translate(formError.validateCode) : ''} + errorText={formError.validateCode ? formError.validateCode : ''} hasError={hasError} autoFocus /> From 096ed12e2b91ae5bdc14f0db171d6c7aeefbd9a4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 21 Oct 2023 22:24:13 +0700 Subject: [PATCH 002/418] remove redundant dependency --- src/pages/settings/Wallet/ActivatePhysicalCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index dc139c03000f..71b147e3c28c 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -122,7 +122,7 @@ function ActivatePhysicalCardPage({ } CardSettings.activatePhysicalExpensifyCard(Number(lastFourDigits), cardID); - }, [lastFourDigits, cardID, translate]); + }, [lastFourDigits, cardID]); if (_.isEmpty(physicalCard)) { return ; From 0a9a467ac459e36a3e1ad9f059379ee70c4eb778 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 15:37:34 +0700 Subject: [PATCH 003/418] do not translate already translated text in DotIndicatorMessage --- src/components/AvatarWithImagePicker.js | 2 +- src/components/DistanceRequest/index.js | 4 +-- src/components/OfflineWithFeedback.js | 3 +- src/libs/ErrorUtils.ts | 36 +++++++++++++++---- src/pages/SearchPage.js | 4 ++- .../settings/Wallet/ExpensifyCardPage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 3 +- src/pages/signin/UnlinkLoginForm.js | 6 ++-- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 3dd23d9051eb..40ee7aa04208 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -365,7 +365,7 @@ class AvatarWithImagePicker extends React.Component { {this.state.validationError && ( )} diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index bd35678273ec..3d9cdb31195e 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -152,11 +152,11 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe // Initially, both waypoints will be null, and if we give fallback value as empty string that will result in true condition, that's why different default values. if (_.keys(waypoints).length === 2 && lodashGet(waypoints, 'waypoint0.address', 'address1') === lodashGet(waypoints, 'waypoint1.address', 'address2')) { - return {0: translate('iou.error.duplicateWaypointsErrorMessage')}; + return {0: 'iou.error.duplicateWaypointsErrorMessage'}; } if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.emptyWaypointsErrorMessage')}; + return {0: 'iou.error.emptyWaypointsErrorMessage'}; } }; diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 643e7b2f4a2f..a73a41f21810 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -7,6 +7,7 @@ import stylePropTypes from '../styles/stylePropTypes'; import styles from '../styles/styles'; import Tooltip from './Tooltip'; import Icon from './Icon'; +import * as ErrorUtils from '../libs/ErrorUtils'; import * as Expensicons from './Icon/Expensicons'; import * as StyleUtils from '../styles/StyleUtils'; import DotIndicatorMessage from './DotIndicatorMessage'; @@ -103,7 +104,7 @@ function OfflineWithFeedback(props) { const hasErrors = !_.isEmpty(props.errors); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = _.omit(props.errors, (e) => e === null); + const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(_.omit(props.errors, (e) => e === null)); const hasErrorMessages = !_.isEmpty(errorMessages); const isOfflinePendingAction = isOffline && props.pendingAction; const isUpdateOrDeleteError = hasErrors && (props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index ce14d2eda58d..891616669eb3 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,3 +1,5 @@ +import mapKeys from 'lodash/mapKeys'; +import isEmpty from 'lodash/isEmpty'; import CONST from '../CONST'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; @@ -46,9 +48,9 @@ type OnyxDataWithErrors = { errors?: Errors; }; -type TranslationData = [string, Record]; +type TranslationData = [string, Record] | string; -function getLatestErrorMessage(onyxData: TOnyxData): TranslationData | string { +function getLatestErrorMessage(onyxData: TOnyxData): TranslationData { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -64,7 +66,7 @@ type OnyxDataWithErrorFields = { errorFields?: ErrorFields; }; -function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -73,10 +75,10 @@ function getLatestErrorField(onyxData const key = Object.keys(errorsForField).sort().reverse()[0]; - return {[key]: errorsForField[key]}; + return {[key]: [errorsForField[key], {isTranslated: true}]}; } -function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -85,10 +87,30 @@ function getEarliestErrorField(onyxDa const key = Object.keys(errorsForField).sort()[0]; - return {[key]: errorsForField[key]}; + return {[key]: [errorsForField[key], {isTranslated: true}]}; } type ErrorsList = Record; +type ErrorsListWithTranslationData = Record; + +/** + * Method used to attach already translated message with isTranslated: true property + * @param errors - An object containing current errors in the form + * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} + */ +function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsList): ErrorsListWithTranslationData { + if (isEmpty(errors)) { + return {}; + } + + if (typeof errors === 'string' || Array.isArray(errors)) { + const [message, variables] = Array.isArray(errors) ? errors : [errors]; + // eslint-disable-next-line @typescript-eslint/naming-convention + return {0: [message, {...variables, isTranslated: true}]}; + } + + return mapKeys(errors, (message) => [message, {isTranslated: true}]); +} /** * Method used to generate error message for given inputID @@ -113,4 +135,4 @@ function addErrorMessage(errors: ErrorsList, inputID?: string, message?: string) } } -export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, addErrorMessage}; +export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, getErrorMessagesWithTranslationData, addErrorMessage}; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index c671e7b1a096..f0a4eb58916c 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -202,7 +202,9 @@ class SearchPage extends Component { shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ - this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : '' + this.props.network.isOffline + ? [`${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}`, {isTranslated: true}] + : '' } onLayout={this.searchRendered} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index e198d449d57d..d6096a3e3aac 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -90,7 +90,7 @@ function ExpensifyCardPage({ ) : null} diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 2c595a39c201..0196d5f91c02 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -239,11 +239,10 @@ function LoginForm(props) { {!_.isEmpty(props.account.success) && {props.account.success}} {!_.isEmpty(props.closeAccount.success || props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors, so we need to mock an object so that the messages looks similar to prop.account.errors )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 6807ba74c6f9..5b26d254bee5 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -7,6 +7,7 @@ import Str from 'expensify-common/lib/str'; import styles from '../../styles/styles'; import Button from '../../components/Button'; import Text from '../../components/Text'; +import * as ErrorUtils from '../../libs/ErrorUtils'; import * as Session from '../../libs/actions/Session'; import ONYXKEYS from '../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -63,18 +64,17 @@ function UnlinkLoginForm(props) { {props.translate('unlinkLoginForm.noLongerHaveAccess', {primaryLogin})} {!_.isEmpty(props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors so we need to mock an object so that the messages looks similar to prop.account.errors )} {!_.isEmpty(props.account.errors) && ( )} From b04abe52ac9f1f58ce85ea4398f51647c69b8699 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 16:00:13 +0700 Subject: [PATCH 004/418] fix missing translation for FormAlertWrapper --- src/components/FormAlertWithSubmitButton.js | 2 +- src/components/FormAlertWrapper.js | 2 +- src/pages/EnablePayments/OnfidoPrivacy.js | 6 ++++-- src/pages/settings/Wallet/ReportCardLostPage.js | 4 ++-- src/pages/settings/Wallet/TransferBalancePage.js | 3 ++- src/pages/tasks/NewTaskPage.js | 6 +++--- src/pages/workspace/WorkspaceInvitePage.js | 3 ++- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index 33d188719d11..f078b99ec47c 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -27,7 +27,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.string, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Callback fired when the "fix the errors" link is pressed */ onFixTheErrorsLinkPressed: PropTypes.func, diff --git a/src/components/FormAlertWrapper.js b/src/components/FormAlertWrapper.js index 67e031ce6ab6..757bc1cca2fb 100644 --- a/src/components/FormAlertWrapper.js +++ b/src/components/FormAlertWrapper.js @@ -27,7 +27,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.string, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Props to detect online status */ network: networkPropTypes.isRequired, diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.js index 85ceb03b01d5..5575525890f2 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.js @@ -44,9 +44,11 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; + const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); - onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + if (_.isArray(onfidoError)) { + onfidoError[0] += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + } return ( diff --git a/src/pages/settings/Wallet/ReportCardLostPage.js b/src/pages/settings/Wallet/ReportCardLostPage.js index 29a588916326..696a162ac6e5 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.js +++ b/src/pages/settings/Wallet/ReportCardLostPage.js @@ -182,7 +182,7 @@ function ReportCardLostPage({ @@ -200,7 +200,7 @@ function ReportCardLostPage({ diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index ae54dab569f7..34c97f8e5277 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -3,6 +3,7 @@ import React, {useEffect} from 'react'; import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import * as ErrorUtils from '../../../libs/ErrorUtils'; import ONYXKEYS from '../../../ONYXKEYS'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import ScreenWrapper from '../../../components/ScreenWrapper'; @@ -165,7 +166,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? _.chain(props.walletTransfer.errors).values().first().value() : ''; + const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorMessagesWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js index f0d2d506c9d8..2e1f42d90b6e 100644 --- a/src/pages/tasks/NewTaskPage.js +++ b/src/pages/tasks/NewTaskPage.js @@ -114,17 +114,17 @@ function NewTaskPage(props) { // the response function onSubmit() { if (!props.task.title && !props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.confirmError')); + setErrorMessage('newTaskPage.confirmError'); return; } if (!props.task.title) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskName')); + setErrorMessage('newTaskPage.pleaseEnterTaskName'); return; } if (!props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskDestination')); + setErrorMessage('newTaskPage.pleaseEnterTaskDestination'); return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index a21173dd7d98..33fd3786c490 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -4,6 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import lodashGet from 'lodash/get'; +import * as ErrorUtils from '../../libs/ErrorUtils'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import Navigation from '../../libs/Navigation/Navigation'; @@ -281,7 +282,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={props.policy.alertMessage} + message={ErrorUtils.getErrorMessagesWithTranslationData(props.policy.alertMessage)} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter From 95188d6cb14118265db6a76cb6cbe52eadab039e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 16:44:38 +0700 Subject: [PATCH 005/418] fix missing translation for FormHelpMessage --- src/components/MoneyRequestConfirmationList.js | 2 +- src/pages/NewChatPage.js | 2 +- src/pages/ReimbursementAccount/AddressForm.js | 4 ++-- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- src/pages/iou/steps/MoneyRequestAmountForm.js | 2 +- src/pages/settings/Profile/PersonalDetails/AddressPage.js | 2 +- src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js | 2 +- src/pages/settings/Wallet/AddDebitCardPage.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0b266351a60c..c5f04d52f5f3 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -538,7 +538,7 @@ function MoneyRequestConfirmationList(props) { )} {button} diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 381564b82600..e45635f82f1d 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -251,7 +251,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i shouldShowOptions={isOptionsDataReady} shouldShowConfirmButton confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} - textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} onConfirmSelection={createGroup} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 5089fc8167ce..4eb5009256b1 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -104,7 +104,7 @@ function AddressForm(props) { defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} errorText={props.errors.street ? 'bankAccount.error.addressStreet' : ''} - hint={props.translate('common.noPO')} + hint="common.noPO" renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} /> @@ -144,7 +144,7 @@ function AddressForm(props) { onChangeText={(value) => props.onFieldChange({zipCode: value})} errorText={props.errors.zipCode ? 'bankAccount.error.zipCode' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt2]} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 0ca9b1b7ea92..926eb3f651ac 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -207,7 +207,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul containerStyles={[styles.mt4]} defaultValue={getDefaultStateForField('website', defaultWebsite)} shouldSaveDraft - hint={translate('common.websiteExample')} + hint="common.websiteExample" keyboardType={CONST.KEYBOARD_TYPE.URL} /> )} )} diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index e75c3b2c517e..1f62e6f68ac9 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -178,7 +178,7 @@ function DebitCardPage(props) { accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt4]} /> From e2d2551625ca6798da2de0ea59b8a33f911c90d3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 17:03:38 +0700 Subject: [PATCH 006/418] fix lint --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index c5f04d52f5f3..91cba3f2d9bb 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -544,7 +544,7 @@ function MoneyRequestConfirmationList(props) { {button} ); - }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, translate, formError]); + }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, formError]); const {image: receiptImage, thumbnail: receiptThumbnail} = props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; From 6803203c19db5f5abb2368c954dcfbc936763f18 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 21 Nov 2023 20:59:36 +0700 Subject: [PATCH 007/418] fix translation for AddressForm --- src/components/AddressForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 19ab35f036c1..4684e11dc0bb 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -65,7 +65,7 @@ const defaultProps = { function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) { const {translate} = useLocalize(); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); - const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); + const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}]; const isUSAForm = country === CONST.COUNTRY.US; /** From b8dad84ebd7d172300b1b8460a1c0d0a71ef0360 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 1 Dec 2023 19:43:35 +0700 Subject: [PATCH 008/418] create prop type for translatable text --- src/components/AddressSearch/index.js | 3 ++- src/components/FormAlertWithSubmitButton.js | 3 ++- src/components/FormAlertWrapper.js | 3 ++- src/components/FormHelpMessage.js | 3 ++- src/components/RoomNameInput/roomNameInputPropTypes.js | 3 ++- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 3 ++- src/components/translatableTextPropTypes.js | 8 ++++++++ 7 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 src/components/translatableTextPropTypes.js diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 9f16766a22ae..4143b6e8f699 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -9,6 +9,7 @@ import LocationErrorMessage from '@components/LocationErrorMessage'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import TextInput from '@components/TextInput'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import * as ApiUtils from '@libs/ApiUtils'; import compose from '@libs/compose'; @@ -38,7 +39,7 @@ const propTypes = { onBlur: PropTypes.func, /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** Hint text to display */ hint: PropTypes.string, diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index cab71c6a935c..6a7d770d8779 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import useThemeStyles from '@styles/useThemeStyles'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Text for the button */ @@ -27,7 +28,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Callback fired when the "fix the errors" link is pressed */ onFixTheErrorsLinkPressed: PropTypes.func, diff --git a/src/components/FormAlertWrapper.js b/src/components/FormAlertWrapper.js index d3f49728bfec..f26161efa847 100644 --- a/src/components/FormAlertWrapper.js +++ b/src/components/FormAlertWrapper.js @@ -10,6 +10,7 @@ import {withNetwork} from './OnyxProvider'; import RenderHTML from './RenderHTML'; import Text from './Text'; import TextLink from './TextLink'; +import translatableTextPropTypes from './translatableTextPropTypes'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; const propTypes = { @@ -27,7 +28,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Props to detect online status */ network: networkPropTypes.isRequired, diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js index bec02c3d51f0..6644bbc0ccee 100644 --- a/src/components/FormHelpMessage.js +++ b/src/components/FormHelpMessage.js @@ -9,10 +9,11 @@ import useThemeStyles from '@styles/useThemeStyles'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Error or hint text. Ignored when children is not empty */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Children to render next to dot indicator */ children: PropTypes.node, diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index f457e4e2a494..339c15d0c1e1 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -12,7 +13,7 @@ const propTypes = { disabled: PropTypes.bool, /** Error text to show */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** A ref forwarded to the TextInput */ forwardedRef: refPropTypes, diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 5387d1ff81d1..48e5556738bd 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; const propTypes = { /** Input label */ @@ -17,7 +18,7 @@ const propTypes = { placeholder: PropTypes.string, /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** Icon to display in right side of text input */ icon: PropTypes.func, diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js new file mode 100644 index 000000000000..8da65b0ba202 --- /dev/null +++ b/src/components/translatableTextPropTypes.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; + +/** + * Traslatable text with phrase key and/or variables + * + * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] + */ +export default PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); From bcd1c1950341f43171243e51c7a88d0177a9ac84 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 1 Dec 2023 19:44:59 +0700 Subject: [PATCH 009/418] use translatable text type for hint --- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 48e5556738bd..14f1db5ea045 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -65,7 +65,7 @@ const propTypes = { maxLength: PropTypes.number, /** Hint text to display below the TextInput */ - hint: PropTypes.string, + hint: translatableTextPropTypes, /** Prefix character */ prefixCharacter: PropTypes.string, From 5276da1d4b530f4a584a86cca6c7fda4faab1b0e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:23:25 +0700 Subject: [PATCH 010/418] use MaybePhraseKey type --- src/components/CheckboxWithLabel.tsx | 3 ++- src/components/CountrySelector.js | 3 ++- src/components/FormAlertWithSubmitButton.tsx | 3 ++- src/components/FormAlertWrapper.tsx | 3 ++- src/components/MagicCodeInput.js | 3 ++- src/components/MenuItem.tsx | 3 ++- ...TemporaryForRefactorRequestConfirmationList.js | 4 ++-- src/components/Picker/types.ts | 3 ++- src/components/RadioButtonWithLabel.tsx | 3 ++- src/components/StatePicker/index.js | 3 ++- src/components/TimePicker/TimePicker.js | 2 +- src/components/ValuePicker/index.js | 3 ++- src/components/translatableTextPropTypes.js | 1 + src/libs/ErrorUtils.ts | 15 ++++++--------- 14 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 9660c9e1a2e5..e3a6b5fc44c7 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -1,6 +1,7 @@ import React, {ComponentType, ForwardedRef, useState} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; @@ -38,7 +39,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { style?: StyleProp; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Value for checkbox. This prop is intended to be set by Form.js only */ value?: boolean; diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 68a6486bce48..01d297d35467 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -8,10 +8,11 @@ import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import refPropTypes from './refPropTypes'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Callback called when the country changes. */ onInputChange: PropTypes.func.isRequired, diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index d8e30b27371d..d9412bf79857 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -1,12 +1,13 @@ import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; type FormAlertWithSubmitButtonProps = { /** Error message to display above button */ - message?: string; + message?: MaybePhraseKey; /** Whether the button is disabled */ isDisabled?: boolean; diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index a144bf069502..9d366fd72cb0 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -2,6 +2,7 @@ import React, {ReactNode} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Network from '@src/types/onyx/Network'; import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; @@ -26,7 +27,7 @@ type FormAlertWrapperProps = { isMessageHtml?: boolean; /** Error message to display above button */ - message?: string; + message?: MaybePhraseKey; /** Props to detect online status */ network: Network; diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 55a65237a691..b075edc9aeca 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -14,6 +14,7 @@ import networkPropTypes from './networkPropTypes'; import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; +import translatableTextPropTypes from './translatableTextPropTypes'; const TEXT_INPUT_EMPTY_STATE = ''; @@ -34,7 +35,7 @@ const propTypes = { shouldDelayFocus: PropTypes.bool, /** Error text to display */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code', 'off']).isRequired, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index db150d55f0d2..a713e11e5871 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -12,6 +12,7 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import type {MaybePhraseKey} from '@libs/Localize'; import {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -142,7 +143,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & error?: string; /** Error to display at the bottom of the component */ - errorText?: string; + errorText?: MaybePhraseKey; /** A boolean flag that gives the icon a green fill if true */ success?: boolean; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 20012bc90ef0..32b9100c0803 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -545,13 +545,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {button} ); - }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, translate, formError, styles.ph1, styles.mb2]); + }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 58eed0371893..3fada48005f5 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,5 +1,6 @@ import {ChangeEvent, Component, ReactElement} from 'react'; import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; +import type {MaybePhraseKey} from '@libs/Localize'; type MeasureLayoutOnFailCallback = () => void; @@ -58,7 +59,7 @@ type BasePickerProps = { placeholder?: PickerPlaceholder; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Customize the BasePicker container */ containerStyles?: StyleProp; diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index 4c223262ac50..5327b9dbb2d4 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -1,6 +1,7 @@ import React, {ComponentType} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import FormHelpMessage from './FormHelpMessage'; import * as Pressables from './Pressable'; import RadioButton from './RadioButton'; @@ -26,7 +27,7 @@ type RadioButtonWithLabelProps = { hasError?: boolean; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; }; const PressableWithFeedback = Pressables.PressableWithFeedback; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index 6fa60fbba947..e937fb2f76fd 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -6,13 +6,14 @@ import _ from 'underscore'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import StateSelectorModal from './StateSelectorModal'; const propTypes = { /** Error text to display */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** State to display */ value: PropTypes.string, diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 5b49739150cc..f0633415c78b 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -446,7 +446,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { {isError ? ( ) : ( diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index b5ddaa7dcb73..38e1689f3b10 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -5,6 +5,7 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; @@ -12,7 +13,7 @@ import ValueSelectorModal from './ValueSelectorModal'; const propTypes = { /** Form Error description */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Item to display */ value: PropTypes.string, diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js index 8da65b0ba202..10130ab2da3e 100644 --- a/src/components/translatableTextPropTypes.js +++ b/src/components/translatableTextPropTypes.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; /** * Traslatable text with phrase key and/or variables + * Use Localize.MaybePhraseKey instead for Typescript * * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] */ diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 4c369d6a8b4f..e3dd952c7ad0 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -56,9 +56,7 @@ type OnyxDataWithErrors = { errors?: Errors; }; -type TranslationData = [string, Record] | string; - -function getLatestErrorMessage(onyxData: TOnyxData): TranslationData { +function getLatestErrorMessage(onyxData: TOnyxData): Localize.MaybePhraseKey { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -74,7 +72,7 @@ type OnyxDataWithErrorFields = { errorFields?: ErrorFields; }; -function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -86,7 +84,7 @@ function getLatestErrorField(onyxData return {[key]: [errorsForField[key], {isTranslated: true}]}; } -function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -98,15 +96,14 @@ function getEarliestErrorField(onyxDa return {[key]: [errorsForField[key], {isTranslated: true}]}; } -type ErrorsList = Record; -type ErrorsListWithTranslationData = Record; +type ErrorsList = Record; /** * Method used to attach already translated message with isTranslated: true property * @param errors - An object containing current errors in the form * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} */ -function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsList): ErrorsListWithTranslationData { +function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { if (isEmpty(errors)) { return {}; } @@ -114,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsLis if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {0: [message, {...variables, isTranslated: true}]}; + return {0: [message as string, {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 93de9802ff67a2c8ebe1b493efac97988b5a4190 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:30:58 +0700 Subject: [PATCH 011/418] fix type --- src/libs/ErrorUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index e3dd952c7ad0..0f97fa8f39cc 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -96,7 +96,7 @@ function getEarliestErrorField(onyxDa return {[key]: [errorsForField[key], {isTranslated: true}]}; } -type ErrorsList = Record; +type ErrorsList = Record; /** * Method used to attach already translated message with isTranslated: true property @@ -111,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | E if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {0: [message as string, {...variables, isTranslated: true}]}; + return {'0': [message as string, {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 916e3decb5fb25aa5757645226aa98e1fc8bf640 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:42:35 +0700 Subject: [PATCH 012/418] fix type --- src/components/FormAlertWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index 9d366fd72cb0..ef7e57758e3e 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -67,7 +67,7 @@ function FormAlertWrapper({ {` ${translate('common.inTheFormBeforeContinuing')}.`} ); - } else if (isMessageHtml) { + } else if (isMessageHtml && typeof message === 'string') { content = ${message}`} />; } From a6a834015904b14dbe2e50bad458584e6f0c499a Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:48:44 +0700 Subject: [PATCH 013/418] remove redundant logic --- .../Contacts/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- .../TwoFactorAuthForm/BaseTwoFactorAuthForm.js | 2 +- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index f7008715f406..8b19c7bdd233 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -188,7 +188,7 @@ function BaseValidateCodeForm(props) { name="validateCode" value={validateCode} onChangeText={onTextInput} - errorText={formError.validateCode ? formError.validateCode : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.validateCode || ErrorUtils.getLatestErrorMessage(props.account)} hasError={!_.isEmpty(validateLoginError)} onFulfill={validateAndSubmitForm} autoFocus={false} diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js index 77898916c353..f65f7368de76 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js @@ -93,7 +93,7 @@ function BaseTwoFactorAuthForm(props) { value={twoFactorAuthCode} onChangeText={onTextInput} onFulfill={validateAndSubmitForm} - errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.twoFactorAuthCode || ErrorUtils.getLatestErrorMessage(props.account)} ref={inputRef} autoFocus={false} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index eaff916004be..98dc6bc68f99 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -310,7 +310,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? formError.recoveryCode : ''} + errorText={formError.recoveryCode || ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -326,7 +326,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ''} + errorText={formError.twoFactorAuthCode || ''} hasError={hasError} autoFocus key="twoFactorAuthCode" @@ -356,7 +356,7 @@ function BaseValidateCodeForm(props) { value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? formError.validateCode : ''} + errorText={formError.validateCode || ''} hasError={hasError} autoFocus key="validateCode" From 809a5f3655ca897b01f169c011c32d45745974e1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:05:30 +0700 Subject: [PATCH 014/418] fix missing translation in OfflineWithFeedback --- src/components/OfflineWithFeedback.tsx | 3 ++- src/pages/workspace/WorkspaceMembersPage.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..4f86218eab20 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -3,6 +3,7 @@ import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -82,7 +83,7 @@ function OfflineWithFeedback({ const hasErrors = isNotEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = omitBy(errors, (e) => e === null); + const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(omitBy(errors, (e) => e === null)); const hasErrorMessages = isNotEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 9834d4e9e1c0..3e2c1a5ca93f 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -407,7 +407,7 @@ function WorkspaceMembersPage(props) { return ( Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> From 7647500a94655937c3c7c135ee1aead66c143a13 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:07:46 +0700 Subject: [PATCH 015/418] fix lint --- src/libs/ErrorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 0f97fa8f39cc..e1a585139c5f 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -111,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | E if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message as string, {...variables, isTranslated: true}]}; + return {'0': [message ?? '', {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 5167629906c1d3f411105a9287fca8b70f7798f3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:13:58 +0700 Subject: [PATCH 016/418] fix type --- src/libs/ErrorUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index e1a585139c5f..b694234ce69a 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,4 +1,3 @@ -import isEmpty from 'lodash/isEmpty'; import mapKeys from 'lodash/mapKeys'; import CONST from '@src/CONST'; import {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; @@ -104,14 +103,14 @@ type ErrorsList = Record; * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} */ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { - if (isEmpty(errors)) { + if (!errors || (Array.isArray(errors) && errors.length === 0)) { return {}; } if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message ?? '', {...variables, isTranslated: true}]}; + return {'0': [message, {...(variables ?? []), isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 72e81c06a65ed11aac7e2afa08fad478300873b9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:23:56 +0700 Subject: [PATCH 017/418] fix missing translations for DotIndicatorMessage --- src/libs/actions/Card.js | 5 ++--- src/pages/iou/request/step/IOURequestStepDistance.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index 68642bd8fdf1..d0b589f00fea 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -164,12 +163,12 @@ function revealVirtualCardDetails(cardID) { API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + reject('cardPage.cardDetailsLoadingFailure'); return; } resolve(response); }) - .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); + .catch(() => reject('cardPage.cardDetailsLoadingFailure')); }); } diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 66cbd7f135a9..5d7acb66374e 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -132,7 +132,7 @@ function IOURequestStepDistance({ } if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; + return {0: 'iou.error.atLeastTwoDifferentWaypoints'}; } }; From 94fc3e294f0b8b3281ee6ccb24d2b2262ced3517 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 28 Dec 2023 14:27:55 +0700 Subject: [PATCH 018/418] fix lint --- src/libs/actions/Card.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index d0b589f00fea..1fb0166ccf17 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -163,11 +163,13 @@ function revealVirtualCardDetails(cardID) { API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + // eslint-disable-next-line prefer-promise-reject-errors reject('cardPage.cardDetailsLoadingFailure'); return; } resolve(response); }) + // eslint-disable-next-line prefer-promise-reject-errors .catch(() => reject('cardPage.cardDetailsLoadingFailure')); }); } From dea7484a711629fd7d27c0b474c661e1d79ace15 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 2 Jan 2024 17:13:13 +0700 Subject: [PATCH 019/418] handle client error --- src/components/OfflineWithFeedback.tsx | 2 +- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/libs/ErrorUtils.ts | 34 +++++++++---------- src/libs/Localize/index.ts | 2 +- src/libs/actions/Report.ts | 2 +- src/libs/actions/Session/index.ts | 2 +- .../settings/Wallet/TransferBalancePage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 2 +- src/pages/signin/UnlinkLoginForm.js | 4 +-- src/pages/workspace/WorkspaceInvitePage.js | 2 +- src/types/onyx/OnyxCommon.ts | 3 +- 12 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 0d54431da458..902e20063687 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -83,7 +83,7 @@ function OfflineWithFeedback({ const hasErrors = isNotEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(omitBy(errors, (e) => e === null)); + const errorMessages = ErrorUtils.getErrorsWithTranslationData(omitBy(errors, (e) => e === null)); const hasErrorMessages = isNotEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/languages/en.ts b/src/languages/en.ts index c1decfdf1c70..d86cc1b4d421 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -842,6 +842,9 @@ export default { sharedNoteMessage: 'Keep notes about this chat here. Expensify employees and other users on the team.expensify.com domain can view these notes.', composerLabel: 'Notes', myNote: 'My note', + error: { + genericFailureMessage: "Private notes couldn't be saved", + }, }, addDebitCardPage: { addADebitCard: 'Add a debit card', diff --git a/src/languages/es.ts b/src/languages/es.ts index 42461e766b29..b86a1093bd3a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -838,6 +838,9 @@ export default { sharedNoteMessage: 'Guarda notas sobre este chat aquí. Los empleados de Expensify y otros usuarios del dominio team.expensify.com pueden ver estas notas.', composerLabel: 'Notas', myNote: 'Mi nota', + error: { + genericFailureMessage: 'Notas privadas no han podido ser guardados', + }, }, addDebitCardPage: { addADebitCard: 'Añadir una tarjeta de débito', diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index b694234ce69a..2828c492b123 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,4 +1,4 @@ -import mapKeys from 'lodash/mapKeys'; +import mapValues from 'lodash/mapValues'; import CONST from '@src/CONST'; import {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; @@ -39,8 +39,8 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO * Method used to get an error object with microsecond as the key. * @param error - error key or message to be saved */ -function getMicroSecondOnyxError(error: string): Record { - return {[DateUtils.getMicroseconds()]: error}; +function getMicroSecondOnyxError(error: string, isTranslated = false): Record { + return {[DateUtils.getMicroseconds()]: error && [error, {isTranslated}]}; } /** @@ -51,6 +51,11 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: T } const key = Object.keys(errors).sort().reverse()[0]; - - return [errors[key], {isTranslated: true}]; + return getErrorWithTranslationData(errors[key]); } type OnyxDataWithErrorFields = { @@ -79,8 +83,7 @@ function getLatestErrorField(onyxData } const key = Object.keys(errorsForField).sort().reverse()[0]; - - return {[key]: [errorsForField[key], {isTranslated: true}]}; + return {[key]: getErrorWithTranslationData(errorsForField[key])}; } function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { @@ -91,29 +94,26 @@ function getEarliestErrorField(onyxDa } const key = Object.keys(errorsForField).sort()[0]; - - return {[key]: [errorsForField[key], {isTranslated: true}]}; + return {[key]: getErrorWithTranslationData(errorsForField[key])}; } type ErrorsList = Record; /** - * Method used to attach already translated message with isTranslated: true property + * Method used to attach already translated message with isTranslated property * @param errors - An object containing current errors in the form - * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} + * @returns Errors in the form of {timestamp: [message, {isTranslated}]} */ -function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { +function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { if (!errors || (Array.isArray(errors) && errors.length === 0)) { return {}; } if (typeof errors === 'string' || Array.isArray(errors)) { - const [message, variables] = Array.isArray(errors) ? errors : [errors]; - // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message, {...(variables ?? []), isTranslated: true}]}; + return {'0': getErrorWithTranslationData(errors)}; } - return mapKeys(errors, (message) => [message, {isTranslated: true}]); + return mapValues(errors, getErrorWithTranslationData); } /** @@ -146,6 +146,6 @@ export { getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, - getErrorMessagesWithTranslationData, + getErrorsWithTranslationData, addErrorMessage, }; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 77c34ebdc576..82ba8dc418d3 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -97,7 +97,7 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } -type MaybePhraseKey = string | [string, Record & {isTranslated?: true}] | []; +type MaybePhraseKey = string | [string, Record & {isTranslated?: boolean}] | []; /** * Return translated string for given error. diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 06c0316a40b5..ec917a5eac99 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2379,7 +2379,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = value: { privateNotes: { [accountID]: { - errors: ErrorUtils.getMicroSecondOnyxError("Private notes couldn't be saved"), + errors: ErrorUtils.getMicroSecondOnyxError('privateNotes.error.genericFailureMessage'), }, }, }, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index ca38e0dd5902..32adbcf59cfa 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -647,7 +647,7 @@ function clearAccountMessages() { } function setAccountError(error: string) { - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error, true)}); } // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index 499f50616218..86bf5f9c7a8d 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -167,7 +167,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorMessagesWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; + const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorsWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 8c2acbb17a68..037b52957574 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -288,7 +288,7 @@ function LoginForm(props) { )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 962e17786ce7..1d278760f13c 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -68,14 +68,14 @@ function UnlinkLoginForm(props) { )} {!_.isEmpty(props.account.errors) && ( )} diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index cb2b9ff52670..87b1802511af 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -278,7 +278,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={ErrorUtils.getErrorMessagesWithTranslationData(props.policy.alertMessage)} + message={ErrorUtils.getErrorsWithTranslationData(props.policy.alertMessage)} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 956e9ff36b24..93f5e9df2350 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -1,4 +1,5 @@ import {ValueOf} from 'type-fest'; +import * as Localize from '@libs/Localize'; import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; @@ -8,7 +9,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From a92b6901b0de2c4f45e05f50593d7757480ddc01 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 2 Jan 2024 17:22:05 +0700 Subject: [PATCH 020/418] fix type --- src/libs/ErrorUtils.ts | 1 + src/libs/ReportUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 2828c492b123..bba042d02ef3 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -110,6 +110,7 @@ function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsLi } if (typeof errors === 'string' || Array.isArray(errors)) { + // eslint-disable-next-line @typescript-eslint/naming-convention return {'0': getErrorWithTranslationData(errors)}; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6a4914f44121..7e1e5c1c0f9c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -164,7 +164,7 @@ type ReportRouteParams = { type ReportOfflinePendingActionAndErrors = { addWorkspaceRoomOrChatPendingAction: PendingAction | undefined; - addWorkspaceRoomOrChatErrors: Record | null | undefined; + addWorkspaceRoomOrChatErrors: Errors | null | undefined; }; type OptimisticApprovedReportAction = Pick< @@ -3864,7 +3864,7 @@ function isValidReportIDFromPath(reportIDFromPath: string): boolean { /** * Return the errors we have when creating a chat or a workspace room */ -function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Record | null | undefined { +function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Errors | null | undefined { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to have errors for the same report at the same time, so // simply looking up the first truthy value will get the relevant property if it's set. return report?.errorFields?.addWorkspaceRoom ?? report?.errorFields?.createChat; From a8061efe5d6065b68aa9c1c661a4ba50800271e4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 00:51:10 +0700 Subject: [PATCH 021/418] fix test --- tests/actions/IOUTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 4d9ce42a08ce..f08bfdd73ce9 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe('iou.error.genericCreateFailureMessage'); + expect(_.values(transaction.errors)[0]).toBe(["iou.error.genericCreateFailureMessage", {isTranslated: false}]); resolve(); }, }); @@ -1631,7 +1631,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); const updatedAction = _.find(allActions, (reportAction) => !_.isEmpty(reportAction)); expect(updatedAction.actionName).toEqual('MODIFIEDEXPENSE'); - expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining(['iou.error.genericEditFailureMessage'])); + expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([["iou.error.genericEditFailureMessage", {isTranslated: false}]])); resolve(); }, }); @@ -1846,7 +1846,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connectionID); const erroredAction = _.find(_.values(allActions), (action) => !_.isEmpty(action.errors)); - expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining(['iou.error.other'])); + expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([["iou.error.other", {isTranslated: false}]])); resolve(); }, }); From bb7c9cab65081a15e4215da4d92e617c323eb4c1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 00:58:37 +0700 Subject: [PATCH 022/418] fix lint --- tests/actions/IOUTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index f08bfdd73ce9..7c7bf520675a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe(["iou.error.genericCreateFailureMessage", {isTranslated: false}]); + expect(_.values(transaction.errors)[0]).toBe(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); resolve(); }, }); @@ -1631,7 +1631,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); const updatedAction = _.find(allActions, (reportAction) => !_.isEmpty(reportAction)); expect(updatedAction.actionName).toEqual('MODIFIEDEXPENSE'); - expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([["iou.error.genericEditFailureMessage", {isTranslated: false}]])); + expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([['iou.error.genericEditFailureMessage', {isTranslated: false}]])); resolve(); }, }); @@ -1846,7 +1846,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connectionID); const erroredAction = _.find(_.values(allActions), (action) => !_.isEmpty(action.errors)); - expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([["iou.error.other", {isTranslated: false}]])); + expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([['iou.error.other', {isTranslated: false}]])); resolve(); }, }); From 49a45b2cf26dc05203a066342c0ae6ccde5fa742 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 01:06:08 +0700 Subject: [PATCH 023/418] fix test --- tests/actions/IOUTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 7c7bf520675a..1fecb79c6908 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); + expect(_.values(transaction.errors)[0]).toStrictEqual(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); resolve(); }, }); From 68bc74de6ce6964e01ccb4512f064c60adcb9f92 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jan 2024 00:21:27 +0700 Subject: [PATCH 024/418] update spanish translation message --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index d9785f2d4a55..83d904d4a98e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -843,7 +843,7 @@ export default { composerLabel: 'Notas', myNote: 'Mi nota', error: { - genericFailureMessage: 'Notas privadas no han podido ser guardados', + genericFailureMessage: 'Las notas privadas no han podido ser guardadas', }, }, addDebitCardPage: { From 20cdd2ef71c92969cc1205cec93ec7995a7e7084 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 8 Jan 2024 18:20:48 +0000 Subject: [PATCH 025/418] chore(typescript): migrate moneyrequestheader to typescript --- ...equestHeader.js => MoneyRequestHeader.tsx} | 128 ++++++++---------- src/libs/HeaderUtils.ts | 2 +- 2 files changed, 58 insertions(+), 72 deletions(-) rename src/components/{MoneyRequestHeader.js => MoneyRequestHeader.tsx} (65%) diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.tsx similarity index 65% rename from src/components/MoneyRequestHeader.js rename to src/components/MoneyRequestHeader.tsx index 488630dd0590..73b4148279ba 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.tsx @@ -1,89 +1,78 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import participantPropTypes from './participantPropTypes'; -import transactionPropTypes from './transactionPropTypes'; -const propTypes = { - /** The report currently being looked at */ - report: iouReportPropTypes.isRequired, - - /** The policy which the report is tied to */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - - /* Onyx Props */ +type MoneyRequestHeaderOnyxProps = { /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user email */ - email: PropTypes.string, - }), + session: OnyxEntry; /** The expense report or iou report (only will have a value if this is a transaction thread) */ - parentReport: iouReportPropTypes, - - /** The report action the transaction is tied to from the parent report */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReport: OnyxEntry; /** All the data for the transaction */ - transaction: transactionPropTypes, + transaction: OnyxEntry; + + /** All report actions */ + // eslint-disable-next-line react/no-unused-prop-types + parentReportActions: OnyxEntry; }; -const defaultProps = { - session: { - email: null, - }, - parentReport: {}, - parentReportAction: {}, - transaction: {}, - policy: {}, +type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { + /** The report currently being looked at */ + report: Report; + + /** The policy which the report is tied to */ + policy: Policy; + + /** The report action the transaction is tied to from the parent report */ + parentReportAction: ReportAction & OriginalMessageIOU; + + /** Personal details so we can get the ones for the report participants */ + personalDetails: OnyxCollection; }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}: MoneyRequestHeaderProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const moneyRequestReport = parentReport; - const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); + const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); // Only the requestor can take delete the request, admins can only edit it. - const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); + const isActionOwner = parentReportAction.actorAccountID === (session?.accountID ?? null); const deleteTransaction = useCallback(() => { - IOU.deleteMoneyRequest(lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), parentReportAction, true); - setIsDeleteModalVisible(false); + const { + originalMessage: {IOUTransactionID}, + } = parentReportAction; + if (IOUTransactionID) { + IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + setIsDeleteModalVisible(false); + } }, [parentReportAction, setIsDeleteModalVisible]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); + const isPending = !!transaction && TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); @@ -94,7 +83,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, setIsDeleteModalVisible(false); }, [canModifyRequest]); - const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; + const menuItem = HeaderUtils.getPinMenuItem(report); + const threeDotsMenuItems = menuItem ? [menuItem] : []; if (canModifyRequest) { if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ @@ -122,7 +112,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} report={{ ...report, - ownerAccountID: lodashGet(parentReport, 'ownerAccountID', null), + ownerAccountID: parentReport?.ownerAccountID, }} policy={policy} personalDetails={personalDetails} @@ -159,29 +149,25 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, } MoneyRequestHeader.displayName = 'MoneyRequestHeader'; -MoneyRequestHeader.propTypes = propTypes; -MoneyRequestHeader.defaultProps = defaultProps; -export default compose( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, - canEvict: false, +const MoneyRequestHeaderWithTransaction = withOnyx>({ + transaction: { + key: ({report, parentReportActions}) => { + const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : ({} as ReportAction); + return `${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction.originalMessage as IOUMessage).IOUTransactionID ?? 0}`; }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(MoneyRequestHeader); + }, +})(MoneyRequestHeader); + +export default withOnyx, Omit>({ + session: { + key: ONYXKEYS.SESSION, + }, + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? '0'}`, + canEvict: false, + }, +})(MoneyRequestHeaderWithTransaction); diff --git a/src/libs/HeaderUtils.ts b/src/libs/HeaderUtils.ts index ebf1b1139621..38cd57156af7 100644 --- a/src/libs/HeaderUtils.ts +++ b/src/libs/HeaderUtils.ts @@ -7,7 +7,7 @@ import * as Session from './actions/Session'; import * as Localize from './Localize'; type MenuItem = { - icon: string | IconAsset; + icon: IconAsset; text: string; onSelected: () => void; }; From c37ab71e374b7f853e8ea22bad21fe329cd6fece Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 8 Jan 2024 18:55:27 +0000 Subject: [PATCH 026/418] refactor(typescript): change import to import type for types --- src/components/MoneyRequestHeader.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 73b4148279ba..878f0d3996f2 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; -import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -13,8 +14,8 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; -import {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; @@ -153,8 +154,8 @@ MoneyRequestHeader.displayName = 'MoneyRequestHeader'; const MoneyRequestHeaderWithTransaction = withOnyx>({ transaction: { key: ({report, parentReportActions}) => { - const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : ({} as ReportAction); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction.originalMessage as IOUMessage).IOUTransactionID ?? 0}`; + const parentReportAction = (report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : {}) as ReportAction & OriginalMessageIOU; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${parentReportAction.originalMessage.IOUTransactionID ?? 0}`; }, }, })(MoneyRequestHeader); From cc24a973e993f141dba37895b8f745b18ebe218e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:20:41 -0700 Subject: [PATCH 027/418] Add the data to be loaded from onyx --- .../report/withReportAndReportActionOrNotFound.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index fb0a00e2d10d..5d4590ac4746 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -27,6 +27,9 @@ type OnyxProps = { /** Array of report actions for this report */ reportActions: OnyxEntry; + /** The report's parentReportAction */ + parentReportAction: OnyxEntry; + /** The policies which the user has access to */ policies: OnyxCollection; @@ -114,6 +117,17 @@ export default function (WrappedComponent: key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, canEvict: false, }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, + selector: (parentReportActions, props) => { + const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); + if (!parentReportActionID) { + return {}; + } + return parentReportActions[parentReportActionID]; + }, + canEvict: false, + }, }), withWindowDimensions, )(React.forwardRef(WithReportOrNotFound)); From d318c952d017a1b59d1672bce48fabd3fd483d84 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:25:49 -0700 Subject: [PATCH 028/418] Remove the use of lodash. --- src/pages/home/report/withReportAndReportActionOrNotFound.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 5d4590ac4746..adc9663a4f34 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -120,7 +120,7 @@ export default function (WrappedComponent: parentReportAction: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, selector: (parentReportActions, props) => { - const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); + const parentReportActionID = props?.report?.parentReportActionID; if (!parentReportActionID) { return {}; } From b2b8868c71539cacd95d4f3275bf92cdc3937723 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:38:53 -0700 Subject: [PATCH 029/418] Remove deprecated method --- .../home/report/withReportAndReportActionOrNotFound.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index adc9663a4f34..83c62bca5e4a 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -9,7 +9,6 @@ import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import compose from '@libs/compose'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Report from '@userActions/Report'; @@ -52,11 +51,11 @@ export default function (WrappedComponent: // Handle threads if needed if (!reportAction?.reportActionID) { - reportAction = ReportActionsUtils.getParentReportAction(props.report); + reportAction = props?.parentReportAction ?? {}; } return reportAction; - }, [props.report, props.reportActions, props.route.params.reportActionID]); + }, [props.reportActions, props.route.params.reportActionID, props.parentReportAction]); const reportAction = getReportAction(); From f4aff81e8292c86d1a2ad4325fb9bfdde9082079 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 9 Jan 2024 11:50:14 +0100 Subject: [PATCH 030/418] Use transaction.isLoading to identify pending Distance requests --- src/components/DistanceEReceipt.js | 4 +--- .../ReportActionItem/MoneyRequestPreview.js | 17 ++++++++--------- .../ReportActionItem/MoneyRequestView.js | 9 ++------- .../ReportActionItem/ReportPreview.js | 10 ++-------- src/libs/ReportUtils.ts | 17 +++++++++++++++-- src/libs/TransactionUtils.ts | 18 +++++++++++++++++- src/types/onyx/Transaction.ts | 2 ++ 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 0241eea44063..f566fb77b912 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -7,7 +7,6 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -35,8 +34,7 @@ function DistanceEReceipt({transaction}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; - const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); - const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); + const {formattedAmount: formattedTransactionAmount, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index c052a885245f..f66b5b90efbd 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -152,7 +152,13 @@ function MoneyRequestPreview(props) { // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(props.transaction); + const { + amount: requestAmount, + formattedAmount: formattedRequestAmount, + currency: requestCurrency, + comment: requestComment, + merchant, + } = ReportUtils.getTransactionDetails(props.transaction); const description = truncate(requestComment, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(props.transaction); @@ -166,13 +172,10 @@ function MoneyRequestPreview(props) { // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan const shouldShowMerchant = !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; - const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); let merchantOrDescription = requestMerchant; if (!shouldShowMerchant) { merchantOrDescription = description || ''; - } else if (hasPendingWaypoints) { - merchantOrDescription = requestMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; @@ -221,10 +224,6 @@ function MoneyRequestPreview(props) { }; const getDisplayAmountText = () => { - if (isDistanceRequest) { - return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : translate('common.tbd'); - } - if (isScanning) { return translate('iou.receiptScanning'); } @@ -233,7 +232,7 @@ function MoneyRequestPreview(props) { return Localize.translateLocal('iou.receiptMissingDetails'); } - return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); + return formattedRequestAmount; }; const getDisplayDeleteAmountText = () => { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 37ff163f23c8..42de7b461f9d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -128,7 +128,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const { created: transactionDate, amount: transactionAmount, - currency: transactionCurrency, + formattedAmount: formattedTransactionAmount, comment: transactionDescription, merchant: transactionMerchant, billable: transactionBillable, @@ -140,11 +140,6 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - let formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - const hasPendingWaypoints = lodashGet(transaction, 'pendingFields.waypoints', null); - if (isDistanceRequest && (!formattedTransactionAmount || hasPendingWaypoints)) { - formattedTransactionAmount = translate('common.tbd'); - } const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; @@ -274,7 +269,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null)); - if (hasPendingWaypoints) { - formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')); - } + const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { @@ -178,7 +175,7 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasPendingWaypoints) { + if (hasOnlyLoadingDistanceRequests) { return props.translate('common.tbd'); } if (totalDisplaySpend) { @@ -187,9 +184,6 @@ function ReportPreview(props) { if (isScanning) { return props.translate('iou.receiptScanning'); } - if (hasOnlyDistanceRequests) { - return props.translate('common.tbd'); - } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0d7658adf180..dad7314168ae 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -292,6 +292,7 @@ type TransactionDetails = cardID: number; originalAmount: number; originalCurrency: string; + formattedAmount: string; } | undefined; @@ -1835,11 +1836,23 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF if (!transaction) { return; } + const report = getReport(transaction?.reportID); + const amount = TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)); + const currency = TransactionUtils.getCurrency(transaction); + + let formattedAmount; + if (TransactionUtils.isLoadingDistanceRequest(transaction)) { + formattedAmount = Localize.translateLocal('common.tbd'); + } else { + formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; + } + return { created: TransactionUtils.getCreated(transaction, createdDateFormat), - amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), - currency: TransactionUtils.getCurrency(transaction), + amount, + currency, + formattedAmount, comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), waypoints: TransactionUtils.getWaypoints(transaction), diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..7badbc524085 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -2,6 +2,7 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; @@ -49,6 +50,15 @@ function isDistanceRequest(transaction: Transaction): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } +function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { + if (!transaction) { + return false; + } + + const amount = getAmount(transaction, false); + return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); +} + function isScanRequest(transaction: Transaction): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { @@ -311,7 +321,12 @@ function getOriginalAmount(transaction: Transaction): number { * Return the merchant field from the transaction, return the modifiedMerchant if present. */ function getMerchant(transaction: OnyxEntry): string { - return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; + if (!transaction) { + return ''; + } + + const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; + return isLoadingDistanceRequest(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; } function getDistance(transaction: Transaction): number { @@ -566,6 +581,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, + isLoadingDistanceRequest, isExpensifyCardTransaction, isCardTransaction, isPending, diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8b7e26280305..4b8a0a1cf9b3 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -94,6 +94,8 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; + + isLoading?: boolean; }; export default Transaction; From b30a5e7eb90279177b4c51febdc1cfa5a7c4db36 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 9 Jan 2024 15:15:38 +0100 Subject: [PATCH 031/418] Use transaction.isLoading to identify pending Distance requests --- .../ReportActionItem/ReportPreview.js | 2 +- src/libs/TransactionUtils.ts | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index e0346e3c6bcb..d50811c15fbe 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -157,7 +157,7 @@ function ReportPreview(props) { const hasErrors = hasReceipts && hasMissingSmartscanFields; const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); const previewSubtitle = formattedMerchant || diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 7badbc524085..9ab09a1978e1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -2,7 +2,6 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; @@ -12,6 +11,7 @@ import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/on import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; +import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; @@ -50,15 +50,6 @@ function isDistanceRequest(transaction: Transaction): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } -function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { - if (!transaction) { - return false; - } - - const amount = getAmount(transaction, false); - return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); -} - function isScanRequest(transaction: Transaction): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { @@ -317,6 +308,20 @@ function getOriginalAmount(transaction: Transaction): number { return Math.abs(amount); } +/** + * Verify if the transaction is of Distance request and is not fully ready: + * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server + * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation + */ +function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { + if (!transaction) { + return false; + } + + const amount = getAmount(transaction, false); + return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); +} + /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ From f81c7a1fc924d16ea6043944b3240d2fe8da9ec1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 9 Jan 2024 23:37:19 +0700 Subject: [PATCH 032/418] fix lint --- src/components/MenuItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 14b721fa3d4f..afb5f0cfa173 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -135,8 +135,8 @@ type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { /** Error to display below the title */ error?: string; - /** Error to display at the bottom of the component */ - errorText?: MaybePhraseKey; + /** Error to display at the bottom of the component */ + errorText?: MaybePhraseKey; /** A boolean flag that gives the icon a green fill if true */ success?: boolean; From 48b5419b3c0266f852c30c306e75ab5a316c6408 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Tue, 26 Dec 2023 09:38:00 +0700 Subject: [PATCH 033/418] 33546 visual viewport deplay --- src/components/ScreenWrapper.tsx | 2 +- src/hooks/useWindowDimensions/index.ts | 83 +++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 0653e2ff8577..97bff2a6b3bb 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -118,7 +118,7 @@ function ScreenWrapper( */ const navigationFallback = useNavigation>(); const navigation = navigationProp ?? navigationFallback; - const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const { windowHeight, isSmallScreenWidth } = useWindowDimensions(shouldEnableMaxHeight); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); const keyboardState = useKeyboardState(); diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index b0a29e9f901b..d0cc34701630 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,13 +1,21 @@ +import {useEffect, useMemo, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; +import * as Browser from '@libs/Browser'; import variables from '@styles/variables'; import type WindowDimensions from './types'; +const initalViewportHeight = window.visualViewport?.height ?? window.innerHeight; +const tagNamesOpenKeyboard = ['INPUT', 'TEXTAREA']; + /** * A convenience wrapper around React Native's useWindowDimensions hook that also provides booleans for our breakpoints. */ -export default function (): WindowDimensions { +export default function (isCachedViewportHeight = false): WindowDimensions { + const shouldAwareVitualViewportHeight = isCachedViewportHeight && Browser.isMobileSafari(); + const cachedViewportHeightWithKeyboardRef = useRef(initalViewportHeight); const {width: windowWidth, height: windowHeight} = useWindowDimensions(); + // When the soft keyboard opens on mWeb, the window height changes. Use static screen height instead to get real screenHeight. const screenHeight = Dimensions.get('screen').height; const isExtraSmallScreenHeight = screenHeight <= variables.extraSmallMobileResponsiveHeightBreakpoint; @@ -15,12 +23,69 @@ export default function (): WindowDimensions { const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; - return { - windowWidth, - windowHeight, - isExtraSmallScreenHeight, - isSmallScreenWidth, - isMediumScreenWidth, - isLargeScreenWidth, - }; + const [cachedViewportHeight, setCachedViewportHeight] = useState(windowHeight); + + const handleFocusIn = useRef((event: FocusEvent) => { + const targetElement = event.target as HTMLElement; + if (tagNamesOpenKeyboard.includes(targetElement.tagName)) { + setCachedViewportHeight(cachedViewportHeightWithKeyboardRef.current); + } + }); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + if (shouldAwareVitualViewportHeight) { + window.addEventListener('focusin', handleFocusIn.current); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + window.removeEventListener('focusin', handleFocusIn.current); + }; + } + }, [shouldAwareVitualViewportHeight]); + + const handleFocusOut = useRef((event: FocusEvent) => { + const targetElement = event.target as HTMLElement; + if (tagNamesOpenKeyboard.includes(targetElement.tagName)) { + setCachedViewportHeight(initalViewportHeight); + } + }); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + if (shouldAwareVitualViewportHeight) { + window.addEventListener('focusout', handleFocusOut.current); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + window.removeEventListener('focusout', handleFocusOut.current); + }; + } + }, [shouldAwareVitualViewportHeight]); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + if (shouldAwareVitualViewportHeight && windowHeight < cachedViewportHeightWithKeyboardRef.current) { + setCachedViewportHeight(windowHeight); + } + }, [windowHeight, shouldAwareVitualViewportHeight]); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + if (shouldAwareVitualViewportHeight && window.matchMedia('(orientation: portrait)').matches) { + if (windowHeight < initalViewportHeight) { + cachedViewportHeightWithKeyboardRef.current = windowHeight; + } + } + }, [shouldAwareVitualViewportHeight, windowHeight]); + + return useMemo( + () => ({ + windowWidth, + windowHeight: shouldAwareVitualViewportHeight ? cachedViewportHeight : windowHeight, + isExtraSmallScreenHeight, + isSmallScreenWidth, + isMediumScreenWidth, + isLargeScreenWidth, + }), + [windowWidth, shouldAwareVitualViewportHeight, cachedViewportHeight, windowHeight, isExtraSmallScreenHeight, isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth], + ); } From 5ae984d2c65f9a9ff2f510adab244f43fb3f72f0 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 10 Jan 2024 22:30:34 +0700 Subject: [PATCH 034/418] 33546 cleanup prefer-early-return --- src/components/ScreenWrapper.tsx | 2 +- src/hooks/useWindowDimensions/index.ts | 73 ++++++++++++-------------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 97bff2a6b3bb..5b1ad95b6554 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -118,7 +118,7 @@ function ScreenWrapper( */ const navigationFallback = useNavigation>(); const navigation = navigationProp ?? navigationFallback; - const { windowHeight, isSmallScreenWidth } = useWindowDimensions(shouldEnableMaxHeight); + const {windowHeight, isSmallScreenWidth} = useWindowDimensions(shouldEnableMaxHeight); const {initialHeight} = useInitialDimensions(); const styles = useThemeStyles(); const keyboardState = useKeyboardState(); diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index d0cc34701630..0b805202ede8 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -11,8 +11,8 @@ const tagNamesOpenKeyboard = ['INPUT', 'TEXTAREA']; /** * A convenience wrapper around React Native's useWindowDimensions hook that also provides booleans for our breakpoints. */ -export default function (isCachedViewportHeight = false): WindowDimensions { - const shouldAwareVitualViewportHeight = isCachedViewportHeight && Browser.isMobileSafari(); +export default function (useCachedViewportHeight = false): WindowDimensions { + const isCachedViewportHeight = useCachedViewportHeight && Browser.isMobileSafari(); const cachedViewportHeightWithKeyboardRef = useRef(initalViewportHeight); const {width: windowWidth, height: windowHeight} = useWindowDimensions(); @@ -32,16 +32,16 @@ export default function (isCachedViewportHeight = false): WindowDimensions { } }); - // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { - if (shouldAwareVitualViewportHeight) { - window.addEventListener('focusin', handleFocusIn.current); - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - window.removeEventListener('focusin', handleFocusIn.current); - }; + if (!isCachedViewportHeight) { + return; } - }, [shouldAwareVitualViewportHeight]); + window.addEventListener('focusin', handleFocusIn.current); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + window.removeEventListener('focusin', handleFocusIn.current); + }; + }, [isCachedViewportHeight]); const handleFocusOut = useRef((event: FocusEvent) => { const targetElement = event.target as HTMLElement; @@ -50,42 +50,37 @@ export default function (isCachedViewportHeight = false): WindowDimensions { } }); - // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { - if (shouldAwareVitualViewportHeight) { - window.addEventListener('focusout', handleFocusOut.current); - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - window.removeEventListener('focusout', handleFocusOut.current); - }; + if (!isCachedViewportHeight) { + return; } - }, [shouldAwareVitualViewportHeight]); + window.addEventListener('focusout', handleFocusOut.current); + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + window.removeEventListener('focusout', handleFocusOut.current); + }; + }, [isCachedViewportHeight]); - // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { - if (shouldAwareVitualViewportHeight && windowHeight < cachedViewportHeightWithKeyboardRef.current) { - setCachedViewportHeight(windowHeight); + if (!isCachedViewportHeight && windowHeight >= cachedViewportHeightWithKeyboardRef.current) { + return; } - }, [windowHeight, shouldAwareVitualViewportHeight]); + setCachedViewportHeight(windowHeight); + }, [windowHeight, isCachedViewportHeight]); - // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { - if (shouldAwareVitualViewportHeight && window.matchMedia('(orientation: portrait)').matches) { - if (windowHeight < initalViewportHeight) { - cachedViewportHeightWithKeyboardRef.current = windowHeight; - } + if (!isCachedViewportHeight || !window.matchMedia('(orientation: portrait)').matches || windowHeight >= initalViewportHeight) { + return; } - }, [shouldAwareVitualViewportHeight, windowHeight]); + cachedViewportHeightWithKeyboardRef.current = windowHeight; + }, [isCachedViewportHeight, windowHeight]); - return useMemo( - () => ({ - windowWidth, - windowHeight: shouldAwareVitualViewportHeight ? cachedViewportHeight : windowHeight, - isExtraSmallScreenHeight, - isSmallScreenWidth, - isMediumScreenWidth, - isLargeScreenWidth, - }), - [windowWidth, shouldAwareVitualViewportHeight, cachedViewportHeight, windowHeight, isExtraSmallScreenHeight, isSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth], - ); + return { + windowWidth, + windowHeight: isCachedViewportHeight ? cachedViewportHeight : windowHeight, + isExtraSmallScreenHeight, + isSmallScreenWidth, + isMediumScreenWidth, + isLargeScreenWidth, + }; } From 9a0471b3f43715d78ef2f17fb038e77b1f560487 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:20:20 +0100 Subject: [PATCH 035/418] Create Automation for when main fail --- .github/workflows/failureNotifier.yml | 108 ++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/workflows/failureNotifier.yml diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml new file mode 100644 index 000000000000..794c472994f3 --- /dev/null +++ b/.github/workflows/failureNotifier.yml @@ -0,0 +1,108 @@ +name: Notify on Workflow Failure + +on: + workflow_run: + workflows: ["Process new code merged to main"] + types: + - completed + +jobs: + notify-failure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fetch Workflow Run Details + id: fetch-workflow-details + uses: actions/github-script@v7 + with: + script: | + const runId = "${{ github.event.workflow_run.id }}"; + const runData = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + return runData.data; + + - name: Fetch Workflow Run Jobs + id: fetch-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const runId = "${{ github.event.workflow_run.id }}"; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + + - name: Get merged pull request number + id: getMergedPullRequestNumber + uses: actions-ecosystem/action-get-merged-pull-request@59afe90821bb0b555082ce8ff1e36b03f91553d9 + with: + github_token: ${{ github.token }} + + + - name: Process Each Failed Job + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.OS_BOTIFY_TOKEN }} + script: | + const prNumber = ${{ steps.getMergedPullRequestNumber.outputs.number }}; + const runData = ${{fromJSON(steps.fetch-workflow-details.outputs.result)}}; + const jobs = ${{fromJSON(steps.fetch-workflow-jobs.outputs.result)}}; + const failureLabel = 'workflow-failure'; + + const prData = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + + const pr = prData.data; + + const prLink = pr.html_url; + const prAuthor = pr.user.login; + const prMerger = pr.merged_by.login; + + for (let i=0; i issue.title.includes(jobName)); + if (!existingIssue) { + const issueTitle = `🔍 Investigation Needed: ${jobName} Failure due to PR Merge 🔍`; + const issueBody = `🚨 **Failure Summary** 🚨:\n\n + - **📋 Job Name**: [${jobName}](${jobLink})\n + - **🔧 Failure in Workflow**: Main branch\n + - **🔗 Triggered by PR**: [PR Link](${prLink})\n + - **👤 PR Author**: @${prAuthor}\n + - **🤝 Merged by**: @${prMerger}\n\n + ⚠️ **Action Required** ⚠️:\n\n + 🛠️ A recent merge appears to have caused a failure in the job named [${jobName}](${jobLink}). + This issue has been automatically created and labeled with \`${failureLabel}\` for investigation. \n\n + 👀 **Please look into the following**:\n + 1. **Why the PR caused the job to fail?**\n + 2. **Address any underlying issues.**\n\n + 🐛 We appreciate your help in squashing this bug!`; + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: [failureLabel], + assignees: [prMerger, prAuthor] + }); + } + } + } From 1064c3248a590dd44c11e6d3de27f73b8769d950 Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Wed, 10 Jan 2024 23:23:20 +0700 Subject: [PATCH 036/418] fix eslint --- src/hooks/useWindowDimensions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 0b805202ede8..4ba2c4ad9b41 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -1,4 +1,4 @@ -import {useEffect, useMemo, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import {Dimensions, useWindowDimensions} from 'react-native'; import * as Browser from '@libs/Browser'; From 0ce489bf90216bd510748ce44255f11106eb5d60 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 10 Jan 2024 18:55:29 +0100 Subject: [PATCH 037/418] Fix workflow --- .github/workflows/failureNotifier.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 794c472994f3..f1b94b97f8a3 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -6,6 +6,9 @@ on: types: - completed +permissions: + issues: write + jobs: notify-failure: runs-on: ubuntu-latest @@ -50,11 +53,10 @@ jobs: - name: Process Each Failed Job uses: actions/github-script@v7 with: - github-token: ${{ secrets.OS_BOTIFY_TOKEN }} script: | const prNumber = ${{ steps.getMergedPullRequestNumber.outputs.number }}; const runData = ${{fromJSON(steps.fetch-workflow-details.outputs.result)}}; - const jobs = ${{fromJSON(steps.fetch-workflow-jobs.outputs.result)}}; + const jobs = ${{steps.fetch-workflow-jobs.outputs.result}}; const failureLabel = 'workflow-failure'; const prData = await github.rest.pulls.get({ From 28143ef5b4c778d577abf2d966fa596a2ac6ea8b Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:00:33 +0100 Subject: [PATCH 038/418] fix issue body --- .github/workflows/failureNotifier.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index f1b94b97f8a3..96647ee261c3 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -84,19 +84,19 @@ jobs: const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); if (!existingIssue) { const issueTitle = `🔍 Investigation Needed: ${jobName} Failure due to PR Merge 🔍`; - const issueBody = `🚨 **Failure Summary** 🚨:\n\n - - **📋 Job Name**: [${jobName}](${jobLink})\n - - **🔧 Failure in Workflow**: Main branch\n - - **🔗 Triggered by PR**: [PR Link](${prLink})\n - - **👤 PR Author**: @${prAuthor}\n - - **🤝 Merged by**: @${prMerger}\n\n - ⚠️ **Action Required** ⚠️:\n\n - 🛠️ A recent merge appears to have caused a failure in the job named [${jobName}](${jobLink}). - This issue has been automatically created and labeled with \`${failureLabel}\` for investigation. \n\n - 👀 **Please look into the following**:\n - 1. **Why the PR caused the job to fail?**\n - 2. **Address any underlying issues.**\n\n - 🐛 We appreciate your help in squashing this bug!`; + const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + + `- **📋 Job Name**: [${jobName}](${jobLink})\n` + + `- **🔧 Failure in Workflow**: Main branch\n` + + `- **🔗 Triggered by PR**: [PR Link](${prLink})\n` + + `- **👤 PR Author**: @${prAuthor}\n` + + `- **🤝 Merged by**: @${prMerger}\n\n` + + `⚠️ **Action Required** ⚠️:\n\n` + + `🛠️ A recent merge appears to have caused a failure in the job named [${jobName}](${jobLink}).\n` + + `This issue has been automatically created and labeled with \`${failureLabel}\` for investigation. \n\n` + + `👀 **Please look into the following**:\n` + + `1. **Why the PR caused the job to fail?**\n` + + `2. **Address any underlying issues.**\n\n` + + `🐛 We appreciate your help in squashing this bug!`; github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, From 0e41b6613fc611fc6d407d3a5876f4ac705e3e33 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:24:48 +0100 Subject: [PATCH 039/418] add daily label --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 96647ee261c3..fd70b02adb29 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -102,7 +102,7 @@ jobs: repo: context.repo.repo, title: issueTitle, body: issueBody, - labels: [failureLabel], + labels: [failureLabel, 'daily'], assignees: [prMerger, prAuthor] }); } From 10baa724ac7821a6f70a49c09b70b50ff206f0ae Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:19:08 +0100 Subject: [PATCH 040/418] address review comments --- .github/workflows/failureNotifier.yml | 32 ++++++++------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index fd70b02adb29..82b0b0fbe6ed 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -17,19 +17,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Fetch Workflow Run Details - id: fetch-workflow-details - uses: actions/github-script@v7 - with: - script: | - const runId = "${{ github.event.workflow_run.id }}"; - const runData = await github.rest.actions.getWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - return runData.data; - - name: Fetch Workflow Run Jobs id: fetch-workflow-jobs uses: actions/github-script@v7 @@ -55,8 +42,7 @@ jobs: with: script: | const prNumber = ${{ steps.getMergedPullRequestNumber.outputs.number }}; - const runData = ${{fromJSON(steps.fetch-workflow-details.outputs.result)}}; - const jobs = ${{steps.fetch-workflow-jobs.outputs.result}}; + const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; const failureLabel = 'workflow-failure'; const prData = await github.rest.pulls.get({ @@ -71,7 +57,7 @@ jobs: const prAuthor = pr.user.login; const prMerger = pr.merged_by.login; - for (let i=0; i issue.title.includes(jobName)); if (!existingIssue) { - const issueTitle = `🔍 Investigation Needed: ${jobName} Failure due to PR Merge 🔍`; + const issueTitle = `🔍 Investigation Needed: ${ jobName } Failure due to PR Merge 🔍`; const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + - `- **📋 Job Name**: [${jobName}](${jobLink})\n` + + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + `- **🔧 Failure in Workflow**: Main branch\n` + - `- **🔗 Triggered by PR**: [PR Link](${prLink})\n` + - `- **👤 PR Author**: @${prAuthor}\n` + - `- **🤝 Merged by**: @${prMerger}\n\n` + + `- **🔗 Triggered by PR**: [PR Link](${ prLink })\n` + + `- **👤 PR Author**: @${ prAuthor }\n` + + `- **🤝 Merged by**: @${ prMerger }\n\n` + `⚠️ **Action Required** ⚠️:\n\n` + - `🛠️ A recent merge appears to have caused a failure in the job named [${jobName}](${jobLink}).\n` + - `This issue has been automatically created and labeled with \`${failureLabel}\` for investigation. \n\n` + + `🛠️ A recent merge appears to have caused a failure in the job named [${ jobName }](${ jobLink }).\n` + + `This issue has been automatically created and labeled with \`${ failureLabel }\` for investigation. \n\n` + `👀 **Please look into the following**:\n` + `1. **Why the PR caused the job to fail?**\n` + `2. **Address any underlying issues.**\n\n` + From ad37f60a6122a8e92018ade0f605331ab59312fb Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 12 Jan 2024 18:10:46 +0000 Subject: [PATCH 041/418] refactor(typescript): apply pull request feedback --- src/components/MoneyRequestHeader.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 878f0d3996f2..2f20fe808e59 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -14,7 +14,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -47,7 +47,7 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { parentReportAction: ReportAction & OriginalMessageIOU; /** Personal details so we can get the ones for the report participants */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}: MoneyRequestHeaderProps) { @@ -64,12 +64,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const deleteTransaction = useCallback(() => { const { - originalMessage: {IOUTransactionID}, + originalMessage: {IOUTransactionID = ''}, } = parentReportAction; - if (IOUTransactionID) { - IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); - setIsDeleteModalVisible(false); - } + IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); From 4237770a84762af4549ec8c3713a4990e139373b Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:37:23 -0700 Subject: [PATCH 042/418] Update onyx --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 373610463b38..ac012bea728f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.126", "react-native-pager-view": "6.2.2", "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", @@ -47034,17 +47034,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.126", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz", + "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=16.15.1 <=20.9.0", - "npm": ">=8.11.0 <=10.1.0" + "node": "20.9.0", + "npm": "10.1.0" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -89702,9 +89702,9 @@ } }, "react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.126", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz", + "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index e8b724587ca0..4a28617f649d 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.126", "react-native-pager-view": "6.2.2", "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", From 83b1547492510fc2390c5969db15d40fc092a4b8 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:43:03 -0700 Subject: [PATCH 043/418] Fix types --- .../home/report/withReportAndReportActionOrNotFound.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 83c62bca5e4a..ccad69282fcb 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -117,15 +117,14 @@ export default function (WrappedComponent: canEvict: false, }, parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, - selector: (parentReportActions, props) => { + key: (props) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${props?.report?.parentReportID ?? 0}`, + selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { const parentReportActionID = props?.report?.parentReportActionID; if (!parentReportActionID) { - return {}; + return null; } - return parentReportActions[parentReportActionID]; + return parentReportActions?.[parentReportActionID] ?? null; }, - canEvict: false, }, }), withWindowDimensions, From 244005198afb8fb078561e7bd8b1f80affcd148e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:43:21 -0700 Subject: [PATCH 044/418] Remove extra onyx subscription --- src/pages/FlagCommentPage.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index 47d2ad356dad..c75f76135aaf 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -44,13 +44,13 @@ const propTypes = { ...withLocalizePropTypes, /* Onyx Props */ - /** All the report actions from the parent report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The full action from the parent report */ + parentReportAction: PropTypes.shape(reportActionPropTypes), }; const defaultProps = { reportActions: {}, - parentReportActions: {}, + parentReportAction: {}, report: {}, }; @@ -124,19 +124,18 @@ function FlagCommentPage(props) { // Handle threads if needed if (reportAction === undefined || reportAction.reportActionID === undefined) { - reportAction = props.parentReportActions[props.report.parentReportActionID] || {}; + reportAction = props.parentReportAction; } return reportAction; - }, [props.report, props.reportActions, props.route.params.reportActionID, props.parentReportActions]); + }, [props.reportActions, props.route.params.reportActionID, props.parentReportAction]); const flagComment = (severity) => { let reportID = getReportID(props.route); const reportAction = getActionToFlag(); - const parentReportAction = props.parentReportActions[props.report.parentReportActionID] || {}; // Handle threads if needed - if (ReportUtils.isChatThread(props.report) && reportAction.reportActionID === parentReportAction.reportActionID) { + if (ReportUtils.isChatThread(props.report) && reportAction.reportActionID === props.parentReportAction.reportActionID) { reportID = ReportUtils.getParentReport(props.report).reportID; } @@ -200,10 +199,4 @@ FlagCommentPage.displayName = 'FlagCommentPage'; export default compose( withLocalize, withReportAndReportActionOrNotFound, - withOnyx({ - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || report.reportID}`, - canEvict: false, - }, - }), )(FlagCommentPage); From 05b979d1411ef255ac353b3e5828008c171c7026 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:10:32 -0700 Subject: [PATCH 045/418] Add all withOnyx properties and import type --- src/pages/home/report/withReportAndReportActionOrNotFound.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index ccad69282fcb..f58315f52b88 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -4,6 +4,7 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {useCallback, useEffect} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {WithOnyxInstanceState} from 'react-native-onyx/lib/types'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; @@ -125,6 +126,7 @@ export default function (WrappedComponent: } return parentReportActions?.[parentReportActionID] ?? null; }, + canEvict: false, }, }), withWindowDimensions, From 6f7b07f3be4920e96d274c1b68679e85912ef0f6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:14:10 -0700 Subject: [PATCH 046/418] Remove unused imports --- src/pages/FlagCommentPage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index c75f76135aaf..55346027a0ec 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -17,7 +16,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import withReportAndReportActionOrNotFound from './home/report/withReportAndReportActionOrNotFound'; From fe4cb1a237a69ed68a04b45184f57f1e356f9433 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:47:42 -0700 Subject: [PATCH 047/418] Style --- src/pages/FlagCommentPage.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index 55346027a0ec..330f2fc6c44e 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -194,7 +194,4 @@ FlagCommentPage.propTypes = propTypes; FlagCommentPage.defaultProps = defaultProps; FlagCommentPage.displayName = 'FlagCommentPage'; -export default compose( - withLocalize, - withReportAndReportActionOrNotFound, -)(FlagCommentPage); +export default compose(withLocalize, withReportAndReportActionOrNotFound)(FlagCommentPage); From 7f0d3c4f66378c07a7f6ca862fd03fbc2242d6ce Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 4 Jan 2024 10:42:54 +0100 Subject: [PATCH 048/418] [TS migration] Migrate 'CommunicationsLink.js' component to TypeScript --- src/components/CommunicationsLink.js | 51 --------------------------- src/components/CommunicationsLink.tsx | 42 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 51 deletions(-) delete mode 100644 src/components/CommunicationsLink.js create mode 100644 src/components/CommunicationsLink.tsx diff --git a/src/components/CommunicationsLink.js b/src/components/CommunicationsLink.js deleted file mode 100644 index 01ae0354a66d..000000000000 --- a/src/components/CommunicationsLink.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Clipboard from '@libs/Clipboard'; -import ContextMenuItem from './ContextMenuItem'; -import * as Expensicons from './Icon/Expensicons'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** Children to wrap in CommunicationsLink. */ - children: PropTypes.node.isRequired, - - /** Styles to be assigned to Container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Value to be copied or passed via tap. */ - value: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - containerStyles: [], -}; - -function CommunicationsLink(props) { - const styles = useThemeStyles(); - return ( - - - {props.children} - Clipboard.setString(props.value)} - /> - - - ); -} - -CommunicationsLink.propTypes = propTypes; -CommunicationsLink.defaultProps = defaultProps; -CommunicationsLink.displayName = 'CommunicationsLink'; - -export default withLocalize(CommunicationsLink); diff --git a/src/components/CommunicationsLink.tsx b/src/components/CommunicationsLink.tsx new file mode 100644 index 000000000000..646326e0a632 --- /dev/null +++ b/src/components/CommunicationsLink.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import ContextMenuItem from './ContextMenuItem'; +import * as Expensicons from './Icon/Expensicons'; + +type CommunicationsLinkProps = ChildrenProps & { + /** Styles to be assigned to Container */ + containerStyles?: StyleProp; + + /** Value to be copied or passed via tap. */ + value: string; +}; + +function CommunicationsLink({value, containerStyles, children}: CommunicationsLinkProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + {children} + Clipboard.setString(value)} + /> + + + ); +} + +CommunicationsLink.displayName = 'CommunicationsLink'; + +export default CommunicationsLink; From c02df48b2368b969542700a494a6659b4c618d91 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 16 Jan 2024 17:13:10 +0000 Subject: [PATCH 049/418] refactor(typescript): apply pull request feedback --- src/components/MoneyRequestHeader.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 2f20fe808e59..f3c38b752542 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -63,10 +63,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isActionOwner = parentReportAction.actorAccountID === (session?.accountID ?? null); const deleteTransaction = useCallback(() => { - const { - originalMessage: {IOUTransactionID = ''}, - } = parentReportAction; - IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + IOU.deleteMoneyRequest(parentReportAction.originalMessage?.IOUTransactionID ?? '', parentReportAction, true); setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); From 46dec5946ecb854a0c32b6c6724606bac09089cc Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 16 Jan 2024 22:48:17 +0100 Subject: [PATCH 050/418] Rename isLoadingDistanceRequest -> isDistanceBeingCalculated --- src/components/ReportActionItem/ReportPreview.js | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 4772ec8a55ac..332e45d32cb1 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -166,7 +166,7 @@ function ReportPreview(props) { const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); + const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isDistanceBeingCalculated(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6458ae7368c7..b3218d4747d8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1857,7 +1857,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF const currency = TransactionUtils.getCurrency(transaction); let formattedAmount; - if (TransactionUtils.isLoadingDistanceRequest(transaction)) { + if (TransactionUtils.isDistanceBeingCalculated(transaction)) { formattedAmount = Localize.translateLocal('common.tbd'); } else { formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 9ab09a1978e1..e91ffb9490b8 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -309,11 +309,11 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is not fully ready: + * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server: * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation */ -function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { +function isDistanceBeingCalculated(transaction: OnyxEntry): boolean { if (!transaction) { return false; } @@ -331,7 +331,7 @@ function getMerchant(transaction: OnyxEntry): string { } const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; - return isLoadingDistanceRequest(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; + return isDistanceBeingCalculated(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; } function getDistance(transaction: Transaction): number { @@ -586,7 +586,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, - isLoadingDistanceRequest, + isDistanceBeingCalculated, isExpensifyCardTransaction, isCardTransaction, isPending, From df4e6f0708957a04d26ce6f06f65a92a4fd34fd6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 18 Jan 2024 17:38:27 +0700 Subject: [PATCH 051/418] fix lint --- src/components/AddressSearch/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index f32c83104c94..9b4254a9bc45 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -1,7 +1,7 @@ import type {RefObject} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; -import type { MaybePhraseKey } from '@libs/Localize'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Locale from '@src/types/onyx/Locale'; type CurrentLocationButtonProps = { From 592553b93b7eb1daefcc3960519b8a029be9d6f4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 01:56:34 +0700 Subject: [PATCH 052/418] move translatableTextPropTypes to TS file --- src/components/CountrySelector.js | 2 +- src/components/MagicCodeInput.js | 2 +- .../RoomNameInput/roomNameInputPropTypes.js | 2 +- src/components/StatePicker/index.js | 2 +- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 2 +- src/components/ValuePicker/index.js | 2 +- src/libs/Localize/index.ts | 11 ++++++++++- 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 01d297d35467..36fa55c91f47 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -3,12 +3,12 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import refPropTypes from './refPropTypes'; -import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index b075edc9aeca..c1d3d726e375 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -7,6 +7,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -14,7 +15,6 @@ import networkPropTypes from './networkPropTypes'; import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; -import translatableTextPropTypes from './translatableTextPropTypes'; const TEXT_INPUT_EMPTY_STATE = ''; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f69de3a53f2..f634c6e0b3d6 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Callback to execute when the text input is modified correctly */ diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index dce3c7dc801b..918280f9f953 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -6,9 +6,9 @@ import _ from 'underscore'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import StateSelectorModal from './StateSelectorModal'; const propTypes = { diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index ddc3a5e2f9c2..e6077bde71b3 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import sourcePropTypes from '@components/Image/sourcePropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Input label */ diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index 7f6b310c6374..28fa1ab26af2 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -5,9 +5,9 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import variables from '@styles/variables'; import ValueSelectorModal from './ValueSelectorModal'; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 4e28bdb30549..54a00c3db6f2 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; @@ -97,6 +98,14 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } +/** + * Traslatable text with phrase key and/or variables + * Use MaybePhraseKey for Typescript + * + * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] + */ +const translatableTextPropTypes = PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); + type MaybePhraseKey = string | [string, Record & {isTranslated?: boolean}] | []; /** @@ -174,4 +183,4 @@ function getDevicePreferredLocale(): string { } export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase, MaybePhraseKey}; +export type {PhraseParameters, Phrase, MaybePhraseKey, translatableTextPropTypes}; From ef01f3763274c676dc520dc1a3bd446c27e00ee9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 01:59:55 +0700 Subject: [PATCH 053/418] remove redundant file --- src/components/translatableTextPropTypes.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/components/translatableTextPropTypes.js diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js deleted file mode 100644 index 10130ab2da3e..000000000000 --- a/src/components/translatableTextPropTypes.js +++ /dev/null @@ -1,9 +0,0 @@ -import PropTypes from 'prop-types'; - -/** - * Traslatable text with phrase key and/or variables - * Use Localize.MaybePhraseKey instead for Typescript - * - * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] - */ -export default PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); From e520a8e8628ab7d7b91cd071212b6b8cd80839de Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 02:08:12 +0700 Subject: [PATCH 054/418] fix lint --- src/libs/Localize/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 54a00c3db6f2..c8363043567a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -182,5 +182,5 @@ function getDevicePreferredLocale(): string { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase, MaybePhraseKey, translatableTextPropTypes}; +export {translatableTextPropTypes, translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; +export type {PhraseParameters, Phrase, MaybePhraseKey}; From c978bd5caa0be32ffc290de9d884d00172e2fbe0 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 19 Jan 2024 12:22:08 +0100 Subject: [PATCH 055/418] start migrating jest files to TypeScript --- jest/{setup.js => setup.ts} | 2 +- jest/{setupAfterEnv.js => setupAfterEnv.ts} | 0 jest/{setupMockImages.js => setupMockImages.ts} | 8 ++------ src/types/modules/react-native-clipboard.d.ts | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) rename jest/{setup.js => setup.ts} (96%) rename jest/{setupAfterEnv.js => setupAfterEnv.ts} (100%) rename jest/{setupMockImages.js => setupMockImages.ts} (87%) create mode 100644 src/types/modules/react-native-clipboard.d.ts diff --git a/jest/setup.js b/jest/setup.ts similarity index 96% rename from jest/setup.js rename to jest/setup.ts index 38b4b55a68b3..ff53f957331d 100644 --- a/jest/setup.js +++ b/jest/setup.ts @@ -34,6 +34,6 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn(() => new Promise((res) => res())), CachesDirectoryPath: jest.fn(), })); diff --git a/jest/setupAfterEnv.js b/jest/setupAfterEnv.ts similarity index 100% rename from jest/setupAfterEnv.js rename to jest/setupAfterEnv.ts diff --git a/jest/setupMockImages.js b/jest/setupMockImages.ts similarity index 87% rename from jest/setupMockImages.js rename to jest/setupMockImages.ts index 10925aca8736..c48797b3c07b 100644 --- a/jest/setupMockImages.js +++ b/jest/setupMockImages.ts @@ -1,14 +1,10 @@ import fs from 'fs'; import path from 'path'; -import _ from 'underscore'; -/** - * @param {String} imagePath - */ -function mockImages(imagePath) { +function mockImages(imagePath: string) { const imageFilenames = fs.readdirSync(path.resolve(__dirname, `../assets/${imagePath}/`)); // eslint-disable-next-line rulesdir/prefer-early-return - _.each(imageFilenames, (fileName) => { + imageFilenames.forEach((fileName) => { if (/\.svg/.test(fileName)) { jest.mock(`../assets/${imagePath}/${fileName}`, () => () => ''); } diff --git a/src/types/modules/react-native-clipboard.d.ts b/src/types/modules/react-native-clipboard.d.ts new file mode 100644 index 000000000000..14f418a3f8b9 --- /dev/null +++ b/src/types/modules/react-native-clipboard.d.ts @@ -0,0 +1,16 @@ +declare module '@react-native-clipboard/clipboard/jest/clipboard-mock' { + const mockClipboard: { + getString: jest.MockedFunction<() => Promise>; + getImagePNG: jest.MockedFunction<() => void>; + getImageJPG: jest.MockedFunction<() => void>; + setImage: jest.MockedFunction<() => void>; + setString: jest.MockedFunction<() => void>; + hasString: jest.MockedFunction<() => Promise>; + hasImage: jest.MockedFunction<() => Promise>; + hasURL: jest.MockedFunction<() => Promise>; + addListener: jest.MockedFunction<() => void>; + removeAllListeners: jest.MockedFunction<() => void>; + useClipboard: jest.MockedFunction<() => [string, jest.MockedFunction<() => void>]>; + }; + export default mockClipboard; +} From b87642aad54f4f134e799b1a6406baa88ccb2caa Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 19 Jan 2024 13:29:28 +0100 Subject: [PATCH 056/418] Use "Route pending" instead of "TBD" --- src/components/DistanceEReceipt.js | 4 +- .../MoneyRequestConfirmationList.js | 7 ++- ...oraryForRefactorRequestConfirmationList.js | 7 ++- .../ReportActionItem/MoneyReportView.tsx | 2 +- .../ReportActionItem/MoneyRequestPreview.js | 18 +++---- .../ReportActionItem/MoneyRequestView.js | 4 +- .../ReportActionItem/ReportPreview.js | 11 ++-- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/CurrencyUtils.ts | 8 +-- src/libs/DistanceRequestUtils.ts | 5 +- src/libs/ReportUtils.ts | 38 ++++++-------- src/libs/TransactionUtils.ts | 19 ++----- src/libs/actions/IOU.js | 52 +++++++++++++++++-- src/libs/actions/Transaction.ts | 12 +++-- 15 files changed, 119 insertions(+), 70 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index f566fb77b912..75457f2a7cc9 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -34,7 +35,8 @@ function DistanceEReceipt({transaction}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; - const {formattedAmount: formattedTransactionAmount, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); + const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); + const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index f66e73a2ef02..a54ca8c7a424 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -256,7 +256,7 @@ function MoneyRequestConfirmationList(props) { const hasRoute = TransactionUtils.hasRoute(transaction); const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute - ? translate('common.tbd') + ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, props.isDistanceRequest ? currency : props.iouCurrencyCode, @@ -425,6 +425,11 @@ function MoneyRequestConfirmationList(props) { if (!props.isDistanceRequest) { return; } + + if (!hasRoute) { + IOU.setMoneyRequestPendingFields_temporaryForRefactor(props.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + } + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(props.transactionID, distanceMerchant); }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 36d424ea28f2..c6e044b12918 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -285,7 +285,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const hasRoute = TransactionUtils.hasRoute(transaction); const isDistanceRequestWithoutRoute = isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute - ? translate('common.tbd') + ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, @@ -472,6 +472,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (!isDistanceRequest) { return; } + + if (!hasRoute) { + IOU.setMoneyRequestPendingFields_temporaryForRefactor(transaction.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + } + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(transaction.transactionID, distanceMerchant); }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4fcca3e518a5..f092822b94e8 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -42,7 +42,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend; - const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency, ReportUtils.hasOnlyDistanceRequestTransactions(report.reportID)); + const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency); const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency); const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index f8ddef88663a..fd182b1d96ae 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -155,25 +155,21 @@ function MoneyRequestPreview(props) { // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const { - amount: requestAmount, - formattedAmount: formattedRequestAmount, - currency: requestCurrency, - comment: requestComment, - merchant, - } = ReportUtils.getTransactionDetails(props.transaction); + const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(props.transaction); const description = truncate(requestComment, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(props.transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); + const hasPendingRoute = TransactionUtils.hasPendingRoute(props.transaction); const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); const isSettled = ReportUtils.isSettled(props.iouReport.reportID); const isDeleted = lodashGet(props.action, 'pendingAction', null) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan - const shouldShowMerchant = !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; + const shouldShowMerchant = + !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && !hasPendingRoute; const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; @@ -231,11 +227,15 @@ function MoneyRequestPreview(props) { return translate('iou.receiptScanning'); } + if (hasPendingRoute) { + return translate('iou.routePending'); + } + if (TransactionUtils.hasMissingSmartscanFields(props.transaction)) { return Localize.translateLocal('iou.receiptMissingDetails'); } - return formattedRequestAmount; + return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); }; const getDisplayDeleteAmountText = () => { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 677a75453f0e..7703940ea13f 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -128,7 +128,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const { created: transactionDate, amount: transactionAmount, - formattedAmount: formattedTransactionAmount, + currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, billable: transactionBillable, @@ -140,6 +140,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); + const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); + const formattedTransactionAmount = transactionAmount && !hasPendingRoute ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 6c76b49a89eb..cd682d814ba5 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -141,11 +141,11 @@ function ReportPreview(props) { const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); - const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( + const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyPendingDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(props.iouReportID), areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action), - hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID), + hasOnlyPendingDistanceRequests: ReportUtils.hasOnlyTransactionsWithPendingRoutes(props.iouReportID), hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(props.iouReportID), }), // When transactions get updated these status may have changed, so that is a case where we also want to run this. @@ -174,11 +174,10 @@ function ReportPreview(props) { const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + let formattedMerchant = numberOfRequests === 1 && hasReceipts && !hasOnlyPendingDistanceRequests ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant)) { formattedMerchant = null; } - const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isDistanceBeingCalculated(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { @@ -195,8 +194,8 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasOnlyLoadingDistanceRequests) { - return props.translate('common.tbd'); + if (hasOnlyPendingDistanceRequests) { + return props.translate('iou.routePending'); } if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..f1fe2c0367de 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -580,6 +580,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', + routePending: 'Route pending...', receiptScanning: 'Receipt scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..d163a7bc2fa0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -573,6 +573,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', + routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo de recibo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 42387e03c80b..cec9d1e09088 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -2,7 +2,6 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as Localize from './Localize'; import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; import * as NumberFormatUtils from './NumberFormatUtils'; @@ -98,13 +97,8 @@ function convertToFrontendAmount(amountAsInt: number): number { * * @param amountInCents – should be an integer. Anything after a decimal place will be dropped. * @param currency - IOU currency - * @param shouldFallbackToTbd - whether to return 'TBD' instead of a falsy value (e.g. 0.00) */ -function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD, shouldFallbackToTbd = false): string { - if (shouldFallbackToTbd && !amountInCents) { - return Localize.translateLocal('common.tbd'); - } - +function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { const convertedAmount = convertToFrontendAmount(amountInCents); return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c92e9bfd3f67..2309d517df6d 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -95,8 +95,11 @@ function getDistanceMerchant( translate: LocaleContextProps['translate'], toLocaleDigit: LocaleContextProps['toLocaleDigit'], ): string { - const distanceInUnits = hasRoute ? getRoundedDistanceInUnits(distanceInMeters, unit) : translate('common.tbd'); + if (!hasRoute) { + return translate('iou.routePending'); + } + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9260d39bde56..64985920454d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -335,7 +335,6 @@ type TransactionDetails = cardID: number; originalAmount: number; originalCurrency: string; - formattedAmount: string; } | undefined; @@ -1094,10 +1093,9 @@ function hasSingleParticipant(report: OnyxEntry): boolean { } /** - * Checks whether all the transactions linked to the IOU report are of the Distance Request type - * + * Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes */ -function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { +function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): boolean { const transactions = TransactionUtils.getAllReportTransactions(iouReportID); // Early return false in case not having any transaction @@ -1105,7 +1103,7 @@ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): bo return false; } - return transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + return transactions.every((transaction) => TransactionUtils.hasPendingRoute(transaction)); } /** @@ -1902,7 +1900,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); - const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); + const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, @@ -1944,18 +1942,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF if (!transaction) { return; } - const report = getReport(transaction?.reportID); - const amount = TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)); - const currency = TransactionUtils.getCurrency(transaction); - - let formattedAmount; - if (TransactionUtils.isDistanceBeingCalculated(transaction)) { - formattedAmount = Localize.translateLocal('common.tbd'); - } else { - formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; - } - return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), @@ -2153,6 +2140,11 @@ function getTransactionReportName(reportAction: OnyxEntry): string // Transaction data might be empty on app's first load, if so we fallback to Request return Localize.translateLocal('iou.request'); } + + if (TransactionUtils.hasPendingRoute(transaction)) { + return Localize.translateLocal('iou.routePending'); + } + if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2164,7 +2156,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency, TransactionUtils.isDistanceRequest(transaction)) ?? '', + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', comment: transactionDetails?.comment ?? '', }); } @@ -2177,7 +2169,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string function getReportPreviewMessage( report: OnyxEntry | EmptyObject, reportAction: OnyxEntry | EmptyObject = {}, - shouldConsiderReceiptBeingScanned = false, + shouldConsiderScanningReceiptOrPendingRoute = false, isPreviewMessageForParentChatReport = false, policy: OnyxEntry = null, isForListPreview = false, @@ -2225,12 +2217,16 @@ function getReportPreviewMessage( }); } - if (!isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (!isEmptyObject(reportAction) && shouldConsiderScanningReceiptOrPendingRoute && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } + + if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction)) { + return Localize.translateLocal('iou.routePending'); + } } const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; @@ -4717,7 +4713,7 @@ export { buildTransactionThread, areAllRequestsBeingSmartScanned, getTransactionsWithReceipts, - hasOnlyDistanceRequestTransactions, + hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions, hasMissingSmartscanFields, getIOUReportActionDisplayMessage, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bdb37a3ccd76..f6b60b35bce8 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -11,7 +11,6 @@ import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/on import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; -import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; @@ -317,29 +316,21 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server: - * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server - * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation + * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server */ -function isDistanceBeingCalculated(transaction: OnyxEntry): boolean { +function hasPendingRoute(transaction: OnyxEntry): boolean { if (!transaction) { return false; } - const amount = getAmount(transaction, false); - return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); + return isDistanceRequest(transaction) && !!transaction.pendingFields?.waypoints; } /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ function getMerchant(transaction: OnyxEntry): string { - if (!transaction) { - return ''; - } - - const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; - return isDistanceBeingCalculated(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; + return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; } function getDistance(transaction: Transaction): number { @@ -601,7 +592,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, - isDistanceBeingCalculated, + hasPendingRoute, isExpensifyCardTransaction, isCardTransaction, isPending, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..1a9030c89cc4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -214,6 +214,14 @@ function setMoneyRequestMerchant_temporaryForRefactor(transactionID, merchant) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {merchant: merchant.trim()}); } +/** + * @param {String} transactionID + * @param {Object} pendingFields + */ +function setMoneyRequestPendingFields_temporaryForRefactor(transactionID, pendingFields) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields}); +} + /** * @param {String} transactionID * @param {String} category @@ -964,7 +972,8 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - if (!_.has(transactionChanges, 'waypoints')) { + const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + if (!hasPendingWaypoints) { const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); params.reportActionID = updatedReportAction.reportActionID; @@ -1026,7 +1035,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: { ...updatedTransaction, pendingFields, - isLoading: _.has(transactionChanges, 'waypoints'), + isLoading: hasPendingWaypoints, errorFields: null, }, }); @@ -1090,13 +1099,47 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }); - if (_.has(transactionChanges, 'waypoints')) { + if (hasPendingWaypoints) { + // When updating waypoints, we need to explicitly set the transaction's amount and IOU report's total to 0. + // These values must be calculated on the server because they depend on the distance. + optimisticData.push( + ...[ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + total: CONST.IOU.DEFAULT_AMOUNT, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }, + }, + ], + ); + // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors successData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, value: null, }); + + // Revert the transaction's amount to the original value on failure. + // The IOU Report will be fully reverted in the failureData further below. + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: transaction.amount, + modifiedAmount: transaction.modifiedAmount, + }, + }); } // Clear out loading states, pending fields, and add the error fields @@ -1110,7 +1153,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }); - // Reset the iouReport to it's original state + // Reset the iouReport to its original state failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -3672,6 +3715,7 @@ export { setMoneyRequestDescription_temporaryForRefactor, setMoneyRequestMerchant_temporaryForRefactor, setMoneyRequestParticipants_temporaryForRefactor, + setMoneyRequestPendingFields_temporaryForRefactor, setMoneyRequestReceipt, setMoneyRequestTag_temporaryForRefactor, setMoneyRequestAmount, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..8dc83ed37a75 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -64,7 +64,9 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp [`waypoint${index}`]: waypoint, }, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving a new waypoint as this indicates the user is updating their input errorFields: { route: null, @@ -133,7 +135,9 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: ...transaction.comment, waypoints: reIndexedWaypoints, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), }; if (!isRemovedWaypointEmpty) { @@ -246,7 +250,9 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i comment: { waypoints, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving new waypoints as this indicates the user is updating their input errorFields: { route: null, From 5a24138c6aad5b5a2ed01a511ccd5a2e53dd82fa Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 19 Jan 2024 13:49:52 +0100 Subject: [PATCH 057/418] Fix the pending route logic in Distance e-Receipt --- src/components/DistanceEReceipt.js | 11 ++++---- .../ReportActionItem/ReportPreview.js | 12 +++++---- src/libs/ReceiptUtils.ts | 26 ++++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 75457f2a7cc9..3f337281c548 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -4,7 +4,6 @@ import {ScrollView, View} from 'react-native'; import _ from 'underscore'; import EReceiptBackground from '@assets/images/eReceipt_background.svg'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -33,10 +32,10 @@ function DistanceEReceipt({transaction}) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); - const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; + const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); + const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( @@ -64,7 +63,7 @@ function DistanceEReceipt({transaction}) { /> - {isOffline || !thumbnailSource ? ( + {hasPendingRoute || !thumbnailSource ? ( ) : ( - {formattedTransactionAmount} + {!hasPendingRoute && ( + {formattedTransactionAmount} + )} {transactionMerchant} diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index cd682d814ba5..2e5c73b04f54 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -179,11 +179,13 @@ function ReportPreview(props) { formattedMerchant = null; } const previewSubtitle = - formattedMerchant || - props.translate('iou.requestCount', { - count: numberOfRequests - numberOfScanningReceipts, - scanningReceipts: numberOfScanningReceipts, - }); + numberOfRequests === 1 && hasOnlyPendingDistanceRequests + ? '' + : formattedMerchant || + props.translate('iou.requestCount', { + count: numberOfRequests - numberOfScanningReceipts, + scanningReceipts: numberOfScanningReceipts, + }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index bcba68a3a0bd..9e985f0bd60e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; +import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -29,27 +30,28 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { + if (TransactionUtils.hasPendingRoute(transaction)) { + return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; + } + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); - const hasEReceipt = transaction?.hasEReceipt; - if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { - if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; - } + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } - // For local files, we won't have a thumbnail yet - if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true}; - } + // For local files, we won't have a thumbnail yet + if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { + return {thumbnail: null, image: path, isLocalFile: true}; + } - if (isReceiptImage) { - return {thumbnail: `${path}.1024.jpg`, image: path}; - } + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; } const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; From c77614e859ced32948a8a4084113c3ecb8f61345 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:02:59 +0100 Subject: [PATCH 058/418] address review comments --- .github/workflows/failureNotifier.yml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 82b0b0fbe6ed..667fe3a737b2 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -14,9 +14,6 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'failure' }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Fetch Workflow Run Jobs id: fetch-workflow-jobs uses: actions/github-script@v7 @@ -29,34 +26,26 @@ jobs: run_id: runId, }); return jobsData.data; - - - name: Get merged pull request number - id: getMergedPullRequestNumber - uses: actions-ecosystem/action-get-merged-pull-request@59afe90821bb0b555082ce8ff1e36b03f91553d9 - with: - github_token: ${{ github.token }} - - name: Process Each Failed Job uses: actions/github-script@v7 with: script: | - const prNumber = ${{ steps.getMergedPullRequestNumber.outputs.number }}; - const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; - const failureLabel = 'workflow-failure'; + const jobs = ${{steps.fetch-workflow-jobs.outputs.result}}; - const prData = await github.rest.pulls.get({ + const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; + const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: prNumber, + commit_sha: headCommit, }); - const pr = prData.data; - + const pr = prData.data[0]; const prLink = pr.html_url; const prAuthor = pr.user.login; - const prMerger = pr.merged_by.login; + const prMerger = "${{ github.event.workflow_run.actor.login }}"; + const failureLabel = 'workflow-failure'; for (let i = 0; i < jobs.total_count; i++) { if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; From 685460b21cadc78631c9da1df3b410a358a6cd11 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 19 Jan 2024 17:22:48 +0100 Subject: [PATCH 059/418] [TS migration] Migrate 'WorkspaceInitial' page --- ...nitialPage.js => WorkspaceInitialPage.tsx} | 154 +++++++++--------- .../withPolicyAndFullscreenLoading.tsx | 1 + 2 files changed, 75 insertions(+), 80 deletions(-) rename src/pages/workspace/{WorkspaceInitialPage.js => WorkspaceInitialPage.tsx} (76%) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.tsx similarity index 76% rename from src/pages/workspace/WorkspaceInitialPage.js rename to src/pages/workspace/WorkspaceInitialPage.tsx index 80813c847239..9839c13125ea 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,13 +1,14 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -25,66 +26,66 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as App from '@userActions/App'; import * as Policy from '@userActions/Policy'; import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -const propTypes = { - ...policyPropTypes, +type WorkspaceMenuItems = { + translationKey: TranslationPaths; + icon: IconAsset; + action: () => void; + brickRoadIndicator?: ValueOf; +}; - /** All reports shared with the user (coming from Onyx) */ - reports: PropTypes.objectOf(reportPropTypes), +type WorkspaceInitialPageOnyxProps = { + /** All reports shared with the user */ + reports: OnyxCollection; /** Bank account attached to free plan */ - reimbursementAccount: ReimbursementAccountProps.reimbursementAccountPropTypes, + reimbursementAccount: OnyxEntry; }; -const defaultProps = { - reports: {}, - ...policyDefaultProps, - reimbursementAccount: {}, -}; +type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps; -/** - * @param {string} policyID - */ -function openEditor(policyID) { +function openEditor(policyID: string) { Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)); } -/** - * @param {string} policyID - */ -function dismissError(policyID) { +function dismissError(policyID: string) { Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); Policy.removeWorkspace(policyID); } -function WorkspaceInitialPage(props) { +function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reportsProp, policyMembers, reimbursementAccount}: WorkspaceInitialPageProps) { const styles = useThemeStyles(); - const policy = props.policyDraft && props.policyDraft.id ? props.policyDraft : props.policy; + const policy = policyDraft?.id ? policyDraft : policyProp; const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); - const hasPolicyCreationError = Boolean(policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); + const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const policyID = useMemo(() => policy.id, [policy]); + const policyID = useMemo(() => policy?.id, [policy]); const [policyReports, adminsRoom, announceRoom] = useMemo(() => { - const reports = []; - let admins; - let announce; - _.each(props.reports, (report) => { + const reports: OnyxTypes.Report[] = []; + let admins: OnyxTypes.Report | undefined; + let announce: OnyxTypes.Report | undefined; + + Object.values(reportsProp ?? {}).forEach((report) => { if (!report || report.policyID !== policyID) { return; } @@ -104,101 +105,97 @@ function WorkspaceInitialPage(props) { announce = report; } }); + return [reports, admins, announce]; - }, [policyID, props.reports]); + }, [policyID, reportsProp]); - /** - * Call the delete policy and hide the modal - */ + /** Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, policyReports, policy.name); + Policy.deleteWorkspace(policyID ?? '', policyReports, policy?.name ?? ''); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); - }, [policyID, policy.name, policyReports]); + }, [policyID, policy?.name, policyReports]); useEffect(() => { - const policyDraftId = lodashGet(props.policyDraft, 'id', null); + const policyDraftId = policyDraft?.id; + if (!policyDraftId) { return; } - App.savePolicyDraftByNewWorkspace(props.policyDraft.id, props.policyDraft.name, '', props.policyDraft.makeMeAdmin); + App.savePolicyDraftByNewWorkspace(policyDraft.id, policyDraft.name, '', policyDraft.makeMeAdmin); // We only care when the component renders the first time // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (!isCurrencyModalOpen || policy.outputCurrency !== CONST.CURRENCY.USD) { + if (!isCurrencyModalOpen || policy?.outputCurrency !== CONST.CURRENCY.USD) { return; } setIsCurrencyModalOpen(false); - }, [policy.outputCurrency, isCurrencyModalOpen]); + }, [policy?.outputCurrency, isCurrencyModalOpen]); - /** - * Call update workspace currency and hide the modal - */ + /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID, policy.name, CONST.CURRENCY.USD); + Policy.updateGeneralSettings(policyID ?? '', policy?.name ?? '', CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); - ReimbursementAccount.navigateToBankAccountRoute(policyID); - }, [policyID, policy.name]); + ReimbursementAccount.navigateToBankAccountRoute(policyID ?? ''); + }, [policyID, policy?.name]); - const policyName = lodashGet(policy, 'name', ''); - const hasMembersError = PolicyUtils.hasPolicyMemberError(props.policyMembers); - const hasGeneralSettingsError = !_.isEmpty(lodashGet(policy, 'errorFields.generalSettings', {})) || !_.isEmpty(lodashGet(policy, 'errorFields.avatar', {})); - const hasCustomUnitsError = PolicyUtils.hasCustomUnitsError(policy); - const menuItems = [ + const policyName = policy?.name ?? ''; + const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); + const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); + const menuItems: WorkspaceMenuItems[] = [ { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)))), - brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy?.id ?? '')))), + brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id)))), - error: hasCustomUnitsError, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id)))), - brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy?.id ?? '')))), + brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, action: () => - policy.outputCurrency === CONST.CURRENCY.USD + policy?.outputCurrency === CONST.CURRENCY.USD ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRouteWithoutParams())))() : setIsCurrencyModalOpen(true), - brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + brickRoadIndicator: !isEmptyObject(reimbursementAccount?.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, ]; - const threeDotsMenuItems = useMemo(() => { + const threeDotsMenuItems: ThreeDotsMenuItem[] = useMemo(() => { const items = [ { icon: Expensicons.Trashcan, @@ -227,7 +224,7 @@ function WorkspaceInitialPage(props) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = - _.isEmpty(policy) || + isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); @@ -241,7 +238,7 @@ function WorkspaceInitialPage(props) { Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={shouldShowNotFoundPage} - subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > dismissError(policy.id)} - errors={policy.errors} + pendingAction={policy?.pendingAction} + onClose={() => dismissError(policy?.id ?? '')} + errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} > @@ -269,14 +266,14 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy?.id ?? '')))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ROLE.BUTTON} > - {!_.isEmpty(policy.name) && ( + {!!policy?.name && ( ( + {menuItems.map((item) => ( ({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, @@ -364,4 +357,5 @@ export default compose( key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, }), + withPolicyAndFullscreenLoading, )(WorkspaceInitialPage); diff --git a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx index 892facb92823..d3375bb948a4 100644 --- a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx +++ b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx @@ -63,3 +63,4 @@ export default function withPolicyAndFullscreenLoading Date: Sat, 20 Jan 2024 14:12:24 +0100 Subject: [PATCH 060/418] Show the "Pending route..." as merchant for requests with manual amount --- src/components/DistanceEReceipt.js | 4 +--- .../ReportActionItem/MoneyRequestPreview.js | 7 ++++-- .../ReportActionItem/MoneyRequestView.js | 3 +-- .../ReportActionItem/ReportPreview.js | 24 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 3f337281c548..c507e713b546 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -75,9 +75,7 @@ function DistanceEReceipt({transaction}) { )} - {!hasPendingRoute && ( - {formattedTransactionAmount} - )} + {!hasPendingRoute && {formattedTransactionAmount}} {transactionMerchant} diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index fd182b1d96ae..5f555e6993d4 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -169,7 +169,10 @@ function MoneyRequestPreview(props) { // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan const shouldShowMerchant = - !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && !hasPendingRoute; + !_.isEmpty(requestMerchant) && + requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && + requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && + !(hasPendingRoute && !requestAmount); const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; @@ -227,7 +230,7 @@ function MoneyRequestPreview(props) { return translate('iou.receiptScanning'); } - if (hasPendingRoute) { + if (hasPendingRoute && !requestAmount) { return translate('iou.routePending'); } diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 7703940ea13f..5a84a5e6de21 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -140,8 +140,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); - const formattedTransactionAmount = transactionAmount && !hasPendingRoute ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; + const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 2e5c73b04f54..1f9755c2a3e8 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -174,18 +174,16 @@ function ReportPreview(props) { const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - let formattedMerchant = numberOfRequests === 1 && hasReceipts && !hasOnlyPendingDistanceRequests ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant)) { formattedMerchant = null; } const previewSubtitle = - numberOfRequests === 1 && hasOnlyPendingDistanceRequests - ? '' - : formattedMerchant || - props.translate('iou.requestCount', { - count: numberOfRequests - numberOfScanningReceipts, - scanningReceipts: numberOfScanningReceipts, - }); + formattedMerchant || + props.translate('iou.requestCount', { + count: numberOfRequests - numberOfScanningReceipts, + scanningReceipts: numberOfScanningReceipts, + }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; @@ -196,15 +194,15 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasOnlyPendingDistanceRequests) { - return props.translate('iou.routePending'); - } if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); } if (isScanning) { return props.translate('iou.receiptScanning'); } + if (hasOnlyPendingDistanceRequests) { + return props.translate('iou.routePending'); + } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; @@ -253,6 +251,8 @@ function ReportPreview(props) { return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSubtitle = + !isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant && !(hasOnlyPendingDistanceRequests && !totalDisplaySpend))); return ( @@ -301,7 +301,7 @@ function ReportPreview(props) { )} - {!isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant)) && ( + {shouldShowSubtitle && ( {previewSubtitle || moneyRequestComment} From 373c670b35dd8d332503c042c267bf9d4dc06c2c Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 15:53:13 +0100 Subject: [PATCH 061/418] Update hasPendingRoute logic --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/libs/TransactionUtils.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 97f97877aa10..f8484373cad8 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -172,7 +172,7 @@ function MoneyRequestPreview(props) { !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && - !(hasPendingRoute && !requestAmount); + !(isDistanceRequest && hasPendingRoute && !requestAmount); const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f6b60b35bce8..f4b3b464d9d1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -316,14 +316,10 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server + * Verify if the transaction is expecting the distance to be calculated on the server */ function hasPendingRoute(transaction: OnyxEntry): boolean { - if (!transaction) { - return false; - } - - return isDistanceRequest(transaction) && !!transaction.pendingFields?.waypoints; + return !!transaction?.pendingFields?.waypoints; } /** From fc7458999c2342c8e4f39798f0de18ab339c5e18 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 20 Jan 2024 21:53:55 +0530 Subject: [PATCH 062/418] Add formatted amount to pay elsewhere --- src/components/SettlementButton.js | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 0c8e193af4cc..e8a7597daa4c 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -160,7 +160,7 @@ function SettlementButton({ value: CONST.IOU.PAYMENT_TYPE.VBBA, }, [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: translate('iou.payElsewhere'), + text: translate('iou.payElsewhere', {formattedAmount}), icon: Expensicons.Cash, value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..f3ebd36f0d40 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -593,7 +593,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), - payElsewhere: 'Pay elsewhere', + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), nextStep: 'Next Steps', finished: 'Finished', requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..3e7887d3221c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -586,7 +586,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), - payElsewhere: 'Pagar de otra forma', + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), nextStep: 'Pasos Siguientes', finished: 'Finalizado', requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, From 634b5fa8f1b1f1dd7c1122b1ad9e391f2d4174e6 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 21:17:59 +0100 Subject: [PATCH 063/418] Update "Route pending" display based on amount --- src/components/DistanceEReceipt.js | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index c507e713b546..55ac6d8d6979 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -75,7 +75,7 @@ function DistanceEReceipt({transaction}) { )} - {!hasPendingRoute && {formattedTransactionAmount}} + {!!transactionAmount && {formattedTransactionAmount}} {transactionMerchant} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2a7fd4c74338..e88a62953b3e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2225,7 +2225,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.receiptScanning'); } - if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction)) { + if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction) && !TransactionUtils.getAmount(linkedTransaction)) { return Localize.translateLocal('iou.routePending'); } } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f4b3b464d9d1..103f66d7a683 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -266,8 +266,8 @@ function getDescription(transaction: OnyxEntry): string { /** * Return the amount field from the transaction, return the modifiedAmount if present. */ -function getAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { - // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value +function getAmount(transaction: OnyxEntry, isFromExpenseReport = false): number { + // IOU requests cannot have negative values, but they can be stored as negative values, let's return absolute value if (!isFromExpenseReport) { const amount = transaction?.modifiedAmount ?? 0; if (amount) { From f2a7a06d85a3cfff8e11bbcf8e4b30c205f95ef7 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 21:46:18 +0100 Subject: [PATCH 064/418] Revert some changes --- src/libs/ReceiptUtils.ts | 26 ++++++++++++-------------- src/types/onyx/Transaction.ts | 2 -- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 9e985f0bd60e..bcba68a3a0bd 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,7 +8,6 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; -import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -30,28 +29,27 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (TransactionUtils.hasPendingRoute(transaction)) { - return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; - } - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); + const hasEReceipt = transaction?.hasEReceipt; - if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; - } + if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } - // For local files, we won't have a thumbnail yet - if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true}; - } + // For local files, we won't have a thumbnail yet + if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { + return {thumbnail: null, image: path, isLocalFile: true}; + } - if (isReceiptImage) { - return {thumbnail: `${path}.1024.jpg`, image: path}; + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; + } } const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 4b8a0a1cf9b3..8b7e26280305 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -94,8 +94,6 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; - - isLoading?: boolean; }; export default Transaction; From 98ec065bba2d026e96fb8242cc607279b42905c9 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sun, 21 Jan 2024 21:47:33 +0100 Subject: [PATCH 065/418] Update IOU Report total on waypoints change --- src/libs/actions/IOU.js | 85 +++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 1a9030c89cc4..e5e45dd5de32 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -954,7 +954,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.parentReportID}`]; const isFromExpenseReport = ReportUtils.isExpenseReport(iouReport); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const updatedTransaction = TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport); + let updatedTransaction = TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport); const transactionDetails = ReportUtils.getTransactionDetails(updatedTransaction); // This needs to be a JSON string since we're sending this to the MapBox API @@ -968,13 +968,22 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t transactionID, }; + const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + if (hasPendingWaypoints) { + updatedTransaction = { + ...updatedTransaction, + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }; + } + // Step 3: Build the modified expense report actions // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); if (!hasPendingWaypoints) { - const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); params.reportActionID = updatedReportAction.reportActionID; optimisticData.push({ @@ -1001,31 +1010,31 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }, }); + } - // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. - // Should only update if the transaction matches the currency of the report, else we wait for the update - // from the server with the currency conversion - let updatedMoneyRequestReport = {...iouReport}; - if (updatedTransaction.currency === iouReport.currency && updatedTransaction.modifiedAmount) { - const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); - if (ReportUtils.isExpenseReport(iouReport)) { - updatedMoneyRequestReport.total += diff; - } else { - updatedMoneyRequestReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID, diff, TransactionUtils.getCurrency(transaction), false); - } - - updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: updatedMoneyRequestReport, - }); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: {pendingAction: null}, - }); + // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. + // Should only update if the transaction matches the currency of the report, else we wait for the update + // from the server with the currency conversion + let updatedMoneyRequestReport = {...iouReport}; + if (updatedTransaction.currency === iouReport.currency && (updatedTransaction.modifiedAmount || hasPendingWaypoints)) { + const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); + if (ReportUtils.isExpenseReport(iouReport)) { + updatedMoneyRequestReport.total += diff; + } else { + updatedMoneyRequestReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID, diff, TransactionUtils.getCurrency(transaction), false); } + + updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: updatedMoneyRequestReport, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: {pendingAction: null}, + }); } // Optimistically modify the transaction @@ -1100,29 +1109,6 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }); if (hasPendingWaypoints) { - // When updating waypoints, we need to explicitly set the transaction's amount and IOU report's total to 0. - // These values must be calculated on the server because they depend on the distance. - optimisticData.push( - ...[ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - total: CONST.IOU.DEFAULT_AMOUNT, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - amount: CONST.IOU.DEFAULT_AMOUNT, - modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, - modifiedMerchant: Localize.translateLocal('iou.routePending'), - }, - }, - ], - ); - // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors successData.push({ onyxMethod: Onyx.METHOD.SET, @@ -1138,6 +1124,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: { amount: transaction.amount, modifiedAmount: transaction.modifiedAmount, + modifiedMerchant: transaction.modifiedMerchant, }, }); } From 9e7c34e219424158674b06084d7ea4eaa2d4198e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 22 Jan 2024 16:28:00 +0100 Subject: [PATCH 066/418] Code improvements --- src/pages/workspace/WorkspaceInitialPage.tsx | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 9839c13125ea..5e230869c4a9 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -22,7 +22,6 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -41,7 +40,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -type WorkspaceMenuItems = { +type WorkspaceMenuItem = { translationKey: TranslationPaths; icon: IconAsset; action: () => void; @@ -79,7 +78,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const policyID = useMemo(() => policy?.id, [policy]); + const policyID = useMemo(() => policy?.id ?? '', [policy]); const [policyReports, adminsRoom, announceRoom] = useMemo(() => { const reports: OnyxTypes.Report[] = []; let admins: OnyxTypes.Report | undefined; @@ -111,7 +110,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID ?? '', policyReports, policy?.name ?? ''); + Policy.deleteWorkspace(policyID, policyReports, policy?.name ?? ''); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); @@ -138,50 +137,50 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID ?? '', policy?.name ?? '', CONST.CURRENCY.USD); + Policy.updateGeneralSettings(policyID, policy?.name ?? '', CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); - ReimbursementAccount.navigateToBankAccountRoute(policyID ?? ''); + ReimbursementAccount.navigateToBankAccountRoute(policyID); }, [policyID, policy?.name]); const policyName = policy?.name ?? ''; const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); - const menuItems: WorkspaceMenuItems[] = [ + const menuItems: WorkspaceMenuItem[] = [ { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policyID)))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policyID)))), }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policyID)))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policyID)))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { @@ -255,7 +254,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports dismissError(policy?.id ?? '')} + onClose={() => dismissError(policyID)} errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} > @@ -266,14 +265,16 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports openEditor(policy?.id ?? '')))} + onPress={singleExecution(waitForNavigate(() => openEditor(policyID)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ROLE.BUTTON} > ({ reports: { key: ONYXKEYS.COLLECTION.REPORT, @@ -356,6 +357,5 @@ export default compose( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, - }), - withPolicyAndFullscreenLoading, -)(WorkspaceInitialPage); + })(WorkspaceInitialPage), +); From 127b8a7a3fefa712dedd13ed2ab04987d9683567 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 22 Jan 2024 22:16:11 +0100 Subject: [PATCH 067/418] add a warning comment --- .github/workflows/preDeploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 8f9512062e9d..f09865de0194 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -1,3 +1,4 @@ +# Reminder: If this workflow's name changes, update the name in the dependent workflow at .github/workflows/failureNotifier.yml. name: Process new code merged to main on: From e834c5fe137c16e44f5bf1d23d4e90bbea614591 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 11:22:06 +0700 Subject: [PATCH 068/418] reapply changes --- src/pages/SearchPage/index.js | 2 +- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- src/pages/tasks/TaskShareDestinationSelectorModal.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 211f3622e06c..8e6cacc2073a 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -50,7 +50,7 @@ function SearchPage({betas, reports}) { const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 0949081435c4..171f3c421171 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -83,7 +83,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 9567b17ecdf5..6257c2065592 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -88,7 +88,7 @@ function MoneyRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index 64fd5f50b61f..fc0f4880efd9 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -145,7 +145,7 @@ function TaskShareDestinationSelectorModal(props) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd} textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={isOffline ? `${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} ref={inputCallbackRef} From ed0cce8af3ebdca68b3d1d2ce2a7df1c373f17d0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 11:30:22 +0700 Subject: [PATCH 069/418] fix lint --- src/pages/tasks/TaskShareDestinationSelectorModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index fc0f4880efd9..b8d9229e6158 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -145,7 +145,7 @@ function TaskShareDestinationSelectorModal(props) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd} textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} + textInputAlert={isOffline ? [`${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} ref={inputCallbackRef} From 9dde0a3e9a42374c4cfd661ba5f12633db2b8415 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:26:31 +0100 Subject: [PATCH 070/418] Update issueTitle Co-authored-by: Joel Davies --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 667fe3a737b2..2377cd71d4d8 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -58,7 +58,7 @@ jobs: }); const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); if (!existingIssue) { - const issueTitle = `🔍 Investigation Needed: ${ jobName } Failure due to PR Merge 🔍`; + const issueTitle = `Investigate workflow job failing on main: ${ jobName }`; const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + `- **🔧 Failure in Workflow**: Main branch\n` + From 92449cabf9dbe28c0f9bbe9184972f6128cc2aaf Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:27:04 +0100 Subject: [PATCH 071/418] spacing Co-authored-by: Joel Davies --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 2377cd71d4d8..96718a16c29f 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -31,7 +31,7 @@ jobs: uses: actions/github-script@v7 with: script: | - const jobs = ${{steps.fetch-workflow-jobs.outputs.result}}; + const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ From 6aef1a7916c445a500df43ac891f5f8e2cbf4eee Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:28:31 +0100 Subject: [PATCH 072/418] Update issueBody --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 96718a16c29f..476f27737d6f 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -61,7 +61,7 @@ jobs: const issueTitle = `Investigate workflow job failing on main: ${ jobName }`; const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + - `- **🔧 Failure in Workflow**: Main branch\n` + + `- **🔧 Failure in Workflow**: Process new code merged to main\n` + `- **🔗 Triggered by PR**: [PR Link](${ prLink })\n` + `- **👤 PR Author**: @${ prAuthor }\n` + `- **🤝 Merged by**: @${ prMerger }\n\n` + From 5888b46da4ec069cdd01e71e59d8d461490843db Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Tue, 23 Jan 2024 14:58:23 +0400 Subject: [PATCH 073/418] fix login background image moving down --- .../SignInPageLayout/BackgroundImage/index.ios.js | 4 +++- src/pages/signin/SignInPageLayout/index.js | 1 + src/styles/index.ts | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index da6a6b9ee4fb..646234469611 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -6,6 +6,7 @@ import MobileBackgroundImage from '@assets/images/home-background--mobile-new.sv import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import defaultPropTypes from './propTypes'; +import useWindowDimensions from "@hooks/useWindowDimensions"; const defaultProps = { isSmallScreen: false, @@ -20,12 +21,13 @@ const propTypes = { function BackgroundImage(props) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {windowHeight} = useWindowDimensions() const src = useMemo(() => (props.isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [props.isSmallScreen]); return ( ); } diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index e2f9c28f9fcd..823a08ba6b7d 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -162,6 +162,7 @@ function SignInPageLayout(props) { ref={scrollViewRef} > + signInBackground: { position: 'absolute', - bottom: 0, left: 0, minHeight: 700, }, + signInBackgroundFillView: { + position: 'absolute', + left: 0, + bottom: 0, + height: '50%', + width: '100%', + backgroundColor: theme.signInPage + }, + signInPageInner: { marginLeft: 'auto', marginRight: 'auto', From c6ed187f899e58c632c89ce60633bd9d5f27ed17 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 23 Jan 2024 12:37:34 +0100 Subject: [PATCH 074/418] Use TransactionUtils.hasPendingRoute to check pending route for receipt --- src/libs/ReceiptUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 7fca9f54b744..9e985f0bd60e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; +import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -29,7 +30,7 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + if (TransactionUtils.hasPendingRoute(transaction)) { return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; } From 03a41e373463a19c5981a52b74305200e08bdfd2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 18:41:30 +0700 Subject: [PATCH 075/418] fix remaining cases --- src/components/DotIndicatorMessage.tsx | 8 ++++---- src/components/TextInput/BaseTextInput/types.ts | 2 +- src/libs/ErrorUtils.ts | 12 ++++++------ src/pages/iou/steps/MoneyRequestAmountForm.js | 6 +++--- .../Profile/CustomStatus/StatusClearAfterPage.js | 12 ++++-------- src/pages/settings/Wallet/TransferBalancePage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 2 +- src/pages/signin/UnlinkLoginForm.js | 2 +- src/pages/workspace/WorkspaceInvitePage.js | 3 +-- src/stories/Form.stories.js | 2 +- 10 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index d18704fdfb05..113a262a6f97 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -23,7 +23,7 @@ type DotIndicatorMessageProps = { * timestamp: 'message', * } */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; @@ -36,8 +36,8 @@ type DotIndicatorMessageProps = { }; /** Check if the error includes a receipt. */ -function isReceiptError(message: string | ReceiptError): message is ReceiptError { - if (typeof message === 'string') { +function isReceiptError(message: Localize.MaybePhraseKey | ReceiptError): message is ReceiptError { + if (typeof message === 'string' || Array.isArray(message)) { return false; } return (message?.error ?? '') === CONST.IOU.RECEIPT_ERROR; @@ -58,7 +58,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica .map((key) => messages[key]); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages = [...new Set(sortedMessages)].map((message) => Localize.translateIfPhraseKey(message)); + const uniqueMessages = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); const isErrorMessage = type === 'error'; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 21875d4dcc64..9931f8ec90d4 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -65,7 +65,7 @@ type CustomBaseTextInputProps = { hideFocusedState?: boolean; /** Hint text to display below the TextInput */ - hint?: string; + hint?: MaybePhraseKey; /** Prefix character */ prefixCharacter?: string; diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index aa74d84e7b65..208b824ef872 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -52,7 +52,7 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: T } const key = Object.keys(errors).sort().reverse()[0]; - return getErrorWithTranslationData(errors[key]); + return getErrorMessageWithTranslationData(errors[key]); } type OnyxDataWithErrorFields = { @@ -83,7 +83,7 @@ function getLatestErrorField(onyxData } const key = Object.keys(errorsForField).sort().reverse()[0]; - return {[key]: getErrorWithTranslationData(errorsForField[key])}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { @@ -94,7 +94,7 @@ function getEarliestErrorField(onyxDa } const key = Object.keys(errorsForField).sort()[0]; - return {[key]: getErrorWithTranslationData(errorsForField[key])}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } type ErrorsList = Record; @@ -111,10 +111,10 @@ function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsLi if (typeof errors === 'string' || Array.isArray(errors)) { // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': getErrorWithTranslationData(errors)}; + return {'0': getErrorMessageWithTranslationData(errors)}; } - return mapValues(errors, getErrorWithTranslationData); + return mapValues(errors, getErrorMessageWithTranslationData); } /** diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 536944f4a2d8..de9b9708eb3e 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -228,12 +228,12 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward */ const submitAndNavigateToNextPage = useCallback(() => { if (isAmountInvalid(currentAmount)) { - setFormError(translate('iou.error.invalidAmount')); + setFormError('iou.error.invalidAmount'); return; } if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { - setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); + setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); return; } @@ -243,7 +243,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward initializeAmount(backendAmount); onSubmitButtonPress({amount: currentAmount, currency}); - }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, translate, initializeAmount]); + }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, initializeAmount]); /** * Input handler to check for a forward-delete key (or keyboard shortcut) press. diff --git a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js index 84ca74c2842f..61208447495d 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js @@ -54,21 +54,17 @@ function getSelectedStatusType(data) { } const useValidateCustomDate = (data) => { - const {translate} = useLocalize(); const [customDateError, setCustomDateError] = useState(''); const [customTimeError, setCustomTimeError] = useState(''); const validate = () => { const {dateValidationErrorKey, timeValidationErrorKey} = ValidationUtils.validateDateTimeIsAtLeastOneMinuteInFuture(data); - const dateError = dateValidationErrorKey ? translate(dateValidationErrorKey) : ''; - setCustomDateError(dateError); - - const timeError = timeValidationErrorKey ? translate(timeValidationErrorKey) : ''; - setCustomTimeError(timeError); + setCustomDateError(dateValidationErrorKey); + setCustomTimeError(timeValidationErrorKey); return { - dateError, - timeError, + dateValidationErrorKey, + timeValidationErrorKey, }; }; diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index 44c9bde8cd3c..3dfb1f059933 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -167,7 +167,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorsWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; + const errorMessage = ErrorUtils.getLatestErrorMessage(props.walletTransfer); const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 40b8b7d97bd1..e9ec74e506e2 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -297,7 +297,7 @@ function LoginForm(props) { )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 1d278760f13c..52eb710e2ea5 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -68,7 +68,7 @@ function UnlinkLoginForm(props) { )} {!_.isEmpty(props.account.errors) && ( diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 6b8a77f739af..e453ea632863 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -15,7 +15,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as ErrorUtils from '@libs/ErrorUtils'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -306,7 +305,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={ErrorUtils.getErrorsWithTranslationData(props.policy.alertMessage)} + message={[props.policy.alertMessage, {isTranslated: true}]} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 6a4274c87eda..8a152d040a1f 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -68,7 +68,7 @@ function Template(args) { label="Street" inputID="street" containerStyles={[defaultStyles.mt4]} - hint="No PO box" + hint="common.noPO" /> Date: Tue, 23 Jan 2024 15:16:09 +0100 Subject: [PATCH 076/418] Update issue label Co-authored-by: Joel Davies --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 476f27737d6f..c8901d499982 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -77,7 +77,7 @@ jobs: repo: context.repo.repo, title: issueTitle, body: issueBody, - labels: [failureLabel, 'daily'], + labels: [failureLabel, 'Daily'], assignees: [prMerger, prAuthor] }); } From 2aa59017fb9e43831aaf8b2f91ba2c20c2ece9a1 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:28:20 +0100 Subject: [PATCH 077/418] update workflow failure label name --- .github/workflows/failureNotifier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index c8901d499982..79de3d3f5c7d 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -45,7 +45,7 @@ jobs: const prAuthor = pr.user.login; const prMerger = "${{ github.event.workflow_run.actor.login }}"; - const failureLabel = 'workflow-failure'; + const failureLabel = 'Workflow Failure'; for (let i = 0; i < jobs.total_count; i++) { if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; From 3ad76b18a649ae5d13373b71b125cd2048725020 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:33:31 +0100 Subject: [PATCH 078/418] add error message to the issue body --- .github/workflows/failureNotifier.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 79de3d3f5c7d..604770eff4d7 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -58,13 +58,24 @@ jobs: }); const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); if (!existingIssue) { + const annotations = await github.rest.checks.listAnnotations({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: jobs.jobs[i].id, + }); + let errorMessage = ""; + for(let j = 0; j < annotations.data.length; j++) { + errorMessage += annotations.data[j].annotation_level + ": "; + errorMessage += annotations.data[j].message + "\n"; + } const issueTitle = `Investigate workflow job failing on main: ${ jobName }`; const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + `- **🔧 Failure in Workflow**: Process new code merged to main\n` + `- **🔗 Triggered by PR**: [PR Link](${ prLink })\n` + `- **👤 PR Author**: @${ prAuthor }\n` + - `- **🤝 Merged by**: @${ prMerger }\n\n` + + `- **🤝 Merged by**: @${ prMerger }\n` + + `- **🐛 Error Message**: \n ${errorMessage}\n\n` + `⚠️ **Action Required** ⚠️:\n\n` + `🛠️ A recent merge appears to have caused a failure in the job named [${ jobName }](${ jobLink }).\n` + `This issue has been automatically created and labeled with \`${ failureLabel }\` for investigation. \n\n` + From c1ccade52293b555868c34f7c23350a0448f208e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 24 Jan 2024 00:52:37 +0100 Subject: [PATCH 079/418] add olddot-wireframe to Expensicons --- assets/images/olddot-wireframe.svg | 3422 ++++++++++++++++++++++++++++ src/components/Icon/Expensicons.ts | 4 +- 2 files changed, 3424 insertions(+), 2 deletions(-) create mode 100644 assets/images/olddot-wireframe.svg diff --git a/assets/images/olddot-wireframe.svg b/assets/images/olddot-wireframe.svg new file mode 100644 index 000000000000..ee9aa93be255 --- /dev/null +++ b/assets/images/olddot-wireframe.svg @@ -0,0 +1,3422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 364fb03a2055..33bfa1855e74 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -93,6 +93,7 @@ import NewWindow from '@assets/images/new-window.svg'; import NewWorkspace from '@assets/images/new-workspace.svg'; import OfflineCloud from '@assets/images/offline-cloud.svg'; import Offline from '@assets/images/offline.svg'; +import OldDotWireframe from '@assets/images/olddot-wireframe.svg'; import Paperclip from '@assets/images/paperclip.svg'; import Paycheck from '@assets/images/paycheck.svg'; import Pencil from '@assets/images/pencil.svg'; @@ -106,7 +107,6 @@ import QuestionMark from '@assets/images/question-mark-circle.svg'; import ReceiptSearch from '@assets/images/receipt-search.svg'; import Receipt from '@assets/images/receipt.svg'; import Rotate from '@assets/images/rotate-image.svg'; -import RotateLeft from '@assets/images/rotate-left.svg'; import Scan from '@assets/images/scan.svg'; import Send from '@assets/images/send.svg'; import Shield from '@assets/images/shield.svg'; @@ -230,6 +230,7 @@ export { NewWorkspace, Offline, OfflineCloud, + OldDotWireframe, Paperclip, Paycheck, Pencil, @@ -243,7 +244,6 @@ export { Receipt, ReceiptSearch, Rotate, - RotateLeft, Scan, Send, Shield, From 4f29dce373fae4b93ab512dbb698e00855dde428 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Wed, 24 Jan 2024 16:56:03 +0100 Subject: [PATCH 080/418] add ManageTeamsExpensesModal --- src/components/ManageTeamsExpensesModal.tsx | 114 ++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/components/ManageTeamsExpensesModal.tsx diff --git a/src/components/ManageTeamsExpensesModal.tsx b/src/components/ManageTeamsExpensesModal.tsx new file mode 100644 index 000000000000..81dd25511060 --- /dev/null +++ b/src/components/ManageTeamsExpensesModal.tsx @@ -0,0 +1,114 @@ +import {useNavigation} from '@react-navigation/native'; +import React, {useCallback, useMemo, useState} from 'react'; +import {ScrollView, View} from 'react-native'; +import Button from '@components/Button'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Expensicons from './Icon/Expensicons'; +import type {MenuItemProps} from './MenuItem'; +import MenuItemList from './MenuItemList'; +import Modal from './Modal'; +import Text from './Text'; + +const TEAMS_EXPENSE_CHOICE = { + MULTI_LEVEL: 'Multi level approval', + CUSTOM_EXPENSE: 'Custom expense coding', + CARD_TRACKING: 'Company Card Tracking', + ACCOUNTING: 'Accounting integrations', + RULE: 'Rule enforcement', +}; + +const menuIcons = { + [TEAMS_EXPENSE_CHOICE.MULTI_LEVEL]: Expensicons.Task, + [TEAMS_EXPENSE_CHOICE.CUSTOM_EXPENSE]: Expensicons.ReceiptSearch, + [TEAMS_EXPENSE_CHOICE.CARD_TRACKING]: Expensicons.CreditCard, + [TEAMS_EXPENSE_CHOICE.ACCOUNTING]: Expensicons.Sync, + [TEAMS_EXPENSE_CHOICE.RULE]: Expensicons.Gear, +}; + +function ManageTeamsExpensesModal() { + const styles = useThemeStyles(); + const {isSmallScreenWidth, isExtraSmallScreenHeight} = useWindowDimensions(); + const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); + const [isModalOpen, setIsModalOpen] = useState(true); + const theme = useTheme(); + + const closeModal = useCallback(() => { + Report.dismissEngagementModal(); + setIsModalOpen(false); + }, []); + + const menuItems: MenuItemProps[] = useMemo( + () => + Object.values(TEAMS_EXPENSE_CHOICE).map((choice) => { + const translationKey = `${choice}` as const; + return { + key: translationKey, + title: translationKey, + icon: menuIcons[choice], + numberOfLinesTitle: 2, + interactive: false, + }; + }), + [], + ); + + return ( + + + + + + + + Do you require any of the following features + + + + + + + +