diff --git a/android/app/build.gradle b/android/app/build.gradle index fac54600f021..ea7e6bdfce45 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000200 - versionName "9.0.2-0" + versionCode 1009000203 + versionName "9.0.2-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md index 6bc3b0896912..155512866a8f 100644 --- a/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md +++ b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md @@ -46,6 +46,12 @@ Log in to QuickBooks Online and ensure all of your employees are setup as either
  • Enter your Intuit login details to import your settings from QuickBooks Online to Expensify.
  • +![The toggle location to enable accounting integrations like QuickBooks Online]({{site.url}}/assets/images/ExpensifyHelp-QBO-1.png){:width="100%"} + +![How to enable accounting integrations like QuickBooks Online]({{site.url}}/assets/images/ExpensifyHelp-QBO-2.png){:width="100%"} + +![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-3.png){:width="100%"} + # Step 3: Configure import settings The following steps help you determine how data will be imported from QuickBooks Online to Expensify. diff --git a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md index 73bff6ad5862..47917f2dffc3 100644 --- a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md +++ b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md @@ -23,6 +23,12 @@ To set up your Xero connection, complete the 4 steps below.
  • Enter your Xero login details to import your settings from Xero to Expensify.
  • +![The toggle location to enable accounting integrations like QuickBooks Online]({{site.url}}/assets/images/ExpensifyHelp-Xero-1.png){:width="100%"} + +![How to enable accounting integrations like QuickBooks Online]({{site.url}}/assets/images/ExpensifyHelp-Xero-2.png){:width="100%"} + +![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-Xero-3.png){:width="100%"} + # Step 2: Configure import settings The following steps help you determine how data will be imported from Xero to Expensify. diff --git a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md index 0cf642c76e4c..c037e8fe9cd3 100644 --- a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md +++ b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md @@ -29,7 +29,11 @@ To approve an expense, {% include info.html %} Admins can modify an expense, if needed. {% include end-info.html %} - + +![The approve button in an expense]({{site.url}}/assets/images/ExpensifyHelp_ApproveExpense_1.png){:width="100%"} + +![The approve button when you click into the expense]({{site.url}}/assets/images/ExpensifyHelp_ApproveExpense_2.png){:width="100%"} + You’re now ready to pay the expense. # Hold an expense diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a2e274eafc4d..1653edce72b6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.2.0 + 9.0.2.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e1f9960caa92..8341a5d96c13 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.2.0 + 9.0.2.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7d83d9f3d273..d9fcba7e3c9d 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.2 CFBundleVersion - 9.0.2.0 + 9.0.2.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index fb15d51d1389..93ca7e345a53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.2-0", + "version": "9.0.2-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.2-0", + "version": "9.0.2-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d4be691e2fc2..fecf0742629e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.2-0", + "version": "9.0.2-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -36,7 +36,7 @@ "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "typecheck": "tsc", - "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\" \"*.ts\" \"*.tsx\")", "lint-watch": "npx eslint-watch --watch --changed", "shellcheck": "./scripts/shellCheck.sh", diff --git a/src/CONST.ts b/src/CONST.ts index e71ad55a452c..0297f7bc0d5a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -601,6 +601,7 @@ const CONST = { ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', LIST_OF_RESTRICTED_BUSINESSES: 'https://community.expensify.com/discussion/6191/list-of-restricted-businesses', TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, + PRICING: `https://www.expensify.com/pricing`, // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 46d8be0f7e82..2cb615ae0af8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -339,6 +339,9 @@ const ONYXKEYS = { // Paths of PDF file that has been cached during one session CACHED_PDF_PATHS: 'cachedPDFPaths', + /** Stores iframe link to verify 3DS flow for subscription */ + VERIFY_3DS_SUBSCRIPTION: 'verify3dsSubscription', + /** Holds the checks used while transferring the ownership of the workspace */ POLICY_OWNERSHIP_CHANGE_CHECKS: 'policyOwnershipChangeChecks', @@ -405,8 +408,8 @@ const ONYXKEYS = { /** List of Form ids */ FORMS: { - ADD_DEBIT_CARD_FORM: 'addDebitCardForm', - ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft', + ADD_PAYMENT_CARD_FORM: 'addPaymentCardForm', + ADD_PAYMENT_CARD_FORM_DRAFT: 'addPaymentCardFormDraft', WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', @@ -475,6 +478,8 @@ const ONYXKEYS = { SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusSetClearAfterFormDraft', SETTINGS_STATUS_CLEAR_DATE_FORM: 'settingsStatusClearDateForm', SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT: 'settingsStatusClearDateFormDraft', + CHANGE_BILLING_CURRENCY_FORM: 'changeBillingCurrencyForm', + CHANGE_BILLING_CURRENCY_FORM_DRAFT: 'changeBillingCurrencyFormDraft', PRIVATE_NOTES_FORM: 'privateNotesForm', PRIVATE_NOTES_FORM_DRAFT: 'privateNotesFormDraft', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', @@ -517,7 +522,7 @@ const ONYXKEYS = { type AllOnyxKeys = DeepValueOf; type OnyxFormValuesMapping = { - [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; @@ -549,6 +554,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.SettingsStatusClearDateForm; + [ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM]: FormTypes.ChangeBillingCurrencyForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.SettingsStatusSetClearAfterForm; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: FormTypes.PrivateNotesForm; [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: FormTypes.IKnowTeacherForm; @@ -704,6 +710,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_CHECKING_PUBLIC_ROOM]: boolean; [ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record; [ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID]: string; + [ONYXKEYS.VERIFY_3DS_SUBSCRIPTION]: string; [ONYXKEYS.PREFERRED_THEME]: ValueOf; [ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken; [ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c38ec192127e..5c8cfdcc8a68 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -95,6 +95,7 @@ const ROUTES = { WORKSPACE_SWITCHER: 'workspace-switcher', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', + SETTINGS_CHANGE_CURRENCY: 'settings/add-payment-card/change-currency', SETTINGS_SHARE_CODE: 'settings/shareCode', SETTINGS_DISPLAY_NAME: 'settings/profile/display-name', SETTINGS_TIMEZONE: 'settings/profile/timezone', @@ -107,6 +108,8 @@ const ROUTES = { getRoute: (canChangeSize: 0 | 1) => `settings/subscription/subscription-size?canChangeSize=${canChangeSize}` as const, }, SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', + SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency', + SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY: 'settings/subscription/add-payment-card/change-payment-currency', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 6e3d1f3276e9..5c5fc6c31092 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -41,6 +41,7 @@ const SCREENS = { SAVE_THE_WORLD: 'Settings_TeachersUnite', APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', + ADD_PAYMENT_CARD_CHANGE_CURRENCY: 'Settings_Add_Payment_Card_Change_Currency', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', CLOSE: 'Settings_Close', TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth', @@ -104,6 +105,8 @@ const SCREENS = { SIZE: 'Settings_Subscription_Size', ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', + CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency', + CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency', }, }, SAVE_THE_WORLD: { diff --git a/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx new file mode 100644 index 000000000000..39fea0f4df77 --- /dev/null +++ b/src/components/AddPaymentCard/PaymentCardChangeCurrencyForm.tsx @@ -0,0 +1,142 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm'; +import PaymentCardCurrencyHeader from './PaymentCardCurrencyHeader'; +import PaymentCardCurrencyModal from './PaymentCardCurrencyModal'; + +type PaymentCardFormProps = { + initialCurrency?: ValueOf; + isSecurityCodeRequired?: boolean; + changeBillingCurrency: (currency?: ValueOf, values?: FormOnyxValues) => void; +}; + +const REQUIRED_FIELDS = [INPUT_IDS.SECURITY_CODE]; + +function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeRequired, initialCurrency}: PaymentCardFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); + const [currency, setCurrency] = useState>(initialCurrency ?? CONST.CURRENCY.USD); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); + + if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) { + errors.securityCode = translate('addPaymentCardPage.error.securityCode'); + } + + return errors; + }; + + const {sections} = useMemo( + () => ({ + sections: [ + { + data: (Object.keys(CONST.CURRENCY) as Array>).map((currencyItem) => ({ + text: currencyItem, + value: currencyItem, + keyForList: currencyItem, + isSelected: currencyItem === currency, + })), + }, + ], + }), + [currency], + ); + + const showCurrenciesModal = useCallback(() => { + setIsCurrencyModalVisible(true); + }, []); + + const changeCurrency = useCallback((selectedCurrency: ValueOf) => { + setCurrency(selectedCurrency); + setIsCurrencyModalVisible(false); + }, []); + + const selectCurrency = useCallback( + (selectedCurrency: ValueOf) => { + setCurrency(selectedCurrency); + changeBillingCurrency(selectedCurrency); + }, + [changeBillingCurrency], + ); + + if (isSecurityCodeRequired) { + return ( + changeBillingCurrency(currency, values)} + submitButtonText={translate('common.save')} + scrollContextEnabled + style={[styles.mh5, styles.flexGrow1]} + > + + <> + + + + + + >} + currentCurrency={currency} + onCurrencyChange={changeCurrency} + onClose={() => setIsCurrencyModalVisible(false)} + /> + + ); + } + + return ( + + } + initiallyFocusedOptionKey={currency} + containerStyle={[styles.mhn5]} + sections={sections} + onSelectRow={(option) => { + selectCurrency(option.value); + }} + showScrollIndicator + shouldStopPropagation + shouldUseDynamicMaxToRenderPerBatch + ListItem={RadioListItem} + /> + + ); +} + +PaymentCardChangeCurrencyForm.displayName = 'PaymentCardChangeCurrencyForm'; + +export default PaymentCardChangeCurrencyForm; diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyHeader.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyHeader.tsx new file mode 100644 index 000000000000..e5142aec8efc --- /dev/null +++ b/src/components/AddPaymentCard/PaymentCardCurrencyHeader.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +function PaymentCardCurrencyHeader({isSectionList}: {isSectionList?: boolean}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + return ( + + + {`${translate('billingCurrency.note')}`}{' '} + {`${translate('billingCurrency.noteLink')}`}{' '} + {`${translate('billingCurrency.noteDetails')}`} + + + ); +} + +PaymentCardCurrencyHeader.displayName = 'PaymentCardCurrencyHeader'; + +export default PaymentCardCurrencyHeader; diff --git a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx index 60fa838b0577..19b3399caea8 100644 --- a/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx +++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx @@ -1,4 +1,5 @@ import React, {useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -13,16 +14,16 @@ type PaymentCardCurrencyModalProps = { /** Whether the modal is visible */ isVisible: boolean; - /** The list of years to render */ - currencies: Array; + /** The list of currencies to render */ + currencies: Array>; - /** Currently selected year */ - currentCurrency: keyof typeof CONST.CURRENCY; + /** Currently selected currency */ + currentCurrency: ValueOf; - /** Function to call when the user selects a year */ - onCurrencyChange?: (currency: keyof typeof CONST.CURRENCY) => void; + /** Function to call when the user selects a currency */ + onCurrencyChange?: (currency: ValueOf) => void; - /** Function to call when the user closes the year picker */ + /** Function to call when the user closes the currency picker */ onClose?: () => void; }; @@ -57,7 +58,7 @@ function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONS useNativeDriver > , currency?: ValueOf) => void; + addPaymentCard: (values: FormOnyxValues, currency?: ValueOf) => void; submitButtonText: string; /** Custom content to display in the footer after card form */ footerContent?: ReactNode; /** Custom content to display in the header before card form */ headerContent?: ReactNode; + /** object to get currency route details from */ + currencySelectorRoute?: typeof ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY; }; function IAcceptTheLabel() { @@ -61,6 +62,7 @@ const REQUIRED_FIELDS = [ INPUT_IDS.SECURITY_CODE, INPUT_IDS.ADDRESS_ZIP_CODE, INPUT_IDS.ADDRESS_STATE, + INPUT_IDS.CURRENCY, ]; const CARD_TYPES = { @@ -127,42 +129,44 @@ function PaymentCardForm({ showStateSelector, footerContent, headerContent, + currencySelectorRoute, }: PaymentCardFormProps) { const styles = useThemeStyles(); + const [data] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); + const {translate} = useLocalize(); const route = useRoute(); const label = CARD_LABELS[isDebitCard ? CARD_TYPES.DEBIT_CARD : CARD_TYPES.PAYMENT_CARD]; const cardNumberRef = useRef(null); - const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); - const [currency, setCurrency] = useState(CONST.CURRENCY.USD); + const [cardNumber, setCardNumber] = useState(''); - const validate = (values: FormOnyxValues): FormInputErrors => { + const validate = (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS); if (values.nameOnCard && !ValidationUtils.isValidLegalName(values.nameOnCard)) { - errors.nameOnCard = translate('addDebitCardPage.error.invalidName'); + errors.nameOnCard = translate(label.error.nameOnCard); } if (values.cardNumber && !ValidationUtils.isValidDebitCard(values.cardNumber.replace(/ /g, ''))) { - errors.cardNumber = translate('addDebitCardPage.error.debitCardNumber'); + errors.cardNumber = translate(label.error.cardNumber); } if (values.expirationDate && !ValidationUtils.isValidExpirationDate(values.expirationDate)) { - errors.expirationDate = translate('addDebitCardPage.error.expirationDate'); + errors.expirationDate = translate(label.error.expirationDate); } if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) { - errors.securityCode = translate('addDebitCardPage.error.securityCode'); + errors.securityCode = translate(label.error.securityCode); } if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { - errors.addressStreet = translate('addDebitCardPage.error.addressStreet'); + errors.addressStreet = translate(label.error.addressStreet); } if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { - errors.addressZipCode = translate('addDebitCardPage.error.addressZipCode'); + errors.addressZipCode = translate(label.error.addressZipCode); } if (!values.acceptTerms) { @@ -172,13 +176,21 @@ function PaymentCardForm({ return errors; }; - const showCurrenciesModal = useCallback(() => { - setIsCurrencyModalVisible(true); - }, []); + const onChangeCardNumber = useCallback((newValue: string) => { + // replace all characters that are not spaces or digits + let validCardNumber = newValue.replace(/[^\d ]/g, ''); + + // gets only the first 16 digits if the inputted number have more digits than that + validCardNumber = validCardNumber.match(/(?:\d *){1,16}/)?.[0] ?? ''; - const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => { - setCurrency(newCurrency); - setIsCurrencyModalVisible(false); + // add the spacing between every 4 digits + validCardNumber = + validCardNumber + .replace(/ /g, '') + .match(/.{1,4}/g) + ?.join(' ') ?? ''; + + setCardNumber(validCardNumber); }, []); if (!shouldShowPaymentCardForm) { @@ -189,9 +201,9 @@ function PaymentCardForm({ <> {headerContent} addPaymentCard(formData, currency)} + onSubmit={addPaymentCard} submitButtonText={submitButtonText} scrollContextEnabled style={[styles.mh5, styles.flexGrow1]} @@ -199,15 +211,19 @@ function PaymentCardForm({ )} {!!showCurrencyField && ( - - {(isHovered) => ( - - )} - + + + )} {!!showAcceptTerms && ( @@ -298,19 +309,11 @@ function PaymentCardForm({ 'common.privacyPolicy', )}`} inputID={INPUT_IDS.ACCEPT_TERMS} - defaultValue={false} + defaultValue={!!data?.acceptTerms} LabelComponent={IAcceptTheLabel} /> )} - - } - currentCurrency={currency} - onCurrencyChange={changeCurrency} - onClose={() => setIsCurrencyModalVisible(false)} - /> {footerContent} diff --git a/src/components/AddPaymentMethodMenu.tsx b/src/components/AddPaymentMethodMenu.tsx index 734e8affa9ea..325bab091bec 100644 --- a/src/components/AddPaymentMethodMenu.tsx +++ b/src/components/AddPaymentMethodMenu.tsx @@ -11,7 +11,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {AnchorPosition} from '@src/styles'; import type {Report, Session} from '@src/types/onyx'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as Expensicons from './Icon/Expensicons'; import type {PaymentMethod} from './KYCWall/types'; import PopoverMenu from './PopoverMenu'; @@ -32,7 +31,7 @@ type AddPaymentMethodMenuProps = AddPaymentMethodMenuOnyxProps & { onItemSelected: (paymentMethod: PaymentMethod) => void; /** The IOU/Expense report we are paying */ - iouReport?: OnyxEntry | EmptyObject; + iouReport?: OnyxEntry; /** Anchor position for the AddPaymentMenu. */ anchorPosition: AnchorPosition; @@ -65,9 +64,9 @@ function AddPaymentMethodMenu({ // Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report // which then starts a bottom up flow and creates a Collect workspace where the payer is an admin and payee is an employee. - const isIOUReport = ReportUtils.isIOUReport(iouReport ?? {}); + const isIOUReport = ReportUtils.isIOUReport(iouReport); const canUseBusinessBankAccount = - ReportUtils.isExpenseReport(iouReport ?? {}) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '-1', session?.accountID ?? -1)); + ReportUtils.isExpenseReport(iouReport) || (isIOUReport && !ReportActionsUtils.hasRequestFromCurrentAccount(iouReport?.reportID ?? '-1', session?.accountID ?? -1)); const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || isIOUReport; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 19bad0fbdf5a..df027ed6edb4 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -26,7 +26,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type ModalType from '@src/types/utils/ModalType'; import AttachmentCarousel from './Attachments/AttachmentCarousel'; @@ -99,7 +98,7 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { headerTitle?: string; /** The report that has this attachment */ - report?: OnyxEntry | EmptyObject; + report?: OnyxEntry; /** The type of the attachment */ type?: ValueOf; diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 1d10a3b48d55..7e29da7763c5 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -94,8 +94,8 @@ function ConnectionLayout({ }: ConnectionLayoutProps) { const {translate} = useLocalize(); - const policy = PolicyUtils.getPolicy(policyID ?? ''); - const isConnectionEmpty = isEmpty(policy.connections?.[connectionName]); + const policy = PolicyUtils.getPolicy(policyID); + const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]); const renderSelectionContent = useMemo( () => ( diff --git a/src/components/CurrencySelector.tsx b/src/components/CurrencySelector.tsx new file mode 100644 index 000000000000..6c69f7b31dc1 --- /dev/null +++ b/src/components/CurrencySelector.tsx @@ -0,0 +1,78 @@ +import {useIsFocused} from '@react-navigation/native'; +import React, {forwardRef, useEffect, useRef} from 'react'; +import type {ForwardedRef} from 'react'; +import type {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; + +type CurrencySelectorProps = { + /** Form error text. e.g when no currency is selected */ + errorText?: string; + + /** Callback called when the currency changes. */ + onInputChange?: (value?: string) => void; + + /** Current selected currency */ + value?: ValueOf; + + /** inputID used by the Form component */ + // eslint-disable-next-line react/no-unused-prop-types + inputID: string; + + /** Callback to call when the picker modal is dismissed */ + onBlur?: () => void; + + /** object to get route details from */ + currencySelectorRoute?: typeof ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY | typeof ROUTES.SETTINGS_CHANGE_CURRENCY; +}; + +function CurrencySelector( + {errorText = '', value: currency, onInputChange = () => {}, onBlur, currencySelectorRoute = ROUTES.SETTINGS_CHANGE_CURRENCY}: CurrencySelectorProps, + ref: ForwardedRef, +) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const currencyTitleDescStyle = currency ? styles.textNormal : null; + + const didOpenCurrencySelector = useRef(false); + const isFocused = useIsFocused(); + useEffect(() => { + if (!isFocused || !didOpenCurrencySelector.current) { + return; + } + didOpenCurrencySelector.current = false; + onBlur?.(); + }, [isFocused, onBlur]); + + useEffect(() => { + // This will cause the form to revalidate and remove any error related to currency + onInputChange(currency); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currency]); + + return ( + { + didOpenCurrencySelector.current = true; + Navigation.navigate(currencySelectorRoute); + }} + /> + ); +} + +CurrencySelector.displayName = 'CurrencySelector'; + +export default forwardRef(CurrencySelector); diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 9aa8bc921164..6245fdcf7b49 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -8,6 +8,7 @@ import type AmountPicker from '@components/AmountPicker'; import type AmountTextInput from '@components/AmountTextInput'; import type CheckboxWithLabel from '@components/CheckboxWithLabel'; import type CountrySelector from '@components/CountrySelector'; +import type CurrencySelector from '@components/CurrencySelector'; import type DatePicker from '@components/DatePicker'; import type EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import type Picker from '@components/Picker'; @@ -35,6 +36,7 @@ type ValidInputs = | typeof Picker | typeof AddressSearch | typeof CountrySelector + | typeof CurrencySelector | typeof AmountForm | typeof BusinessTypePicker | typeof StateSelector diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer.tsx index 49850d73e2d7..66e297e50734 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer.tsx @@ -15,7 +15,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MentionReportOnyxProps = { @@ -27,7 +26,7 @@ type MentionReportRendererProps = MentionReportOnyxProps & CustomRendererProps value.replace(CONST.UNICODE.LTR, '').replace('#', ''); -const getMentionDetails = (htmlAttributeReportID: string, currentReport: OnyxEntry | EmptyObject, reports: OnyxCollection, tnode: TText | TPhrasing) => { +const getMentionDetails = (htmlAttributeReportID: string, currentReport: OnyxEntry, reports: OnyxCollection, tnode: TText | TPhrasing) => { let reportID: string | undefined; let mentionDisplayText: string; diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 53ed00e04143..568f2a15903f 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -7,7 +7,6 @@ import type {Route} from '@src/ROUTES'; import type {Report} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; type Source = ValueOf; @@ -45,7 +44,7 @@ type KYCWallProps = { chatReportID?: string; /** The IOU/Expense report we are paying */ - iouReport?: OnyxEntry | EmptyObject; + iouReport?: OnyxEntry; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index efd67d6c6b50..2cc931303a30 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -133,7 +133,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio if (ReportActionsUtils.isMoneyRequestAction(lastReportAction)) { lastReportActionTransactionID = ReportActionsUtils.getOriginalMessage(lastReportAction)?.IOUTransactionID ?? '-1'; } - const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; return ( ; @@ -58,7 +57,7 @@ type OptionRowLHNDataProps = { transaction: OnyxEntry; /** The transaction linked to the report's last action */ - lastReportActionTransaction?: OnyxEntry; + lastReportActionTransaction?: OnyxEntry; /** Whether a report contains a draft */ hasDraftComment: boolean; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index e0e30d14d2a2..322a68ffe32a 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -72,7 +72,7 @@ const LocaleContext = createContext({ preferredLocale: CONST.LOCALES.DEFAULT, }); -function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {}, children}: LocaleContextProviderProps) { +function LocaleContextProvider({preferredLocale, currentUserPersonalDetails, children}: LocaleContextProviderProps) { const locale = preferredLocale ?? CONST.LOCALES.DEFAULT; const selectedTimezone = useMemo(() => currentUserPersonalDetails?.timezone?.selected, [currentUserPersonalDetails]); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 64c0de7d7eb2..fa3454ed9e61 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -98,10 +98,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea // Only the requestor can delete the request, admins can only edit it. const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; + const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction; const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; - const canDeleteRequest = - isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [paymentType, setPaymentType] = useState(); const [requestType, setRequestType] = useState(); @@ -249,14 +248,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isHoldCreator = ReportUtils.isHoldCreator(transaction, moneyRequestReport?.reportID) && isRequestIOU; const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(moneyRequestReport); const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover); - if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) { + const isInvoiceReport = ReportUtils.isInvoiceReport(moneyRequestReport); + if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus)) && !isInvoiceReport) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, text: translate('iou.unholdExpense'), onSelected: () => changeMoneyRequestStatus(), }); } - if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning) { + if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning && !isInvoiceReport) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, text: translate('iou.hold'), diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 173525bdaaa9..d55d3cc19fe9 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -64,7 +64,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); - const moneyRequestReport = parentReport; + const isSelfDMTrackExpenseReport = ReportUtils.isTrackExpenseReport(report) && ReportUtils.isSelfDM(parentReport); + const moneyRequestReport = !isSelfDMTrackExpenseReport ? parentReport : undefined; const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); @@ -103,7 +104,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction && !ReportUtils.isArchivedRoom(parentReport); // If the report supports adding transactions to it, then it also supports deleting transactions from it. - const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(report)) && !isDeletedParentAction; + const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; const changeMoneyRequestStatus = () => { const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; diff --git a/src/components/PopoverProvider/index.native.tsx b/src/components/PopoverProvider/index.native.tsx index b13909945bef..d58322fafe63 100644 --- a/src/components/PopoverProvider/index.native.tsx +++ b/src/components/PopoverProvider/index.native.tsx @@ -3,7 +3,7 @@ import type {PopoverContextProps, PopoverContextValue} from './types'; const PopoverContext = React.createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); @@ -13,7 +13,7 @@ function PopoverContextProvider(props: PopoverContextProps) { () => ({ onOpen: () => {}, close: () => {}, - popover: {}, + popover: null, isOpen: false, }), [], diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx index cc6c84477525..82f3c6c7d61a 100644 --- a/src/components/PopoverProvider/index.tsx +++ b/src/components/PopoverProvider/index.tsx @@ -6,7 +6,7 @@ import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types' const PopoverContext = createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index 5022aee0f843..b3d21e9ed5d9 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -8,7 +8,7 @@ type PopoverContextProps = { type PopoverContextValue = { onOpen?: (popoverParams: AnchorRef) => void; - popover?: AnchorRef | Record | null; + popover?: AnchorRef | null; close: (anchorRef?: RefObject) => void; isOpen: boolean; }; diff --git a/src/components/Reactions/ReactionTooltipContent.tsx b/src/components/Reactions/ReactionTooltipContent.tsx index 198eba1f969c..8f469b01272c 100644 --- a/src/components/Reactions/ReactionTooltipContent.tsx +++ b/src/components/Reactions/ReactionTooltipContent.tsx @@ -23,7 +23,7 @@ type ReactionTooltipContentProps = Pick PersonalDetailsUtils.getPersonalDetailsByIDs(accountIDs, currentUserPersonalDetails.accountID, true), [currentUserPersonalDetails.accountID, accountIDs]); diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index bdb9d52a67b7..9e31dc110579 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -4,6 +4,7 @@ import truncate from 'lodash/truncate'; import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -42,7 +43,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {MoneyRequestPreviewProps, PendingMessageProps} from './types'; @@ -247,10 +247,8 @@ function MoneyRequestPreviewContent({ }; const getDisplayDeleteAmountText = (): string => { - const iouOriginalMessage: OriginalMessageIOU | EmptyObject = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action) ?? {} : {}; - const {amount = 0, currency = CONST.CURRENCY.USD} = iouOriginalMessage; - - return CurrencyUtils.convertToDisplayString(amount, currency); + const iouOriginalMessage: OnyxEntry = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action) ?? undefined : undefined; + return CurrencyUtils.convertToDisplayString(iouOriginalMessage?.amount, iouOriginalMessage?.currency); }; const displayAmount = isDeleted ? getDisplayDeleteAmountText() : getDisplayAmountText(); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index fd2427a4ddc3..c796a267fd01 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -131,15 +131,16 @@ function ReportPreview({ const {isSmallScreenWidth} = useWindowDimensions(); const [paymentType, setPaymentType] = useState(); - const managerID = iouReport?.managerID ?? 0; + const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? 0; const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); - const iouSettled = ReportUtils.isSettled(iouReportID); + const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); - const isApproved = ReportUtils.isReportApproved(iouReport); + const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); const numberOfRequests = allTransactions.length; @@ -198,7 +199,7 @@ function ReportPreview({ if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else { - IOU.approveMoneyRequest(iouReport ?? {}, true); + IOU.approveMoneyRequest(iouReport, true); } }; @@ -372,7 +373,7 @@ function ReportPreview({ {getDisplayAmount()} - {ReportUtils.isSettled(iouReportID) && ( + {iouSettled && ( | EmptyObject; + iouReport?: OnyxEntry; /** Should we show the payment options? */ shouldHidePaymentOptions?: boolean; @@ -121,9 +120,9 @@ function SettlementButton({ chatReportID = '', currency = CONST.CURRENCY.USD, enablePaymentsRoute, - // The "iouReport" and "nvpLastPaymentMethod" objects needs to be stable to prevent the "useMemo" - // hook from being recreated unnecessarily, hence the use of CONST.EMPTY_ARRAY and CONST.EMPTY_OBJECT - iouReport = CONST.EMPTY_OBJECT, + iouReport, + // The "nvpLastPaymentMethod" object needs to be stable to prevent the "useMemo" + // hook from being recreated unnecessarily, hence the use of CONST.EMPTY_OBJECT nvpLastPaymentMethod = CONST.EMPTY_OBJECT, isDisabled = false, isLoading = false, @@ -148,7 +147,7 @@ function SettlementButton({ const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || -1}`); const isInvoiceReport = (!isEmptyObject(iouReport) && ReportUtils.isInvoiceReport(iouReport)) || false; const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(chatReport); - const shouldShowPaywithExpensifyOption = !isPaidGroupPolicy || (!shouldHidePaymentOptions && ReportUtils.isPayer(session, iouReport as OnyxEntry)); + const shouldShowPaywithExpensifyOption = !isPaidGroupPolicy || (!shouldHidePaymentOptions && ReportUtils.isPayer(session, iouReport)); const shouldShowPayElsewhereOption = (!isPaidGroupPolicy || policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL) && !isInvoiceReport; const paymentButtonOptions = useMemo(() => { const buttonOptions = []; @@ -238,7 +237,7 @@ function SettlementButton({ if (confirmApproval) { confirmApproval(); } else { - IOU.approveMoneyRequest(iouReport ?? {}); + IOU.approveMoneyRequest(iouReport); } return; } diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 8bbaf1c9305c..91fd388eabf1 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -4,10 +4,8 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import getComponentDisplayName from '@libs/getComponentDisplayName'; import type {PersonalDetails} from '@src/types/onyx'; -type CurrentUserPersonalDetails = PersonalDetails | Record; - type HOCProps = { - currentUserPersonalDetails: CurrentUserPersonalDetails; + currentUserPersonalDetails: PersonalDetails; }; type WithCurrentUserPersonalDetailsProps = HOCProps; @@ -32,4 +30,4 @@ export default function ; - function useCurrentUserPersonalDetails() { const session = useSession(); - const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; + const personalDetails = usePersonalDetails(); const accountID = session?.accountID ?? -1; const accountPersonalDetails = personalDetails?.[accountID]; - const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( - () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails, - [accountPersonalDetails, accountID], - ); + const currentUserPersonalDetails: PersonalDetails = useMemo(() => ({...accountPersonalDetails, accountID}), [accountPersonalDetails, accountID]); return currentUserPersonalDetails; } diff --git a/src/hooks/useKeyboardShortcut.ts b/src/hooks/useKeyboardShortcut.ts index 6bf8b2c52bc3..1c5bbc426ef2 100644 --- a/src/hooks/useKeyboardShortcut.ts +++ b/src/hooks/useKeyboardShortcut.ts @@ -26,7 +26,7 @@ type KeyboardShortcutConfig = { * Register a keyboard shortcut handler. * Recommendation: To ensure stability, wrap the `callback` function with the useCallback hook before using it with this hook. */ -export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: GestureResponderEvent | KeyboardEvent) => void, config: KeyboardShortcutConfig | Record = {}) { +export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: GestureResponderEvent | KeyboardEvent) => void, config: KeyboardShortcutConfig = {}) { const { captureOnInputs = true, shouldBubble = false, diff --git a/src/languages/en.ts b/src/languages/en.ts index 2a5b32be5038..a864e70b6189 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -273,7 +273,7 @@ export default { your: 'your', conciergeHelp: 'Please reach out to Concierge for help.', youAppearToBeOffline: 'You appear to be offline.', - thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.', + thisFeatureRequiresInternet: 'This feature requires an active internet connection.', attachementWillBeAvailableOnceBackOnline: 'Attachment will become available once back online.', areYouSure: 'Are you sure?', verify: 'Verify', @@ -343,40 +343,40 @@ export default { }, location: { useCurrent: 'Use current location', - notFound: 'We were unable to find your location, please try again or enter an address manually.', - permissionDenied: 'It looks like you have denied permission to your location.', + notFound: 'We were unable to find your location. Please try again or enter an address manually.', + permissionDenied: "It looks like you've denied access to your location.", please: 'Please', - allowPermission: 'allow location permission in settings', - tryAgain: 'and then try again.', + allowPermission: 'allow location access in settings', + tryAgain: 'and try again.', }, anonymousReportFooter: { logoTagline: 'Join the discussion.', }, attachmentPicker: { cameraPermissionRequired: 'Camera access', - expensifyDoesntHaveAccessToCamera: "Expensify can't take photos without access to your camera. Tap Settings to update permissions.", + expensifyDoesntHaveAccessToCamera: "Expensify can't take photos without access to your camera. Tap settings to update permissions.", attachmentError: 'Attachment error', - errorWhileSelectingAttachment: 'An error occurred while selecting an attachment, please try again.', - errorWhileSelectingCorruptedAttachment: 'An error occurred while selecting a corrupted attachment, please try another file.', + errorWhileSelectingAttachment: 'An error occurred while selecting an attachment. Please try again.', + errorWhileSelectingCorruptedAttachment: 'An error occurred while selecting a corrupted attachment. Please try another file.', takePhoto: 'Take photo', chooseFromGallery: 'Choose from gallery', chooseDocument: 'Choose file', - attachmentTooLarge: 'Attachment too large', - sizeExceeded: 'Attachment size is larger than 24 MB limit.', - attachmentTooSmall: 'Attachment too small', - sizeNotMet: 'Attachment size must be greater than 240 bytes.', + attachmentTooLarge: 'Attachment is too large', + sizeExceeded: 'Attachment size is larger than 24 MB limit', + attachmentTooSmall: 'Attachment is too small', + sizeNotMet: 'Attachment size must be greater than 240 bytes', wrongFileType: 'Invalid file type', - notAllowedExtension: 'This file type is not allowed', - folderNotAllowedMessage: 'Uploading a folder is not allowed. Try a different file.', + notAllowedExtension: 'This file type is not allowed. Please try a different file type.', + folderNotAllowedMessage: 'Uploading a folder is not allowed. Please try a different file.', protectedPDFNotSupported: 'Password-protected PDF is not supported', }, connectionComplete: { - title: 'Connection Complete', + title: 'Connection complete', supportingText: 'You can close this window and head back to the Expensify app.', }, avatarCropModal: { title: 'Edit photo', - description: 'Drag, zoom, and rotate your image to your preferred specifications', + description: 'Drag, zoom, and rotate your image however you like.', }, composer: { noExtensionFoundForMimeType: 'No extension found for mime type', @@ -385,7 +385,7 @@ export default { }, baseUpdateAppModal: { updateApp: 'Update app', - updatePrompt: 'A new version of this app is available.\nUpdate now or restart the app at a later time to download the latest changes.', + updatePrompt: 'A new version of this app is available.\nUpdate now or restart the app later to download the latest changes.', }, deeplinkWrapper: { launching: 'Launching Expensify', @@ -401,17 +401,17 @@ export default { continueInWeb: 'continue to the web app', }, validateCodeModal: { - successfulSignInTitle: 'Abracadabra,\nyou are signed in!', + successfulSignInTitle: "Abracadabra,\nyou're signed in!", successfulSignInDescription: 'Head back to your original tab to continue.', - title: 'Here is your magic code', - description: 'Please enter the code using the device\nwhere it was originally requested', + title: "Here's your magic code", + description: 'Please enter the code from the device\nwhere it was originally requested', or: ', or', signInHere: 'just sign in here', expiredCodeTitle: 'Magic code expired', expiredCodeDescription: 'Go back to the original device and request a new code.', successfulNewCodeRequest: 'Code requested. Please check your device.', tfaRequiredTitle: 'Two-factor authentication\nrequired', - tfaRequiredDescription: 'Please enter the two-factor authentication code\nwhere you are trying to sign in.', + tfaRequiredDescription: "Please enter the two-factor authentication code\nwhere you're trying to sign in.", }, moneyRequestConfirmationList: { paidBy: 'Paid by', @@ -429,7 +429,7 @@ export default { welcomeText: { getStarted: 'Get started below.', anotherLoginPageIsOpen: 'Another login page is open.', - anotherLoginPageIsOpenExplanation: "You've opened the login page in a separate tab, please login from that specific tab.", + anotherLoginPageIsOpenExplanation: "You've opened the login page in a separate tab. Please log in from that tab.", welcome: 'Welcome!', welcomeWithoutExclamation: 'Welcome', phrase2: "Money talks. And now that chat and payments are in one place, it's also easy.", @@ -445,7 +445,7 @@ export default { }, }, thirdPartySignIn: { - alreadySignedIn: ({email}: AlreadySignedInParams) => `You are already signed in as ${email}.`, + alreadySignedIn: ({email}: AlreadySignedInParams) => `You're already signed in as ${email}.`, goBackMessage: ({provider}: GoBackMessageParams) => `Don't want to sign in with ${provider}?`, continueWithMyCurrentSession: 'Continue with my current session', redirectToDesktopMessage: "We'll redirect you to the desktop app once you finish signing in.", @@ -455,7 +455,7 @@ export default { }, samlSignIn: { welcomeSAMLEnabled: 'Continue logging in with single sign-on:', - orContinueWithMagicCode: 'Or optionally, your company allows signing in with a magic code', + orContinueWithMagicCode: 'You can also sign in with a magic code', useSingleSignOn: 'Use single sign-on', useMagicCode: 'Use magic code', launching: 'Launching...', @@ -546,7 +546,7 @@ export default { hereAlternateText: 'Notify everyone in this conversation', }, newMessages: 'New messages', - youHaveBeenBanned: 'Note: You have been banned from communicating in this channel', + youHaveBeenBanned: "Note: You've been banned from chatting in this channel.", reportTypingIndicator: { isTyping: 'is typing...', areTyping: 'are typing...', @@ -601,8 +601,8 @@ export default { chooseFile: 'Choose file', takePhoto: 'Take a photo', cameraAccess: 'Camera access is required to take pictures of receipts.', - cameraErrorTitle: 'Camera Error', - cameraErrorMessage: 'An error occurred while taking a photo, please try again.', + cameraErrorTitle: 'Camera error', + cameraErrorMessage: 'An error occurred while taking a photo. Please try again.', dropTitle: 'Let it go', dropMessage: 'Drop your file here', flash: 'flash', @@ -658,12 +658,12 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', - pendingMatchWithCreditCard: 'Receipt pending match with credit card.', - pendingMatchWithCreditCardDescription: 'Receipt pending match with credit card. Mark as cash to ignore and request payment.', + pendingMatchWithCreditCard: 'Receipt pending match with card transaction', + pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.', markAsCash: 'Mark as cash', routePending: 'Route pending...', receiptScanning: 'Receipt scanning...', - receiptScanInProgress: 'Receipt scan in progress.', + receiptScanInProgress: 'Receipt scan in progress', receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', receiptIssuesFound: (count: number) => `${count === 1 ? 'Issue' : 'Issues'} found`, fieldPending: 'Pending...', @@ -673,8 +673,8 @@ export default { missingMerchant: 'Missing merchant', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", - receiptScanningFailed: 'Receipt scanning failed. Enter the details manually.', - transactionPendingDescription: 'Transaction pending. It can take a few days from the date the card was used for the transaction to post.', + receiptScanningFailed: 'Receipt scanning failed. Please enter the details manually.', + transactionPendingDescription: 'Transaction pending. It may take a few days to post.', expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' @@ -689,7 +689,7 @@ export default { settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as a business` : `Pay as a business`), payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), - nextStep: 'Next Steps', + nextStep: 'Next steps', finished: 'Finished', sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, submitAmount: ({amount}: RequestAmountParams) => `submit ${amount}`, @@ -731,24 +731,24 @@ export default { tagSelection: 'Select a tag to better organize your spend.', categorySelection: 'Select a category to better organize your spend.', error: { - invalidCategoryLength: 'The length of the category chosen exceeds the maximum allowed (255). Please choose a different or shorten the category name first.', + invalidCategoryLength: 'The category name exceeds 255 characters. Please shorten it or choose a different category.', invalidAmount: 'Please enter a valid amount before continuing.', invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`, invalidSplit: 'The sum of splits must equal the total amount.', - invalidSplitParticipants: 'Enter an amount greater than zero for at least two participants.', - other: 'Unexpected error, please try again later.', + invalidSplitParticipants: 'Please enter an amount greater than zero for at least two participants.', + other: 'Unexpected error. Please try again later.', genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.', - genericCreateInvoiceFailureMessage: 'Unexpected error sending invoice, please try again later.', - genericHoldExpenseFailureMessage: 'Unexpected error while holding the expense. Please try again later.', - genericUnholdExpenseFailureMessage: 'Unexpected error while taking the expense off hold. Please try again later.', + genericCreateInvoiceFailureMessage: 'Unexpected error sending this invoice. Please try again later.', + genericHoldExpenseFailureMessage: 'Unexpected error holding this expense. Please try again later.', + genericUnholdExpenseFailureMessage: 'Unexpected error taking this expense off hold. Please try again later.', receiptDeleteFailureError: 'Unexpected error deleting this receipt. Please try again later.', // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: "The receipt didn't upload. ", // eslint-disable-next-line rulesdir/use-periods-for-error-messages saveFileMessage: 'Download the file ', loseFileMessage: 'or dismiss this error and lose it.', - genericDeleteFailureMessage: 'Unexpected error deleting this expense, please try again later.', - genericEditFailureMessage: 'Unexpected error editing this expense, please try again later.', + genericDeleteFailureMessage: 'Unexpected error deleting this expense. Please try again later.', + genericEditFailureMessage: 'Unexpected error editing this expense. Please try again later.', genericSmartscanFailureMessage: 'Transaction is missing fields.', duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints.', atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses.', @@ -756,7 +756,7 @@ export default { invalidMerchant: 'Please enter a correct merchant.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`, - enableWallet: 'Enable Wallet', + enableWallet: 'Enable wallet', hold: 'Hold', unhold: 'Unhold', holdExpense: 'Hold expense', @@ -766,9 +766,9 @@ export default { explainHold: "Explain why you're holding this expense.", reason: 'Reason', holdReasonRequired: 'A reason is required when holding.', - expenseOnHold: 'This expense was put on hold. Review the comments for next steps.', - expensesOnHold: 'All expenses were put on hold. Review the comments for next steps.', - expenseDuplicate: 'This expense has the same details as another one. Review the duplicates to remove the hold.', + expenseOnHold: 'This expense was put on hold. Please review the comments for next steps.', + expensesOnHold: 'All expenses were put on hold. Please review the comments for next steps.', + expenseDuplicate: 'This expense has the same details as another one. Please review the duplicates to remove the hold.', reviewDuplicates: 'Review duplicates', keepAll: 'Keep all', confirmApprove: 'Confirm approval amount', @@ -781,9 +781,9 @@ export default { whatIsHoldTitle: 'What is hold?', whatIsHoldExplain: 'Hold is our way of streamlining financial collaboration. "Reject" is so harsh!', holdIsTemporaryTitle: 'Hold is usually temporary', - holdIsTemporaryExplain: "Because hold is used to clear up confusion or clarify an important detail before payment, it's not permanent.", + holdIsTemporaryExplain: "Hold is used to clear up confusion or clarify an important detail before payment. Don't worry, it's not permanent!", deleteHoldTitle: "Delete whatever won't be paid", - deleteHoldExplain: "In the rare case where something is put on hold and won't be paid, it's on the person requesting payment to delete it.", + deleteHoldExplain: "In the rare case where something's put on hold and won't be paid, it's on the person requesting payment to delete it.", set: 'set', changed: 'changed', removed: 'removed', @@ -801,8 +801,8 @@ export default { }, }, loginField: { - numberHasNotBeenValidated: 'The number has not yet been validated. Click the button to resend the validation link via text.', - emailHasNotBeenValidated: 'The email has not yet been validated. Click the button to resend the validation link via text.', + numberHasNotBeenValidated: "The number hasn't been validated. Click the button to resend the validation link via text.", + emailHasNotBeenValidated: "The email hasn't been validated. Click the button to resend the validation link via text.", }, avatarWithImagePicker: { uploadPhoto: 'Upload photo', @@ -810,7 +810,7 @@ export default { editImage: 'Edit photo', viewPhoto: 'View photo', imageUploadFailed: 'Image upload failed', - deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar.', + deleteWorkspaceError: 'Sorry, there was an unexpected problem deleting your workspace avatar', sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `The selected image exceeds the maximum upload size of ${maxUploadSizeInMB}MB.`, resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Please upload an image larger than ${minHeightInPx}x${minWidthInPx} pixels and smaller than ${maxHeightInPx}x${maxWidthInPx} pixels.`, @@ -825,18 +825,18 @@ export default { setMyTimezoneAutomatically: 'Set my timezone automatically', timezone: 'Timezone', invalidFileMessage: 'Invalid file. Please try a different image.', - avatarUploadFailureMessage: 'An error occurred uploading the avatar, please try again.', + avatarUploadFailureMessage: 'An error occurred uploading the avatar. Please try again.', online: 'Online', offline: 'Offline', syncing: 'Syncing', profileAvatar: 'Profile avatar', publicSection: { title: 'Public', - subtitle: 'These details are displayed on your public profile, available for people to see.', + subtitle: 'These details are displayed on your public profile. Anyone can see them.', }, privateSection: { title: 'Private', - subtitle: 'These details are used for travel and payments. They are never shown on your public profile.', + subtitle: "These details are used for travel and payments. They're never shown on your public profile.", }, }, securityPage: { @@ -861,10 +861,9 @@ export default { getInTouch: "Whenever we need to get in touch with you, we'll use this contact method.", enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod}`, setAsDefault: 'Set as default', - yourDefaultContactMethod: - 'This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”.', + yourDefaultContactMethod: "This is your current default contact method. Before you can delete it, you'll need to choose another contact method and click “Set as default”.", removeContactMethod: 'Remove contact method', - removeAreYouSure: 'Are you sure you want to remove this contact method? This action cannot be undone.', + removeAreYouSure: "Are you sure you want to remove this contact method? This action can't be undone.", failedNewContact: 'Failed to add this contact method.', genericFailureMessages: { requestContactMethodValidateCode: 'Failed to send a new magic code. Please wait a bit and try again.', @@ -872,7 +871,7 @@ export default { deleteContactMethod: 'Failed to delete contact method. Please reach out to Concierge for help.', setDefaultContactMethod: 'Failed to set a new default contact method. Please reach out to Concierge for help.', addContactMethod: 'Failed to add this contact method. Please reach out to Concierge for help.', - enteredMethodIsAlreadySubmited: 'The Entered Contact Method already exists.', + enteredMethodIsAlreadySubmited: 'This contact method already exists.', passwordRequired: 'password required.', contactMethodRequired: 'Contact method is required.', invalidContactMethod: 'Invalid contact method', @@ -918,7 +917,7 @@ export default { initialSettingsPage: { about: 'About', aboutPage: { - description: 'The New Expensify App is built by a community of open source developers from around the world. Help us build the future of Expensify.', + description: 'The New Expensify App is built by a community of open-source developers from around the world. Help us build the future of Expensify.', appDownloadLinks: 'App download links', viewKeyboardShortcuts: 'View keyboard shortcuts', viewTheCode: 'View the code', @@ -972,7 +971,7 @@ export default { security: 'Security', signOut: 'Sign out', restoreStashed: 'Restore stashed login', - signOutConfirmationText: "You'll lose any offline changes if you sign-out.", + signOutConfirmationText: "You'll lose any offline changes if you sign out.", versionLetter: 'v', readTheTermsAndPrivacy: { phrase1: 'Read the', @@ -992,7 +991,7 @@ export default { enterMessageHere: 'Enter message here', closeAccountWarning: 'Closing your account cannot be undone.', closeAccountPermanentlyDeleteData: 'Are you sure you want to delete your account? This will permanently delete any outstanding expenses.', - enterDefaultContactToConfirm: 'Please type your default contact method to confirm you wish to close your account. Your default contact method is:', + enterDefaultContactToConfirm: 'Please enter your default contact method to confirm you wish to close your account. Your default contact method is:', enterDefaultContact: 'Enter your default contact method', defaultContact: 'Default contact method:', enterYourDefaultContactMethod: 'Please enter your default contact method to close your account.', @@ -1002,7 +1001,7 @@ export default { changingYourPasswordPrompt: 'Changing your password will update your password for both your Expensify.com and New Expensify accounts.', currentPassword: 'Current password', newPassword: 'New password', - newPasswordPrompt: 'New password must be different than your old password, have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', + newPasswordPrompt: 'Your new password must be different from your old password and contain at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', }, twoFactorAuth: { headerTitle: 'Two-factor authentication', @@ -1015,13 +1014,13 @@ export default { stepCodes: 'Recovery codes', keepCodesSafe: 'Keep these recovery codes safe!', codesLoseAccess: - 'If you lose access to your authenticator app and don’t have these codes, you will lose access to your account. \n\nNote: Setting up two-factor authentication will log you out of all other active sessions.', + "If you lose access to your authenticator app and don’t have these codes, you'll lose access to your account. \n\nNote: Setting up two-factor authentication will log you out of all other active sessions.", errorStepCodes: 'Please copy or download codes before continuing.', stepVerify: 'Verify', scanCode: 'Scan the QR code using your', authenticatorApp: 'authenticator app', addKey: 'Or add this secret key to your authenticator app:', - enterCode: 'Then enter the six digit code generated from your authenticator app.', + enterCode: 'Then enter the six-digit code generated from your authenticator app.', stepSuccess: 'Finished', enabled: 'Two-factor authentication is now enabled!', congrats: 'Congrats, now you’ve got that extra security.', @@ -1056,6 +1055,18 @@ export default { genericFailureMessage: "Private notes couldn't be saved.", }, }, + billingCurrency: { + error: { + securityCode: 'Please enter a valid security code.', + }, + securityCode: 'Security code', + changeBillingCurrency: 'Change billing currency', + changePaymentCurrency: 'Change payment currency', + paymentCurrency: 'Payment currency', + note: 'Note: Changing your payment currency can impact how much you’ll pay for Expensify. Refer to our', + noteLink: 'pricing page', + noteDetails: 'for full details.', + }, addDebitCardPage: { addADebitCard: 'Add a debit card', nameOnCard: 'Name on card', @@ -1072,10 +1083,10 @@ export default { debitCardNumber: 'Please enter a valid debit card number.', expirationDate: 'Please select a valid expiration date.', securityCode: 'Please enter a valid security code.', - addressStreet: 'Please enter a valid billing address that is not a PO Box.', + addressStreet: "Please enter a valid billing address that's not a PO box.", addressState: 'Please select a state.', addressCity: 'Please enter a city.', - genericFailureMessage: 'An error occurred while adding your card, please try again.', + genericFailureMessage: 'An error occurred while adding your card. Please try again.', password: 'Please enter your Expensify password.', }, }, @@ -1095,10 +1106,10 @@ export default { paymentCardNumber: 'Please enter a valid card number.', expirationDate: 'Please select a valid expiration date.', securityCode: 'Please enter a valid security code.', - addressStreet: 'Please enter a valid billing address that is not a PO Box.', + addressStreet: "Please enter a valid billing address that's not a PO box.", addressState: 'Please select a state.', addressCity: 'Please enter a city.', - genericFailureMessage: 'An error occurred while adding your card, please try again.', + genericFailureMessage: 'An error occurred while adding your card. Please try again.', password: 'Please enter your Expensify password.', }, }, @@ -1107,7 +1118,7 @@ export default { setDefaultConfirmation: 'Make default payment method', setDefaultSuccess: 'Default payment method set!', deleteAccount: 'Delete account', - deleteConfirmation: 'Are you sure that you want to delete this account?', + deleteConfirmation: 'Are you sure you want to delete this account?', error: { notOwnerOfBankAccount: 'There was an error setting this bank account as your default payment method.', invalidBankAccount: 'This bank account is temporarily suspended.', @@ -1130,13 +1141,13 @@ export default { assignedCards: 'Assigned cards', assignedCardsDescription: 'These are cards assigned by a workspace admin to manage company spend.', expensifyCard: 'Expensify Card', - walletActivationPending: "We're reviewing your information, please check back in a few minutes!", - walletActivationFailed: 'Unfortunately your wallet cannot be enabled at this time. Please chat with Concierge for further assistance.', - addYourBankAccount: 'Add your bank account.', + walletActivationPending: "We're reviewing your information. Please check back in a few minutes!", + walletActivationFailed: "Unfortunately, your wallet can't be enabled at this time. Please chat with Concierge for further assistance.", + addYourBankAccount: 'Add your bank account', addBankAccountBody: "Let's connect your bank account to Expensify so it’s easier than ever to send and receive payments directly in the app.", - chooseYourBankAccount: 'Choose your bank account.', + chooseYourBankAccount: 'Choose your bank account', chooseAccountBody: 'Make sure that you select the right one.', - confirmYourBankAccount: 'Confirm your bank account.', + confirmYourBankAccount: 'Confirm your bank account', }, cardPage: { expensifyCard: 'Expensify Card', @@ -1159,7 +1170,7 @@ export default { reportFraud: 'Report virtual card fraud', reviewTransaction: 'Review transaction', suspiciousBannerTitle: 'Suspicious transaction', - suspiciousBannerDescription: 'We noticed suspicious transaction on your card. Tap below to review.', + suspiciousBannerDescription: 'We noticed suspicious transactions on your card. Tap below to review.', cardLocked: "Your card is temporarily locked while our team reviews your company's account.", cardDetails: { cardNumber: 'Virtual card number', @@ -1206,12 +1217,12 @@ export default { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'The delayed submission parameter could not be changed. Please try again or contact support.', - autoReportingFrequencyErrorMessage: 'The submission frequency could not be changed. Please try again or contact support.', - monthlyOffsetErrorMessage: 'The monthly frequency could not be changed. Please try again or contact support.', + autoReportingErrorMessage: "Delayed submission couldn't be changed. Please try again or contact support.", + autoReportingFrequencyErrorMessage: "Submission frequency couldn't be changed. Please try again or contact support.", + monthlyOffsetErrorMessage: "Monthly frequency couldn't be changed. Please try again or contact support.", }, workflowsApprovalPage: { - genericErrorMessage: 'The approver could not be changed. Please try again or contact support.', + genericErrorMessage: "The approver couldn't be changed. Please try again or contact support.", }, workflowsPayerPage: { title: 'Authorized payer', @@ -1296,7 +1307,7 @@ export default { }, priorityModePage: { priorityMode: 'Priority mode', - explainerText: 'Choose whether to show all chats by default sorted with most recent with pinned items at the top, or #focus on unread pinned items, sorted alphabetically.', + explainerText: 'Choose whether to #focus on unread and pinned chats only, or show everything with the most recent and pinned chats at the top.', priorityModes: { default: { label: 'Most recent', @@ -1397,7 +1408,7 @@ export default { error: { invalidFormatEmailLogin: 'The email entered is invalid. Please fix the format and try again.', }, - cannotGetAccountDetails: "Couldn't retrieve account details, please try to sign in again.", + cannotGetAccountDetails: "Couldn't retrieve account details. Please try to sign in again.", loginForm: 'Login form', notYou: ({user}: NotYouParams) => `Not ${user}?`, }, @@ -1622,7 +1633,7 @@ export default { }, }, messages: { - errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, + errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US, please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, errorMessageInvalidEmail: 'Invalid email', userIsAlreadyMember: ({login, name}: UserIsAlreadyMemberParams) => `${login} is already a member of ${name}`, }, @@ -1641,24 +1652,24 @@ export default { originalDocumentNeeded: 'Please upload an original image of your ID rather than a screenshot or scanned image.', documentNeedsBetterQuality: 'Your ID appears to be damaged or has missing security features. Please upload an original image of an undamaged ID that is entirely visible.', imageNeedsBetterQuality: "There's an issue with the image quality of your ID. Please upload a new image where your entire ID can be seen clearly.", - selfieIssue: "There's an issue with your selfie/video. Please upload a new selfie/video in real time.", + selfieIssue: "There's an issue with your selfie/video. Please upload a live selfie/video.", selfieNotMatching: "Your selfie/video doesn't match your ID. Please upload a new selfie/video where your face can be clearly seen.", selfieNotLive: "Your selfie/video doesn't appear to be a live photo/video. Please upload a live selfie/video.", }, additionalDetailsStep: { headerTitle: 'Additional details', - helpText: 'We need to confirm the following information before you can send and receive money from your Wallet.', - helpTextIdologyQuestions: 'We need to ask you just a few more questions to finish validating your identity.', + helpText: 'We need to confirm the following information before you can send and receive money with your wallet.', + helpTextIdologyQuestions: 'Just a few more questions to finish verifying your identity.', helpLink: 'Learn more about why we need this.', legalFirstNameLabel: 'Legal first name', legalMiddleNameLabel: 'Legal middle name', legalLastNameLabel: 'Legal last name', selectAnswer: 'You need to select a response to proceed.', - ssnFull9Error: 'Please enter a valid 9 digit SSN.', + ssnFull9Error: 'Please enter a valid 9-digit SSN.', needSSNFull9: "We're having trouble verifying your SSN. Please enter the full 9 digits of your SSN.", weCouldNotVerify: 'We could not verify', pleaseFixIt: 'Please fix this information before continuing.', - failedKYCTextBefore: "We weren't able to successfully verify your identity. Please try again later and reach out to ", + failedKYCTextBefore: "We weren't able to verify your identity. Please try again later or reach out to ", failedKYCTextAfter: ' if you have any questions.', }, termsStep: { @@ -1692,7 +1703,7 @@ export default { weChargeOneFee: 'We charge one type of fee.', fdicInsurance: 'Your funds are eligible for FDIC insurance.', generalInfo: 'For general information about prepaid accounts, visit', - conditionsDetails: 'Find details and conditions for all fees and services by visiting', + conditionsDetails: 'For details and conditions for all fees and services, visit', conditionsPhone: 'or calling +1 833-400-0904.', instant: '(instant)', electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(min ${amount})`, @@ -1703,19 +1714,19 @@ export default { feeAmountHeader: 'Fee amount', moreDetailsHeader: 'More details', openingAccountTitle: 'Opening an account', - openingAccountDetails: 'There is no fee to open an account.', - monthlyFeeDetails: 'There is no monthly fee.', + openingAccountDetails: "There's no fee to open an account.", + monthlyFeeDetails: "There's no monthly fee.", customerServiceTitle: 'Customer service', customerServiceDetails: 'There are no customer service fees.', - inactivityDetails: 'There is no inactivity fee.', + inactivityDetails: "There's no inactivity fee.", sendingFundsTitle: 'Sending funds to another account holder', - sendingFundsDetails: 'There is no fee to send funds to another account holder using your balance, bank account, or debit card.', + sendingFundsDetails: "There's no fee to send funds to another account holder using your balance, bank account, or debit card.", electronicFundsStandardDetails: - 'There is no fee to transfer funds from your Expensify Wallet ' + + "There's no fee to transfer funds from your Expensify Wallet " + 'to your bank account using the standard option. This transfer usually completes within 1-3 business' + ' days.', electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => - 'There is a fee to transfer funds from your Expensify Wallet to ' + + "There's a fee to transfer funds from your Expensify Wallet to " + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`, fdicInsuranceBancorp: ({amount}: TermsParams) => @@ -1729,7 +1740,7 @@ export default { generalInformation2: 'If you have a complaint about a prepaid account, call the Consumer Financial Protection Bureau at 1-855-411-2372 or visit', printerFriendlyView: 'View printer-friendly version', automated: 'Automated', - liveAgent: 'Live Agent', + liveAgent: 'Live agent', instant: 'Instant', electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Min ${amount}`, }, @@ -1773,18 +1784,18 @@ export default { }, personalInfoStep: { personalInfo: 'Personal info', - enterYourLegalFirstAndLast: 'Enter your legal first and last name.', + enterYourLegalFirstAndLast: "What's your legal name?", legalFirstName: 'Legal first name', legalLastName: 'Legal last name', legalName: 'Legal name', - enterYourDateOfBirth: 'Enter your date of birth.', - enterTheLast4: 'Enter the last 4 of your SSN.', + enterYourDateOfBirth: "What's your date of birth?", + enterTheLast4: 'What are the last four digits of your Social Security Number?', dontWorry: "Don't worry, we don't do any personal credit checks!", - last4SSN: 'Last 4 Social Security Number', - enterYourAddress: 'Enter your address.', + last4SSN: 'Last 4 of SSN', + enterYourAddress: "What's your address?", address: 'Address', letsDoubleCheck: "Let's double check that everything looks right.", - byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + byAddingThisBankAccount: "By adding this bank account, you confirm that you've read, understand, and accept", whatsYourLegalName: 'What’s your legal name?', whatsYourDOB: 'What’s your date of birth?', whatsYourAddress: 'What’s your address?', @@ -1795,17 +1806,17 @@ export default { weNeedThisToVerify: 'We need this to verify your wallet.', }, businessInfoStep: { - businessInfo: 'Business info', - enterTheNameOfYourBusiness: 'Enter the name of your business.', - businessName: 'Legal business name', - enterYourCompanysTaxIdNumber: 'Enter your company’s Tax ID number.', + businessInfo: 'Company info', + enterTheNameOfYourBusiness: "What's the name of your company?", + businessName: 'Legal company name', + enterYourCompanysTaxIdNumber: "What's your company’s Tax ID number?", taxIDNumber: 'Tax ID number', taxIDNumberPlaceholder: '9 digits', - enterYourCompanysWebsite: 'Enter your company’s website.', + enterYourCompanysWebsite: "What's your company’s website?", companyWebsite: 'Company website', - enterYourCompanysPhoneNumber: 'Enter your company’s phone number.', - enterYourCompanysAddress: 'Enter your company’s address.', - selectYourCompanysType: 'Select your company’s type.', + enterYourCompanysPhoneNumber: "What's your company’s phone number?", + enterYourCompanysAddress: "What's your company’s address?", + selectYourCompanysType: 'What type of company is it?', companyType: 'Company type', incorporationType: { LLC: 'LLC', @@ -1815,11 +1826,11 @@ export default { SOLE_PROPRIETORSHIP: 'Sole proprietorship', OTHER: 'Other', }, - selectYourCompanysIncorporationDate: 'Select your company’s incorporation date.', + selectYourCompanysIncorporationDate: "What's your company’s incorporation date?", incorporationDate: 'Incorporation date', incorporationDatePlaceholder: 'Start date (yyyy-mm-dd)', incorporationState: 'Incorporation state', - pleaseSelectTheStateYourCompanyWasIncorporatedIn: 'Please select the state your company was incorporated in.', + pleaseSelectTheStateYourCompanyWasIncorporatedIn: 'Which state was your company incorporated in?', letsDoubleCheck: "Let's double check that everything looks right.", companyAddress: 'Company address', listOfRestrictedBusinesses: 'list of restricted businesses', @@ -1829,36 +1840,35 @@ export default { doYouOwn25percent: 'Do you own 25% or more of', doAnyIndividualOwn25percent: 'Do any individuals own 25% or more of', areThereMoreIndividualsWhoOwn25percent: 'Are there more individuals who own 25% or more of', - regulationRequiresUsToVerifyTheIdentity: 'Regulation requires us to verify the identity of any individual that owns more than 25% of the company.', + regulationRequiresUsToVerifyTheIdentity: 'Regulation requires us to verify the identity of any individual who owns more than 25% of the company.', companyOwner: 'Company owner', - enterLegalFirstAndLastName: 'Enter the legal first and last name of the owner.', + enterLegalFirstAndLastName: "What's the owner's legal name?", legalFirstName: 'Legal first name', legalLastName: 'Legal last name', - enterTheDateOfBirthOfTheOwner: 'Enter the date of birth of the owner.', - enterTheLast4: 'Enter the last 4 of the owner’s SSN.', - last4SSN: 'Last 4 Social Security Number', + enterTheDateOfBirthOfTheOwner: "What's the owner's date of birth?", + enterTheLast4: 'What are the last 4 digits of the owner’s Social Security Number?', + last4SSN: 'Last 4 of SSN', dontWorry: "Don't worry, we don't do any personal credit checks!", - enterTheOwnersAddress: 'Enter the owner’s address.', + enterTheOwnersAddress: "What's the owner's address?", letsDoubleCheck: 'Let’s double check that everything looks right.', legalName: 'Legal name', address: 'Address', - byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + byAddingThisBankAccount: "By adding this bank account, you confirm that you've read, understand, and accept", owners: 'Owners', }, validationStep: { - headerTitle: 'Validate Bank Account', + headerTitle: 'Validate bank account', buttonText: 'Finish setup', maxAttemptsReached: 'Validation for this bank account has been disabled due to too many incorrect attempts.', - description: 'A day or two after you add your account to Expensify we send three (3) transactions to your account. They have a merchant line like "Expensify, Inc. Validation".', + description: `Within 1-2 business days, we'll send three (3) small transactions to your bank account from a name like "Expensify, Inc. Validation".`, descriptionCTA: 'Please enter each transaction amount in the fields below. Example: 1.51.', reviewingInfo: "Thanks! We're reviewing your information, and will be in touch shortly. Please check your chat with Concierge ", forNextStep: ' for next steps to finish setting up your bank account.', letsChatCTA: "Yes, let's chat", - letsChatText: 'Thanks for doing that. We need your help verifying a few pieces of information, but we can work this out quickly over chat. Ready?', + letsChatText: 'Almost there! We need your help verifying a few last bits of information over chat. Ready?', letsChatTitle: "Let's chat!", - enable2FATitle: 'Prevent fraud, enable two-factor authentication!', - enable2FAText: - 'We take your security seriously, so please set up two-factor authentication for your account now. That will allow us to dispute Expensify Card digital transactions, and will reduce your risk for fraud.', + enable2FATitle: 'Prevent fraud, enable two-factor authentication (2FA)', + enable2FAText: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', secureYourAccount: 'Secure your account', }, beneficialOwnersStep: { @@ -1880,7 +1890,7 @@ export default { completeVerification: 'Complete verification', confirmAgreements: 'Please confirm the agreements below.', certifyTrueAndAccurate: 'I certify that the information provided is true and accurate', - certifyTrueAndAccurateError: 'Must certify information is true and accurate', + certifyTrueAndAccurateError: 'Please certify that the information is true and accurate', isAuthorizedToUseBankAccount: 'I am authorized to use my company bank account for business spend', isAuthorizedToUseBankAccountError: 'You must be a controlling officer with authorization to operate the business bank account.', termsAndConditions: 'terms and conditions', @@ -1892,21 +1902,20 @@ export default { validateButtonText: 'Validate', validationInputLabel: 'Transaction', maxAttemptsReached: 'Validation for this bank account has been disabled due to too many incorrect attempts.', - description: 'A day or two after you add your account to Expensify we send three (3) transactions to your account. They have a merchant line like "Expensify, Inc. Validation".', + description: `Within 1-2 business days, we'll send three (3) small transactions to your bank account from a name like "Expensify, Inc. Validation".`, descriptionCTA: 'Please enter each transaction amount in the fields below. Example: 1.51.', - reviewingInfo: "Thanks! We're reviewing your information, and will be in touch shortly. Please check your chat with Concierge ", + reviewingInfo: "Thanks! We're reviewing your information and will be in touch shortly. Please check your chat with Concierge ", forNextSteps: ' for next steps to finish setting up your bank account.', letsChatCTA: "Yes, let's chat", - letsChatText: 'Thanks for doing that. We need your help verifying a few pieces of information, but we can work this out quickly over chat. Ready?', + letsChatText: 'Almost there! We need your help verifying a few last bits of information over chat. Ready?', letsChatTitle: "Let's chat!", - enable2FATitle: 'Prevent fraud, enable two-factor authentication!', - enable2FAText: - 'We take your security seriously, so please set up two-factor authentication for your account now. That will allow us to dispute Expensify Card digital transactions, and will reduce your risk for fraud.', + enable2FATitle: 'Prevent fraud, enable two-factor authentication (2FA)', + enable2FAText: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', secureYourAccount: 'Secure your account', }, reimbursementAccountLoadingAnimation: { oneMoment: 'One moment', - explanationLine: 'We’re taking a look at your information. You will be able to continue with next steps shortly.', + explanationLine: "We’re taking a look at your information. You'll be able to continue with next steps shortly.", }, session: { offlineMessageRetry: "Looks like you're offline. Please check your connection and try again.", @@ -1974,14 +1983,14 @@ export default { settlementFrequency: 'Settlement frequency', deleteConfirmation: 'Are you sure you want to delete this workspace?', unavailable: 'Unavailable workspace', - memberNotFound: 'Member not found. To invite a new member to the workspace, please use the Invite button above.', - notAuthorized: `You do not have access to this page. Are you trying to join the workspace? Please reach out to the owner of this workspace so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, + memberNotFound: 'Member not found. To invite a new member to the workspace, please use the invite button above.', + notAuthorized: `You don't have access to this page. If you're trying to join this workspace, just ask the workspace owner to add you as a member. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}.`, goToRoom: ({roomName}: GoToRoomParams) => `Go to ${roomName} room`, workspaceName: 'Workspace name', workspaceOwner: 'Owner', workspaceType: 'Workspace type', workspaceAvatar: 'Workspace avatar', - mustBeOnlineToViewMembers: 'You must be online in order to view members of this workspace.', + mustBeOnlineToViewMembers: 'You need to be online in order to view members of this workspace.', moreFeatures: 'More features', requested: 'Requested', distanceRates: 'Distance rates', @@ -2037,7 +2046,7 @@ export default { }, receivable: 'Accounts receivable', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. archive: 'Accounts receivable archive', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. - exportInvoicesDescription: 'Invoices will export to this account in QuickBooks Online.', + exportInvoicesDescription: 'Use this account when exporting invoices to QuickBooks Online.', exportCompanyCardsDescription: 'Set how company card purchases export to QuickBooks Online.', vendor: 'Vendor', defaultVendor: 'Default vendor', @@ -2151,7 +2160,7 @@ export default { }, exportDate: { label: 'Export date', - description: 'Use this date when exporting reports to Xero.', + description: 'Use this date when exporting purchase bills to Xero.', values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { label: 'Date of last expense', @@ -2169,7 +2178,7 @@ export default { }, invoiceStatus: { label: 'Purchase bill status', - description: 'Choose a status for purchase bills exported to Xero.', + description: 'Use this status when exporting purchase bills to Xero.', values: { [CONST.XERO_CONFIG.INVOICE_STATUS.DRAFT]: 'Draft', [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_APPROVAL]: 'Awaiting approval', @@ -2371,25 +2380,25 @@ export default { selectAll: 'Select all', error: { genericAdd: 'There was a problem adding this workspace member.', - cannotRemove: 'You cannot remove yourself or the workspace owner.', + cannotRemove: "You can't remove yourself or the workspace owner.", genericRemove: 'There was a problem removing that workspace member.', }, - addedWithPrimary: 'Some users were added with their primary logins.', + addedWithPrimary: 'Some members were added with their primary logins.', invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, membersListTitle: 'Directory of all workspace members.', }, card: { header: 'Unlock free Expensify Cards', headerWithEcard: 'Cards are ready!', - noVBACopy: 'Connect a bank account to issue Expensify Cards to your workspace members, and access these incredible benefits and more:', - VBANoECardCopy: 'Add a work email address to issue unlimited Expensify Cards for your workspace members, as well as all of these incredible benefits:', + noVBACopy: 'Connect a bank account to issue Expensify Cards to your workspace members and access exclusive benefits like:', + VBANoECardCopy: 'Add a work email to issue unlimited Expensify Cards to your workspace members and enjoy exclusive benefits like:', VBAWithECardCopy: 'Access these incredible benefits and more:', benefit1: 'Cash back on every US purchase', - benefit2: 'Digital and physical cards', + benefit2: 'Unlimited virtual and physical cards', benefit3: 'No personal liability', - benefit4: 'Customizable limits', + benefit4: 'Customizable limits and spend controls', addWorkEmail: 'Add work email address', - checkingDomain: 'Hang tight! We are still working on enabling your Expensify Cards. Check back here in a few minutes.', + checkingDomain: "Hang tight! We're still working on enabling your Expensify Cards. Check back here in a few minutes.", }, reimburse: { captureReceipts: 'Capture receipts', @@ -2403,10 +2412,10 @@ export default { trackDistanceChooseUnit: 'Choose a default unit to track.', unlockNextDayReimbursements: 'Unlock next-day reimbursements', captureNoVBACopyBeforeEmail: 'Ask your workspace members to forward receipts to ', - captureNoVBACopyAfterEmail: ' and download the Expensify App to track cash expenses on the go.', - unlockNoVBACopy: 'Connect a bank account to reimburse your workspace members online.', + captureNoVBACopyAfterEmail: ' and download the Expensify app to track expenses on the go.', + unlockNoVBACopy: 'Connect a bank account to reimburse your workspace members quickly and easily.', fastReimbursementsVBACopy: "You're all set to reimburse receipts from your bank account!", - updateCustomUnitError: "Your changes couldn't be saved. The workspace was modified while you were offline, please try again.", + updateCustomUnitError: "Your changes couldn't be saved because the workspace was modified while you were offline. Please try again.", invalidRateError: 'Please enter a valid rate.', lowRateError: 'Rate must be greater than 0.', }, @@ -2451,14 +2460,14 @@ export default { taxes: 'Taxes', imported: 'Imported', notImported: 'Not imported', - importAsCategory: 'Imported, displayed as categories', + importAsCategory: 'Imported as categories', importTypes: { [CONST.INTEGRATION_ENTITY_MAP_TYPES.IMPORTED]: 'Imported', - [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: 'Imported, displayed as tags', + [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: 'Imported as tags', [CONST.INTEGRATION_ENTITY_MAP_TYPES.DEFAULT]: 'Imported', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NOT_IMPORTED]: 'Not imported', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE]: 'Not imported', - [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported, displayed as report fields', + [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported as report fields', }, disconnectPrompt: (integrationToConnect?: ConnectionName, currentIntegration?: ConnectionName): string => { switch (integrationToConnect) { @@ -2590,10 +2599,10 @@ export default { }, invoices: { invoiceClientsAndCustomers: 'Invoice clients and customers', - invoiceFirstSectionCopy: 'Send beautiful, professional invoices directly to your clients and customers right from within the Expensify app.', + invoiceFirstSectionCopy: 'Send beautiful, professional invoices directly to your clients and customers right from the Expensify app.', viewAllInvoices: 'View all invoices', unlockOnlineInvoiceCollection: 'Unlock online invoice collection', - unlockNoVBACopy: 'Connect your bank account to accept online payments for invoices - by ACH or credit card - to be deposited straight into your account.', + unlockNoVBACopy: 'Connect your bank account to accept online invoice payments by ACH or credit card.', moneyBackInAFlash: 'Money back, in a flash!', unlockVBACopy: "You're all set to accept payments by ACH or credit card!", viewUnpaidInvoices: 'View unpaid invoices', @@ -2619,7 +2628,7 @@ export default { member: 'Invite member', members: 'Invite members', invitePeople: 'Invite new members', - genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', + genericFailureMessage: 'An error occurred inviting the member to the workspace. Please try again.', pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, user: 'user', users: 'users', @@ -2633,14 +2642,14 @@ export default { inviteMessageTitle: 'Add message', inviteMessagePrompt: 'Make your invitation extra special by adding a message below', personalMessagePrompt: 'Message', - genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', + genericFailureMessage: 'An error occurred inviting the member to the workspace. Please try again.', inviteNoMembersError: 'Please select at least one member to invite.', }, distanceRates: { oopsNotSoFast: 'Oops! Not so fast...', workspaceNeeds: 'A workspace needs at least one enabled distance rate.', distance: 'Distance', - centrallyManage: 'Centrally manage rates, choose to track in miles or kilometers, and set a default category.', + centrallyManage: 'Centrally manage rates, track in miles or kilometers, and set a default category.', rate: 'Rate', addRate: 'Add rate', trackTax: 'Track tax', @@ -2659,27 +2668,26 @@ export default { editor: { descriptionInputLabel: 'Description', nameInputLabel: 'Name', - nameInputHelpText: 'This is the name you will see on your workspace.', - nameIsRequiredError: 'You need to define a name for your workspace.', + nameInputHelpText: "This is the name you'll see on your workspace.", + nameIsRequiredError: "You'll need to give your workspace a name.", currencyInputLabel: 'Default currency', currencyInputHelpText: 'All expenses on this workspace will be converted to this currency.', currencyInputDisabledText: "The default currency can't be changed because this workspace is linked to a USD bank account.", save: 'Save', - genericFailureMessage: 'An error occurred updating the workspace, please try again.', - avatarUploadFailureMessage: 'An error occurred uploading the avatar, please try again.', - addressContext: 'A workspace address is required to enable Expensify Travel. Please enter an address associated with your business.', + genericFailureMessage: 'An error occurred updating the workspace. Please try again.', + avatarUploadFailureMessage: 'An error occurred uploading the avatar. Please try again.', + addressContext: 'A Workspace Address is required to enable Expensify Travel. Please enter an address associated with your business.', }, bankAccount: { continueWithSetup: 'Continue with setup', - youreAlmostDone: - "You're almost done setting up your bank account, which will let you issue corporate cards, reimburse expenses, collect invoices, and pay bills all from the same bank account.", + youreAlmostDone: "You're almost done setting up your bank account, which will let you issue corporate cards, reimburse expenses, collect invoices, and pay bills.", streamlinePayments: 'Streamline payments', oneMoreThing: 'One more thing!', allSet: "You're all set!", accountDescriptionNoCards: - 'This bank account will be used to reimburse expenses, collect invoices, and pay bills all from the same account.\n\nPlease add a work email address as a secondary login to enable the Expensify Card.', - accountDescriptionWithCards: 'This bank account will be used to issue corporate cards, reimburse expenses, collect invoices, and pay bills all from the same account.', - addWorkEmail: 'Add work email address', + 'This bank account will be used to reimburse expenses, collect invoices, and pay bills.\n\nPlease add a work email as a secondary login to enable the Expensify Card.', + accountDescriptionWithCards: 'This bank account will be used to issue corporate cards, reimburse expenses, collect invoices, and pay bills.', + addWorkEmail: 'Add work email', letsFinishInChat: "Let's finish in chat!", almostDone: 'Almost done!', disconnectBankAccount: 'Disconnect bank account', @@ -2689,7 +2697,7 @@ export default { yesStartOver: 'Yes, start over', disconnectYour: 'Disconnect your ', bankAccountAnyTransactions: ' bank account. Any outstanding transactions for this account will still complete.', - clearProgress: 'Starting over will clear the progress you have made so far.', + clearProgress: "Starting over will clear any progress you've made.", areYouSure: 'Are you sure?', workspaceCurrency: 'Workspace currency', updateCurrencyPrompt: 'It looks like your workspace is currently set to a different currency than USD. Please click the button below to update your currency to USD now.', @@ -2711,7 +2719,7 @@ export default { addPaymentCardSecurity: 'security', amountOwedTitle: 'Outstanding balance', amountOwedButtonText: 'OK', - amountOwedText: 'This account has an outstanding balance from a previous month.\n\nDo you want to clear balance and take over billing of this workspace?', + amountOwedText: 'This account has an outstanding balance from a previous month.\n\nDo you want to clear the balance and take over billing of this workspace?', ownerOwesAmountTitle: 'Outstanding balance', ownerOwesAmountButtonText: 'Transfer balance', ownerOwesAmountText: ({email, amount}) => @@ -2719,7 +2727,7 @@ export default { subscriptionTitle: 'Take over annual subscription', subscriptionButtonText: 'Transfer subscription', subscriptionText: ({usersCount, finalCount}) => - `Taking over this workspace will merge its associated annual subscription with your current subscription. This will increase your subscription size by ${usersCount} users making your new subscription size ${finalCount}. Would you like to continue?`, + `Taking over this workspace will merge its annual subscription with your current subscription. This will increase your subscription size by ${usersCount} members making your new subscription size ${finalCount}. Would you like to continue?`, duplicateSubscriptionTitle: 'Duplicate subscription alert', duplicateSubscriptionButtonText: 'Continue', duplicateSubscriptionText: ({email, workspaceName}) => @@ -2727,7 +2735,7 @@ export default { hasFailedSettlementsTitle: 'Cannot transfer ownership', hasFailedSettlementsButtonText: 'Got it', hasFailedSettlementsText: ({email}) => - `You cannot take over billing because ${email} has an overdue expensify Expensify Card settlement. Please advise them to reach out to concierge@expensify.com to resolve the issue. Then, you can take over billing for this workspace.`, + `You can't take over billing because ${email} has an overdue expensify Expensify Card settlement. Please ask them to reach out to concierge@expensify.com to resolve the issue. Then, you can take over billing for this workspace.`, failedToClearBalanceTitle: 'Failed to clear balance', failedToClearBalanceButtonText: 'OK', failedToClearBalanceText: 'We were unable to clear the balance. Please try again later.', @@ -2788,14 +2796,14 @@ export default { createRoom: 'Create room', roomAlreadyExistsError: 'A room with this name already exists.', roomNameReservedError: ({reservedName}: RoomNameReservedErrorParams) => `${reservedName} is a default room on all workspaces. Please choose another name.`, - roomNameInvalidError: 'Room names can only include lowercase letters, numbers and hyphens.', + roomNameInvalidError: 'Room names can only include lowercase letters, numbers, and hyphens.', pleaseEnterRoomName: 'Please enter a room name.', pleaseSelectWorkspace: 'Please select a workspace.', renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => ` renamed this room from ${oldName} to ${newName}`, roomRenamedTo: ({newName}: RoomRenamedToParams) => `Room renamed to ${newName}`, social: 'social', selectAWorkspace: 'Select a workspace', - growlMessageOnRenameError: 'Unable to rename policy room, please check your connection and try again.', + growlMessageOnRenameError: 'Unable to rename workspace room. Please check your connection and try again.', visibilityOptions: { restricted: 'Workspace', // the translation for "restricted" visibility is actually workspace. This is so we can display restricted visibility rooms as "workspace" without having to change what's stored. private: 'Private', @@ -2805,8 +2813,8 @@ export default { }, }, roomMembersPage: { - memberNotFound: 'Member not found. To invite a new member to the room, please use the Invite button above.', - notAuthorized: `You do not have access to this page. Are you trying to join the room? Please reach out to a member of this room so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, + memberNotFound: 'Member not found. To invite a new member to the room, please use the invite button above.', + notAuthorized: `You don't have access to this page. If you're trying to join this room, just ask a room member to add you. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, removeMembersPrompt: 'Are you sure you want to remove the selected members from the room?', error: { genericAdd: 'There was a problem adding this room member.', @@ -2833,18 +2841,18 @@ export default { completed: 'marked as complete', canceled: 'deleted task', reopened: 'marked as incomplete', - error: 'You do not have the permission to do the requested action.', + error: "You don't have permission to take the requested action.", }, markAsComplete: 'Mark as complete', markAsIncomplete: 'Mark as incomplete', - assigneeError: 'There was an error assigning this task, please try another assignee.', - genericCreateTaskFailureMessage: 'Unexpected error create task, please try again later.', + assigneeError: 'There was an error assigning this task. Please try another assignee.', + genericCreateTaskFailureMessage: 'There was an error creating this task. Please try again later.', deleteTask: 'Delete task', - deleteConfirmation: 'Are you sure that you want to delete this task?', + deleteConfirmation: 'Are you sure you want to delete this task?', }, statementPage: { title: (year, monthName) => `${monthName} ${year} statement`, - generatingPDF: "We're generating your PDF right now. Please come back later!", + generatingPDF: "We're generating your PDF right now. Please check back soon!", }, keyboardShortcutsPage: { title: 'Keyboard shortcuts', @@ -2875,7 +2883,7 @@ export default { genericErrorPage: { title: 'Uh-oh, something went wrong!', body: { - helpTextMobile: 'Please try closing and reopening the app or switching to', + helpTextMobile: 'Please close and reopen the app, or switch to', helpTextWeb: 'web.', helpTextConcierge: 'If the problem persists, reach out to', }, @@ -2888,12 +2896,12 @@ export default { qrMessage: 'Check your photos or downloads folder for a copy of your QR code. Protip: Add it to a presentation for your audience to scan and connect with you directly.', }, generalError: { - title: 'Attachment Error', - message: 'Attachment cannot be downloaded.', + title: 'Attachment error', + message: "Attachment can't be downloaded.", }, permissionError: { title: 'Storage access', - message: "Expensify can't save attachments without storage access. Tap Settings to update permissions.", + message: "Expensify can't save attachments without storage access. Tap settings to update permissions.", }, }, desktopApplicationMenu: { @@ -2949,26 +2957,26 @@ export default { }, checkForUpdatesModal: { available: { - title: 'Update Available', + title: 'Update available', message: ({isSilentUpdating}: {isSilentUpdating: boolean}) => `The new version will be available shortly.${!isSilentUpdating ? " We'll notify you when we're ready to update." : ''}`, soundsGood: 'Sounds good', }, notAvailable: { - title: 'Update Not Available', - message: 'There is no update available as of now! Check again at a later time.', + title: 'Update unavailable', + message: "There's no update available right now. Please check back later!", okay: 'Okay', }, error: { - title: 'Update Check Failed.', - message: "We couldn't look for an update. Please check again in a bit!.", + title: 'Update check failed.', + message: "We couldn't check for an update. Please try again in a bit.", }, }, report: { - genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later.', - genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later.', - genericUpdateReportFieldFailureMessage: 'Unexpected error while updating the field, please try again later.', - genericUpdateReporNameEditFailureMessage: 'Unexpected error while renaming the report, please try again later.', + genericCreateReportFailureMessage: 'Unexpected error creating this chat. Please try again later.', + genericAddCommentFailureMessage: 'Unexpected error posting the comment. Please try again later.', + genericUpdateReportFieldFailureMessage: 'Unexpected error updating the field. Please try again later.', + genericUpdateReporNameEditFailureMessage: 'Unexpected error renaming the report. Please try again later.', noActivityYet: 'No activity yet', }, chronos: { @@ -3128,8 +3136,8 @@ export default { reasonTitle: 'Why do you need a new card?', cardDamaged: 'My card was damaged', cardLostOrStolen: 'My card was lost or stolen', - confirmAddressTitle: "Please confirm the address below is where you'd like us to send your new card.", - cardDamagedInfo: 'Your new card will arrive in 2-3 business days, and your existing card will continue to work until you activate your new one.', + confirmAddressTitle: 'Please confirm the mailing address for your new card.', + cardDamagedInfo: 'Your new card will arrive in 2-3 business days. Your current card will continue to work until you activate your new one.', cardLostOrStolenInfo: 'Your current card will be permanently deactivated as soon as your order is placed. Most cards arrive in a few business days.', address: 'Address', deactivateCardButton: 'Deactivate card', diff --git a/src/languages/es.ts b/src/languages/es.ts index da228096eaf1..2a11549e7355 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -264,7 +264,7 @@ export default { your: 'tu', conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.', youAppearToBeOffline: 'Parece que estás desconectado.', - thisFeatureRequiresInternet: 'Esta funciĂłn requiere una conexiĂłn a Internet activa para ser utilizada.', + thisFeatureRequiresInternet: 'Esta funciĂłn requiere una conexiĂłn a Internet activa.', attachementWillBeAvailableOnceBackOnline: 'El archivo adjunto estará disponible cuando vuelvas a estar en lĂ­nea.', areYouSure: 'ÂżEstás seguro?', verify: 'Verifique', @@ -333,12 +333,12 @@ export default { disable: 'Deshabilitar', }, connectionComplete: { - title: 'ConexiĂłn Completa', + title: 'ConexiĂłn completa', supportingText: 'Ya puedes cerrar esta página y volver a la App de Expensify.', }, location: { useCurrent: 'Usar ubicaciĂłn actual', - notFound: 'No pudimos encontrar tu ubicaciĂłn, intĂ©ntalo de nuevo o introduce una direcciĂłn manualmente.', + notFound: 'No pudimos encontrar tu ubicaciĂłn. IntĂ©ntalo de nuevo o introduce una direcciĂłn manualmente.', permissionDenied: 'Parece que has denegado el permiso a tu ubicaciĂłn.', please: 'Por favor,', allowPermission: 'habilita el permiso de ubicaciĂłn en la configuraciĂłn', @@ -349,7 +349,7 @@ export default { }, attachmentPicker: { cameraPermissionRequired: 'Permiso para acceder a la cámara', - expensifyDoesntHaveAccessToCamera: 'Expensify no puede tomar fotos sin acceso a la cámara. Haz click en ConfiguraciĂłn para actualizar los permisos.', + expensifyDoesntHaveAccessToCamera: 'Expensify no puede tomar fotos sin acceso a la cámara. Haz click en configuraciĂłn para actualizar los permisos.', attachmentError: 'Error al adjuntar archivo', errorWhileSelectingAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto. Por favor, intĂ©ntalo de nuevo.', errorWhileSelectingCorruptedAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, intĂ©ntalo con otro archivo.', @@ -421,7 +421,7 @@ export default { welcomeText: { getStarted: 'Comience a continuaciĂłn.', anotherLoginPageIsOpen: 'Otra página de inicio de sesiĂłn está abierta.', - anotherLoginPageIsOpenExplanation: 'Ha abierto la página de inicio de sesiĂłn en una pestaña separada, inicie sesiĂłn desde esa pestaña especĂ­fica.', + anotherLoginPageIsOpenExplanation: 'Ha abierto la página de inicio de sesiĂłn en una pestaña separada. Inicie sesiĂłn desde esa pestaña especĂ­fica.', welcome: '¡Bienvenido!', welcomeWithoutExclamation: 'Bienvenido', phrase2: 'El dinero habla. Y ahora que chat y pagos están en un mismo lugar, es tambiĂ©n fácil.', @@ -447,7 +447,7 @@ export default { }, samlSignIn: { welcomeSAMLEnabled: 'Continua iniciando sesiĂłn con el inicio de sesiĂłn Ăşnico:', - orContinueWithMagicCode: 'O, opcionalmente, tu empresa te permite iniciar sesiĂłn con un cĂłdigo mágico', + orContinueWithMagicCode: 'TambiĂ©n puedes iniciar sesiĂłn con un cĂłdigo mágico', useSingleSignOn: 'Usar el inicio de sesiĂłn Ăşnico', useMagicCode: 'Usar cĂłdigo mágico', launching: 'Cargando...', @@ -544,7 +544,7 @@ export default { reportTypingIndicator: { isTyping: 'está escribiendo...', areTyping: 'están escribiendo...', - multipleUsers: 'Varios usuarios', + multipleUsers: 'Varios miembros', }, reportArchiveReasons: { [CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'Esta sala de chat ha sido eliminada.', @@ -596,7 +596,7 @@ export default { takePhoto: 'Haz una foto', cameraAccess: 'Se requiere acceso a la cámara para hacer fotos de los recibos.', cameraErrorTitle: 'Error en la cámara', - cameraErrorMessage: 'Se produjo un error al hacer una foto, Por favor, intĂ©ntalo de nuevo.', + cameraErrorMessage: 'Se produjo un error al hacer una foto. Por favor, intĂ©ntalo de nuevo.', dropTitle: 'SuĂ©ltalo', dropMessage: 'Suelta tu archivo aquĂ­', flash: 'flash', @@ -652,14 +652,14 @@ export default { canceled: 'CancelĂł', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - pendingMatchWithCreditCard: 'Recibo pendiente de adjuntar con la tarjeta de crĂ©dito.', - pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con tarjeta de crĂ©dito. Marca como efectivo para ignorar y solicitar pago.', + pendingMatchWithCreditCard: 'Recibo pendiente de adjuntar con la transacciĂłn de la tarjeta', + pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacciĂłn de la tarjeta. Márcalo como efectivo para cancelar.', markAsCash: 'Marcar como efectivo', routePending: 'Ruta pendiente...', receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, fieldPending: 'Pendiente...', receiptScanning: 'Escaneando recibo...', - receiptScanInProgress: 'Escaneado de recibo en proceso.', + receiptScanInProgress: 'Escaneado de recibo en proceso', receiptScanInProgressDescription: 'Escaneado de recibo en proceso. Vuelve a comprobarlo más tarde o introduce los detalles ahora.', defaultRate: 'Tasa predeterminada', receiptMissingDetails: 'Recibo con campos vacĂ­os', @@ -668,7 +668,7 @@ export default { receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tĂş puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', receiptScanningFailed: 'El escaneo de recibo ha fallado. Introduce los detalles manualmente.', - transactionPendingDescription: 'TransacciĂłn pendiente. La transacciĂłn tarda unos dĂ­as en contabilizarse desde la fecha en que se utilizĂł la tarjeta.', + transactionPendingDescription: 'TransacciĂłn pendiente. Puede tardar unos dĂ­as en contabilizarse.', expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' @@ -683,7 +683,7 @@ export default { settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, settleBusiness: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} como negocio` : `Pagar como empresa`), payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), - nextStep: 'Pasos Siguientes', + nextStep: 'Pasos siguientes', finished: 'Finalizado', sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`, submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, @@ -705,7 +705,7 @@ export default { waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `iniciĂł el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}cancelĂł el pago de ${amount}.`, canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => - `cancelĂł el pago ${amount}, porque ${submitterDisplayName} no habilitĂł tu billetera Expensify en un plazo de 30 dĂ­as.`, + `cancelĂł el pago ${amount}, porque ${submitterDisplayName} no habilitĂł tu Billetera Expensify en un plazo de 30 dĂ­as.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadiĂł una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer ? `${payer} ` : ''}pagĂł ${amount} de otra forma`, @@ -732,12 +732,12 @@ export default { invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`, invalidSplit: 'La suma de las partes debe ser igual al importe total.', invalidSplitParticipants: 'Introduce un importe superior a cero para al menos dos participantes.', - other: 'Error inesperado, por favor, intĂ©ntalo más tarde.', - genericHoldExpenseFailureMessage: 'Error inesperado al bloquear el gasto, por favor, intĂ©ntalo de nuevo más tarde.', - genericUnholdExpenseFailureMessage: 'Error inesperado al desbloquear el gasto, por favor, intĂ©ntalo de nuevo más tarde.', + other: 'Error inesperado. Por favor, intĂ©ntalo más tarde.', + genericHoldExpenseFailureMessage: 'Error inesperado al bloquear el gasto. Por favor, intĂ©ntalo de nuevo más tarde.', + genericUnholdExpenseFailureMessage: 'Error inesperado al desbloquear el gasto. Por favor, intĂ©ntalo de nuevo más tarde.', genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, intĂ©ntalo más tarde.', - genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura, intĂ©ntalo de nuevo más tarde.', - receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Vuelve a intentarlo más tarde.', + genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura. Por favor, intĂ©ntalo de nuevo más tarde.', + receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Por favor, vuelve a intentarlo más tarde.', // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: 'El recibo no se subiĂł. ', // eslint-disable-next-line rulesdir/use-periods-for-error-messages @@ -748,11 +748,11 @@ export default { genericSmartscanFailureMessage: 'La transacciĂłn tiene campos vacĂ­os.', duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados.', atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes.', - splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un Ăşnico espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selecciĂłn.', + splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un Ăşnico espacio de trabajo o con miembros individuales. Por favor, actualiza tu selecciĂłn.', invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `iniciĂł el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`, - enableWallet: 'Habilitar Billetera', + enableWallet: 'Habilitar billetera', holdExpense: 'Bloquear gasto', unholdExpense: 'Desbloquear gasto', heldExpense: 'bloqueĂł este gasto', @@ -761,7 +761,7 @@ export default { reason: 'RazĂłn', holdReasonRequired: 'Se requiere una razĂłn para bloquear.', expenseOnHold: 'Este gasto está bloqueado. Revisa los comentarios para saber como proceder.', - expensesOnHold: 'Todos los gastos quedaron bloqueado. Revisa los comentarios para saber como proceder.', + expensesOnHold: 'Todos los gastos quedaron bloqueados. Revisa los comentarios para saber como proceder.', expenseDuplicate: 'Esta solicitud tiene los mismos detalles que otra. Revisa los duplicados para eliminar el bloqueo.', reviewDuplicates: 'Revisar duplicados', keepAll: 'Mantener todos', @@ -806,7 +806,7 @@ export default { editImage: 'Editar foto', viewPhoto: 'Ver foto', imageUploadFailed: 'Error al cargar la imagen', - deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de tu espacio de trabajo.', + deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de tu espacio de trabajo', sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`, resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Por favor, elige una imagen más grande que ${minHeightInPx}x${minWidthInPx} pĂ­xeles y más pequeña que ${maxHeightInPx}x${maxWidthInPx} pĂ­xeles.`, @@ -858,7 +858,7 @@ export default { enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Por favor, introduce el cĂłdigo mágico enviado a ${contactMethod}`, setAsDefault: 'Establecer como predeterminado', yourDefaultContactMethod: - 'Este es tu mĂ©todo de contacto predeterminado. No podrás eliminarlo hasta que añadas otro mĂ©todo de contacto y lo marques como predeterminado pulsando "Establecer como predeterminado".', + 'Este es tu mĂ©todo de contacto predeterminado. Antes de poder eliminarlo, tendrás que elegir otro mĂ©todo de contacto y haz clic en "Establecer como predeterminado".', removeContactMethod: 'Eliminar mĂ©todo de contacto', removeAreYouSure: 'ÂżEstás seguro de que quieres eliminar este mĂ©todo de contacto? Esta acciĂłn no se puede deshacer.', failedNewContact: 'Hubo un error al añadir este mĂ©todo de contacto.', @@ -998,7 +998,7 @@ export default { changingYourPasswordPrompt: 'El cambio de contraseña va a afectar tanto a la cuenta de Expensify.com como la de Nuevo Expensify.', currentPassword: 'Contraseña actual', newPassword: 'Nueva contraseña', - newPasswordPrompt: 'La nueva contraseña debe ser diferente de la antigua, tener al menos 8 caracteres, 1 letra mayĂşscula, 1 letra minĂşscula y 1 nĂşmero.', + newPasswordPrompt: 'La nueva contraseña debe ser diferente de la antigua y contener al menos 8 caracteres, 1 letra mayĂşscula, 1 letra minĂşscula y 1 nĂşmero.', }, twoFactorAuth: { headerTitle: 'AutenticaciĂłn de dos factores', @@ -1053,6 +1053,18 @@ export default { genericFailureMessage: 'Las notas privadas no han podido ser guardadas.', }, }, + billingCurrency: { + error: { + securityCode: 'Por favor, introduce un cĂłdigo de seguridad válido.', + }, + securityCode: 'CĂłdigo de seguridad', + changePaymentCurrency: 'Cambiar moneda de facturaciĂłn', + changeBillingCurrency: 'Cambiar la moneda de pago', + paymentCurrency: 'Moneda de pago', + note: 'Nota: Cambiar tu moneda de pago puede afectar cuánto pagarás por Expensify. Consulta nuestra', + noteLink: 'página de precios', + noteDetails: 'para conocer todos los detalles.', + }, addDebitCardPage: { addADebitCard: 'Añadir una tarjeta de dĂ©bito', nameOnCard: 'Nombre en la tarjeta', @@ -1072,7 +1084,7 @@ export default { addressStreet: 'Por favor, introduce una direcciĂłn de facturaciĂłn válida que no sea un apartado postal.', addressState: 'Por favor, selecciona un estado.', addressCity: 'Por favor, introduce una ciudad.', - genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Vuelva a intentarlo.', + genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.', password: 'Por favor, introduce tu contraseña de Expensify.', }, }, @@ -1095,7 +1107,7 @@ export default { addressStreet: 'Por favor, introduce una direcciĂłn de facturaciĂłn válida que no sea un apartado postal.', addressState: 'Por favor, selecciona un estado.', addressCity: 'Por favor, introduce una ciudad.', - genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Vuelve a intentarlo.', + genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Por favor, vuelva a intentarlo.', password: 'Por favor, introduce tu contraseña de Expensify.', }, }, @@ -1120,20 +1132,20 @@ export default { expensifyWallet: 'Billetera Expensify', sendAndReceiveMoney: 'EnvĂ­a y recibe dinero desde tu Billetera Expensify.', enableWalletToSendAndReceiveMoney: 'Habilita tu Billetera Expensify para comenzar a enviar y recibir dinero con amigos', - enableWallet: 'Habilitar Billetera', + enableWallet: 'Habilitar billetera', bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para enviar y recibir pagos directamente en la aplicaciĂłn.', addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del espacio de trabajo para gestionar los gastos de la empresa.', expensifyCard: 'Tarjeta Expensify', - walletActivationPending: 'Estamos revisando tu informaciĂłn, por favor vuelve en unos minutos.', + walletActivationPending: 'Estamos revisando tu informaciĂłn. Por favor, vuelve en unos minutos.', walletActivationFailed: 'Lamentablemente, no podemos activar tu billetera en este momento. Chatea con Concierge para obtener más ayuda.', - addYourBankAccount: 'Añadir tu cuenta bancaria.', + addYourBankAccount: 'Añadir tu cuenta bancaria', addBankAccountBody: 'Conectemos tu cuenta bancaria a Expensify para que sea más fácil que nunca enviar y recibir pagos directamente en la aplicaciĂłn.', - chooseYourBankAccount: 'Elige tu cuenta bancaria.', + chooseYourBankAccount: 'Elige tu cuenta bancaria', chooseAccountBody: 'AsegĂşrese de elegir el adecuado.', - confirmYourBankAccount: 'Confirma tu cuenta bancaria.', + confirmYourBankAccount: 'Confirma tu cuenta bancaria', }, cardPage: { expensifyCard: 'Tarjeta Expensify', @@ -1296,7 +1308,7 @@ export default { priorityModePage: { priorityMode: 'Modo prioridad', explainerText: - 'Elige si deseas mostrar por defecto todos los chats ordenados desde el más reciente y con los elementos anclados en la parte superior, o elige el modo #concentraciĂłn, con los elementos no leĂ­dos anclados en la parte superior y ordenados alfabĂ©ticamente.', + 'Elige #concentraciĂłn si deseas enfocarte sĂłlo en los chats no leĂ­dos y en los anclados, o mostrarlo todo con los chats más recientes y los anclados en la parte superior.', priorityModes: { default: { label: 'Más recientes', @@ -1322,7 +1334,7 @@ export default { groupChat: { groupMembersListTitle: 'Directorio de los miembros del grupo.', lastMemberTitle: '¡AtenciĂłn!', - lastMemberWarning: 'Ya que eres la Ăşltima persona aquĂ­, si te vas, este chat quedará inaccesible para todos los usuarios. ÂżEstás seguro de que quieres salir del chat?', + lastMemberWarning: 'Ya que eres la Ăşltima persona aquĂ­, si te vas, este chat quedará inaccesible para todos los miembros. ÂżEstás seguro de que quieres salir del chat?', defaultReportName: ({displayName}: {displayName: string}) => `Chat de group de ${displayName}`, }, languagePage: { @@ -1647,7 +1659,7 @@ export default { userIsAlreadyMember: ({login, name}: UserIsAlreadyMemberParams) => `${login} ya es miembro de ${name}`, }, onfidoStep: { - acceptTerms: 'Al continuar con la solicitud para activar tu billetera Expensify, confirma que ha leĂ­do, comprende y acepta ', + acceptTerms: 'Al continuar con la solicitud para activar tu Billetera Expensify, confirma que ha leĂ­do, comprende y acepta ', facialScan: 'PolĂ­tica y lanzamiento de la exploraciĂłn facial de Onfido', tryAgain: 'Intentar otra vez', verifyIdentity: 'Verificar identidad', @@ -1669,7 +1681,7 @@ export default { }, additionalDetailsStep: { headerTitle: 'Detalles adicionales', - helpText: 'Necesitamos confirmar la siguiente informaciĂłn antes de que puedas enviar y recibir dinero desde tu Billetera.', + helpText: 'Necesitamos confirmar la siguiente informaciĂłn antes de que puedas enviar y recibir dinero desde tu billetera.', helpTextIdologyQuestions: 'Tenemos que preguntarte unas preguntas más para terminar de verificar tu identidad', helpLink: 'ObtĂ©n más informaciĂłn sobre por quĂ© necesitamos esto.', legalFirstNameLabel: 'Primer nombre legal', @@ -1680,7 +1692,7 @@ export default { needSSNFull9: 'Estamos teniendo problemas para verificar tu nĂşmero de seguridad social. Introduce los 9 dĂ­gitos del nĂşmero de seguridad social.', weCouldNotVerify: 'No se pudo verificar', pleaseFixIt: 'Corrige esta informaciĂłn antes de continuar.', - failedKYCTextBefore: 'No se ha podido verificar correctamente tu identidad. Vuelve a intentarlo más tarde y comunicate con ', + failedKYCTextBefore: 'No se ha podido verificar correctamente tu identidad. Vuelve a intentarlo más tarde o comunicate con ', failedKYCTextAfter: ' si tienes alguna pregunta.', }, termsStep: { @@ -1700,7 +1712,7 @@ export default { checkTheBoxes: 'Por favor, marca las siguientes casillas.', agreeToTerms: 'Debes aceptar los tĂ©rminos y condiciones para continuar.', shortTermsForm: { - expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La billetera Expensify es emitida por ${walletProgram}.`, + expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La Billetera Expensify es emitida por ${walletProgram}.`, perPurchase: 'Por compra', atmWithdrawal: 'Retiro en cajeros automáticos', cashReload: 'Recarga de efectivo', @@ -1720,7 +1732,7 @@ export default { electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(mĂ­nimo ${amount})`, }, longTermsForm: { - listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify', + listOfAllFees: 'Una lista de todas las tarifas de la Billetera Expensify', typeOfFeeHeader: 'Tipo de tarifa', feeAmountHeader: 'Importe de la tarifa', moreDetailsHeader: 'Más detalles', @@ -1733,11 +1745,11 @@ export default { sendingFundsTitle: 'Enviar fondos a otro titular de cuenta', sendingFundsDetails: 'No se aplica ningĂşn cargo por enviar fondos a otro titular de cuenta utilizando tu saldo cuenta bancaria o tarjeta de dĂ©bito', electronicFundsStandardDetails: - 'No hay cargo por transferir fondos desde tu billetera Expensify ' + + 'No hay cargo por transferir fondos desde tu Billetera Expensify ' + 'a tu cuenta bancaria utilizando la opciĂłn estándar. Esta transferencia generalmente se completa en' + '1-3 dĂ­as laborables.', electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => - 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + 'Hay una tarifa para transferir fondos desde tu Billetera Expensify a ' + 'la tarjeta de dĂ©bito vinculada utilizando la opciĂłn de transferencia instantánea. Esta transferencia ' + `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` + `transferencia (con una tarifa mĂ­nima de ${amount}). `, @@ -1760,7 +1772,7 @@ export default { activateStep: { headerTitle: 'Habilitar pagos', activatedTitle: '¡Billetera activada!', - activatedMessage: 'Felicidades, tu Billetera está configurada y lista para hacer pagos.', + activatedMessage: 'Felicidades, tu billetera está configurada y lista para hacer pagos.', checkBackLaterTitle: 'Un momento...', checkBackLaterMessage: 'TodavĂ­a estamos revisando tu informaciĂłn. Por favor, vuelve más tarde.', continueToPayment: 'Continuar al pago', @@ -1796,15 +1808,15 @@ export default { }, personalInfoStep: { personalInfo: 'InformaciĂłn Personal', - enterYourLegalFirstAndLast: 'Introduce tu nombre y apellidos', + enterYourLegalFirstAndLast: 'ÂżCuál es tu nombre legal?', legalFirstName: 'Nombre', legalLastName: 'Apellidos', legalName: 'Nombre legal', - enterYourDateOfBirth: 'Introduce tu fecha de nacimiento', - enterTheLast4: 'Introduce los Ăşltimos 4 dĂ­gitos de tu nĂşmero de la seguridad social', - dontWorry: 'No te preocupes, no hacemos ninguna verificaciĂłn de crĂ©dito', + enterYourDateOfBirth: 'ÂżCuál es tu fecha de nacimiento?', + enterTheLast4: 'ÂżCuáles son los Ăşltimos 4 dĂ­gitos de tu nĂşmero de la seguridad social?', + dontWorry: 'No te preocupes, no hacemos verificaciones de crĂ©dito personales.', last4SSN: 'Ăšltimos 4 dĂ­gitos de tu nĂşmero de la seguridad social', - enterYourAddress: 'Introduce tu direcciĂłn', + enterYourAddress: 'ÂżCuál es tu direcciĂłn?', address: 'DirecciĂłn', letsDoubleCheck: 'Revisemos que todo estĂ© bien', byAddingThisBankAccount: 'Añadiendo esta cuenta bancaria, confirmas que has leĂ­do, entendido y aceptado', @@ -1819,16 +1831,16 @@ export default { }, businessInfoStep: { businessInfo: 'InformaciĂłn de la empresa', - enterTheNameOfYourBusiness: 'Introduce el nombre de tu empresa.', + enterTheNameOfYourBusiness: 'ÂżCuál es el nombre de tu empresa?', businessName: 'Nombre de la empresa', - enterYourCompanysTaxIdNumber: 'Introduce el nĂşmero de identificaciĂłn fiscal.', + enterYourCompanysTaxIdNumber: 'ÂżCuál es el nĂşmero de identificaciĂłn fiscal?', taxIDNumber: 'NĂşmero de identificaciĂłn fiscal', taxIDNumberPlaceholder: '9 dĂ­gitos', - enterYourCompanysWebsite: 'Introduce la página web de tu empresa.', + enterYourCompanysWebsite: 'ÂżCuál es la página web de tu empresa?', companyWebsite: 'Página web de la empresa', - enterYourCompanysPhoneNumber: 'Introduce el nĂşmero de telĂ©fono de tu empresa.', - enterYourCompanysAddress: 'Introduce la direcciĂłn de tu empresa.', - selectYourCompanysType: 'Selecciona el tipo de empresa.', + enterYourCompanysPhoneNumber: 'ÂżCuál es el nĂşmero de telĂ©fono de tu empresa?', + enterYourCompanysAddress: 'ÂżCuál es la direcciĂłn de tu empresa?', + selectYourCompanysType: 'ÂżCuál es el tipo de empresa?', companyType: 'Tipo de empresa', incorporationType: { LLC: 'SRL', @@ -1838,11 +1850,11 @@ export default { SOLE_PROPRIETORSHIP: 'Empresa individual', OTHER: 'Otros', }, - selectYourCompanysIncorporationDate: 'Selecciona la fecha de constituciĂłn de la empresa.', + selectYourCompanysIncorporationDate: 'ÂżCuál es la fecha de constituciĂłn de la empresa?', incorporationDate: 'Fecha de constituciĂłn', incorporationDatePlaceholder: 'Fecha de inicio (yyyy-mm-dd)', incorporationState: 'Estado en el que se constituyĂł', - pleaseSelectTheStateYourCompanyWasIncorporatedIn: 'Selecciona el estado en el que se constituyĂł la empresa.', + pleaseSelectTheStateYourCompanyWasIncorporatedIn: 'ÂżCuál es el estado en el que se constituyĂł la empresa?', letsDoubleCheck: 'Verifiquemos que todo estĂ© correcto', companyAddress: 'DirecciĂłn de la empresa', listOfRestrictedBusinesses: 'lista de negocios restringidos', @@ -1854,14 +1866,14 @@ export default { areThereMoreIndividualsWhoOwn25percent: 'ÂżHay más personas que posean el 25% o más de', regulationRequiresUsToVerifyTheIdentity: 'La ley nos exige verificar la identidad de cualquier persona que posea más del 25% de la empresa.', companyOwner: 'Dueño de la empresa', - enterLegalFirstAndLastName: 'Introduce el nombre y apellidos legales del dueño.', + enterLegalFirstAndLastName: 'ÂżCuál es el nombre legal del dueño?', legalFirstName: 'Nombre legal', legalLastName: 'Apellidos legales', - enterTheDateOfBirthOfTheOwner: 'Introduce la fecha de nacimiento del dueño.', - enterTheLast4: 'Introduce los Ăşltimos 4 dĂ­gitos del nĂşmero de la seguridad social del dueño.', + enterTheDateOfBirthOfTheOwner: 'ÂżCuál es la fecha de nacimiento del dueño?', + enterTheLast4: 'ÂżCuáles son los Ăşltimos 4 dĂ­gitos del nĂşmero de la seguridad social del dueño?', last4SSN: 'Ăšltimos 4 dĂ­gitos del nĂşmero de la seguridad social', dontWorry: 'No te preocupes, ¡no realizamos verificaciones de crĂ©dito personales!', - enterTheOwnersAddress: 'Introduce la direcciĂłn del dueño.', + enterTheOwnersAddress: 'ÂżCuál es la direcciĂłn del dueño?', letsDoubleCheck: 'Vamos a verificar que todo estĂ© correcto.', legalName: 'Nombre legal', address: 'DirecciĂłn', @@ -1872,17 +1884,15 @@ export default { headerTitle: 'Validar cuenta bancaria', buttonText: 'Finalizar configuraciĂłn', maxAttemptsReached: 'Se ha inhabilitado la validaciĂłn de esta cuenta bancaria debido a demasiados intentos incorrectos.', - description: - 'Uno o dos dĂ­as despuĂ©s de añadir tu cuenta a Expensify, te enviaremos tres (3) transacciones a tu cuenta. Tienen un nombre de comerciante similar a "Expensify, Inc. Validation".', + description: 'Enviaremos tres (3) pequeñas transacciones a tu cuenta bancaria a nombre de "Expensify, Inc. Validation" dentro de los prĂłximos 1-2 dĂ­as laborables.', descriptionCTA: 'Introduce el importe de cada transacciĂłn en los campos siguientes. Ejemplo: 1.51.', reviewingInfo: '¡Gracias! Estamos revisando tu informaciĂłn y nos comunicaremos contigo en breve. Consulta el chat con Concierge ', forNextStep: ' para conocer los prĂłximos pasos para terminar de configurar tu cuenta bancaria.', letsChatCTA: 'SĂ­, vamos a chatear', - letsChatText: 'Gracias. Necesitamos tu ayuda para verificar la informaciĂłn, pero podemos hacerlo rápidamente a travĂ©s del chat. ÂżEstás listo?', + letsChatText: '¡Ya casi estamos! Necesitamos tu ayuda para verificar unos Ăşltimos datos a travĂ©s del chat. ÂżEstás listo?', letsChatTitle: '¡Vamos a chatear!', enable2FATitle: 'Evita fraudes, activa la autenticaciĂłn de dos factores!', - enable2FAText: - 'Tu seguridad es importante para nosotros. Por favor, configura ahora la autenticaciĂłn de dos factores. Eso nos permitirá disputar las transacciones de la Tarjeta Expensify y reducirá tu riesgo de fraude.', + enable2FAText: 'Tu seguridad es importante para nosotros. Por favor, configura ahora la autenticaciĂłn de dos factores para añadir una capa adicional de protecciĂłn a tu cuenta.', secureYourAccount: 'Asegura tu cuenta', }, beneficialOwnersStep: { @@ -1904,7 +1914,7 @@ export default { completeVerification: 'Completar la verificaciĂłn', confirmAgreements: 'Por favor, confirma los acuerdos siguientes.', certifyTrueAndAccurate: 'Certifico que la informaciĂłn dada es verdadera y precisa', - certifyTrueAndAccurateError: 'Debe certificar que la informaciĂłn es verdadera y precisa', + certifyTrueAndAccurateError: 'Por favor, certifica que la informaciĂłn es verdadera y exacta', isAuthorizedToUseBankAccount: 'Estoy autorizado para usar la cuenta bancaria de mi empresa para gastos de empresa', isAuthorizedToUseBankAccountError: 'Debes ser el responsable oficial con autorizaciĂłn para operar la cuenta bancaria de la empresa.', termsAndConditions: 'TĂ©rminos y Condiciones', @@ -1916,17 +1926,15 @@ export default { validateButtonText: 'Validar', validationInputLabel: 'TransacciĂłn', maxAttemptsReached: 'La validaciĂłn de esta cuenta bancaria se ha desactivado debido a demasiados intentos incorrectos.', - description: - 'Un dĂ­a o dos despuĂ©s de añadir tu cuenta a Expensify, te enviaremos tres (3) transacciones a tu cuenta. Tienen un nombre de comerciante similar a "Expensify, Inc. Validation".', + description: 'Enviaremos tres (3) pequeñas transacciones a tu cuenta bancaria a nombre de "Expensify, Inc. Validation" dentro de los prĂłximos 1-2 dĂ­as laborables.', descriptionCTA: 'Introduce el importe de cada transacciĂłn en los campos siguientes. Ejemplo: 1.51.', reviewingInfo: '¡Gracias! Estamos revisando tu informaciĂłn y nos comunicaremos contigo en breve. Consulta el chat con Concierge ', forNextSteps: ' para conocer los prĂłximos pasos para terminar de configurar tu cuenta bancaria.', letsChatCTA: 'SĂ­, vamos a chatear', - letsChatText: 'Gracias. Necesitamos tu ayuda para verificar la informaciĂłn, pero podemos resolverlo rápidamente a travĂ©s del chat. ÂżEstás Listo?', + letsChatText: '¡Ya casi estamos! Necesitamos tu ayuda para verificar unos Ăşltimos datos a travĂ©s del chat. ÂżEstás listo?', letsChatTitle: '¡Vamos a chatear!', enable2FATitle: '¡Evita fraudes, activa la autenticaciĂłn de dos factores!', - enable2FAText: - 'Tu seguridad es importante para nosotros. Por favor, configura ahora la autenticaciĂłn de dos factores. Eso nos permitirá disputar las transacciones de la Tarjeta Expensify y reducirá tu riesgo de fraude.', + enable2FAText: 'Tu seguridad es importante para nosotros. Por favor, configura ahora la autenticaciĂłn de dos factores para añadir una capa adicional de protecciĂłn a tu cuenta.', secureYourAccount: 'Asegura tu cuenta', }, reimbursementAccountLoadingAnimation: { @@ -1999,8 +2007,8 @@ export default { settlementFrequency: 'Frecuencia de liquidaciĂłn', deleteConfirmation: 'ÂżEstás seguro de que quieres eliminar este espacio de trabajo?', unavailable: 'Espacio de trabajo no disponible', - memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botĂłn Invitar que está arriba.', - notAuthorized: `No tienes acceso a esta página. ÂżEstás tratando de unirte al espacio de trabajo? ComunĂ­cate con el propietario de este espacio de trabajo para que pueda añadirte como miembro. ÂżNecesitas algo más? ComunĂ­cate con ${CONST.EMAIL.CONCIERGE}`, + memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botĂłn invitar que está arriba.', + notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ÂżNecesitas algo más? ComunĂ­cate con ${CONST.EMAIL.CONCIERGE}`, goToRoom: ({roomName}: GoToRoomParams) => `Ir a la sala ${roomName}`, workspaceName: 'Nombre del espacio de trabajo', workspaceOwner: 'Dueño', @@ -2060,7 +2068,7 @@ export default { }, receivable: 'Cuentas por cobrar', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. archive: 'Archivo de cuentas por cobrar', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. - exportInvoicesDescription: 'Las facturas se exportarán a esta cuenta en QuickBooks Online.', + exportInvoicesDescription: 'Usa esta cuenta al exportar facturas a QuickBooks Online.', exportCompanyCardsDescription: 'Establece cĂłmo se exportan las compras con tarjeta de empresa a QuickBooks Online.', account: 'Cuenta', accountDescription: 'Elige dĂłnde contabilizar las compensaciones de entradas a los asientos contables.', @@ -2187,25 +2195,25 @@ export default { }, exportDate: { label: 'Fecha de exportaciĂłn', - description: 'Usa esta fecha al exportar informe a Xero.', + description: 'Usa esta fecha al exportar facturas de compra a Xero.', values: { [CONST.XERO_EXPORT_DATE.LAST_EXPENSE]: { label: 'Fecha del Ăşltimo gasto', - description: 'Fecha del gasto mas reciente en el informe', + description: 'Fecha del gasto mas reciente en el informe.', }, [CONST.XERO_EXPORT_DATE.REPORT_EXPORTED]: { label: 'Fecha de exportaciĂłn', - description: 'Fecha de exportaciĂłn del informe a Xero', + description: 'Fecha de exportaciĂłn del informe a Xero.', }, [CONST.XERO_EXPORT_DATE.REPORT_SUBMITTED]: { label: 'Fecha de envĂ­o', - description: 'Fecha en la que el informe se enviĂł para su aprobaciĂłn', + description: 'Fecha en la que el informe se enviĂł para su aprobaciĂłn.', }, }, }, invoiceStatus: { label: 'Estado de la factura de compra', - description: 'Elige un estado para las facturas de compra exportadas a Xero.', + description: 'Usa este estado al exportar facturas de compra a Xero.', values: { [CONST.XERO_CONFIG.INVOICE_STATUS.DRAFT]: 'Borrador', [CONST.XERO_CONFIG.INVOICE_STATUS.AWAITING_APPROVAL]: 'Pendiente de aprobaciĂłn', @@ -2339,7 +2347,7 @@ export default { deleteFailureMessage: 'Se ha producido un error al intentar eliminar la etiqueta. Por favor, intĂ©ntalo más tarde.', tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.', existingTagError: 'Ya existe una etiqueta con este nombre.', - genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, intĂ©ntelo nuevamente.', + genericFailureMessage: 'Se produjo un error al actualizar la etiqueta. Por favor, intĂ©ntelo nuevamente.', importedFromAccountingSoftware: 'Etiquetas importadas desde', }, taxes: { @@ -2394,7 +2402,7 @@ export default { getTheExpensifyCardAndMore: 'Consigue la Tarjeta Expensify y más', }, people: { - genericFailureMessage: 'Se ha producido un error al intentar eliminar a un usuario del espacio de trabajo. Por favor, intĂ©ntalo más tarde.', + genericFailureMessage: 'Se ha producido un error al intentar eliminar a un miembro del espacio de trabajo. Por favor, intĂ©ntalo más tarde.', removeMembersPrompt: 'ÂżEstás seguro de que deseas eliminar a estos miembros?', removeMembersTitle: 'Eliminar miembros', removeMemberButtonTitle: 'Quitar del espacio de trabajo', @@ -2410,7 +2418,7 @@ export default { cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.', genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, - addedWithPrimary: 'Se agregaron algunos usuarios con sus nombres de usuario principales.', + addedWithPrimary: 'Se agregaron algunos miembros con sus nombres de usuario principales.', invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.', }, @@ -2455,14 +2463,14 @@ export default { taxes: 'Impuestos', imported: 'Importado', notImported: 'No importado', - importAsCategory: 'Importado, mostrado as categorĂ­a', + importAsCategory: 'Importado como categorĂ­as', importTypes: { [CONST.INTEGRATION_ENTITY_MAP_TYPES.IMPORTED]: 'Importado', - [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: 'Importado, mostrado como etiqueta', + [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: 'Importado como etiquetas', [CONST.INTEGRATION_ENTITY_MAP_TYPES.DEFAULT]: 'Importado', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NOT_IMPORTED]: 'No importado', [CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE]: 'No importado', - [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Importado, mostrado como campo de informe', + [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Importado como campos de informe', }, disconnectPrompt: (integrationToConnect?: ConnectionName, currentIntegration?: ConnectionName): string => { switch (integrationToConnect) { @@ -2585,12 +2593,12 @@ export default { card: { header: 'Desbloquea Tarjetas Expensify gratis', headerWithEcard: '¡Tus tarjetas están listas!', - noVBACopy: 'Conecta una cuenta bancaria para emitir tarjetas Expensify a los miembros de tu espacio de trabajo y accede a estos increĂ­bles beneficios y más:', + noVBACopy: 'Conecta una cuenta bancaria para emitir Tarjetas Expensify a los miembros de tu espacio de trabajo y acceder a estos increĂ­bles beneficios y más:', VBANoECardCopy: - 'Añade tu correo electrĂłnico de trabajo para emitir Tarjetas Expensify ilimitadas para los miembros de tu espacio de trabajo y acceder a todas estas increĂ­bles ventajas:', + 'Añade tu correo electrĂłnico de trabajo para emitir Tarjetas Expensify ilimitadas a los miembros de tu espacio de trabajo y acceder a todas estas increĂ­bles ventajas:', VBAWithECardCopy: 'Acceda a estos increĂ­bles beneficios y más:', benefit1: 'DevoluciĂłn de dinero en cada compra en Estados Unidos', - benefit2: 'Tarjetas digitales y fĂ­sicas', + benefit2: 'Tarjetas virtuales y fĂ­sicas ilimitadas', benefit3: 'Sin responsabilidad personal', benefit4: 'LĂ­mites personalizables', addWorkEmail: 'Añadir correo electrĂłnico de trabajo', @@ -2608,7 +2616,7 @@ export default { trackDistanceChooseUnit: 'Elige una unidad predeterminada de medida.', unlockNextDayReimbursements: 'Desbloquea reembolsos diarios', captureNoVBACopyBeforeEmail: 'Pide a los miembros de tu espacio de trabajo que envĂ­en recibos a ', - captureNoVBACopyAfterEmail: ' y descarga la App de Expensify para controlar tus gastos en efectivo sobre la marcha.', + captureNoVBACopyAfterEmail: ' y descarga la app de Expensify para controlar tus gastos en efectivo sobre la marcha.', unlockNoVBACopy: 'Conecta una cuenta bancaria para reembolsar online a los miembros de tu espacio de trabajo.', fastReimbursementsVBACopy: '¡Todo listo para reembolsar recibos desde tu cuenta bancaria!', updateCustomUnitError: 'Los cambios no han podido ser guardados. El espacio de trabajo ha sido modificado mientras estabas desconectado. Por favor, intĂ©ntalo de nuevo.', @@ -2630,7 +2638,7 @@ export default { invoiceFirstSectionCopy: 'EnvĂ­a facturas detalladas y profesionales directamente a tus clientes desde la app de Expensify.', viewAllInvoices: 'Ver facturas emitidas', unlockOnlineInvoiceCollection: 'Desbloquea el cobro de facturas online', - unlockNoVBACopy: 'Conecta tu cuenta bancaria para recibir pagos online de facturas - por transferencia o con tarjeta - directamente en tu cuenta.', + unlockNoVBACopy: 'Conecta tu cuenta bancaria para recibir pagos de facturas online por transferencia o con tarjeta.', moneyBackInAFlash: '¡Tu dinero de vuelta en un momento!', unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', @@ -2656,10 +2664,10 @@ export default { member: 'Invitar miembros', members: 'Invitar miembros', invitePeople: 'Invitar nuevos miembros', - genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', + genericFailureMessage: 'Se produjo un error al invitar al miembro al espacio de trabajo. Vuelva a intentarlo..', pleaseEnterValidLogin: `AsegĂşrese de que el correo electrĂłnico o el nĂşmero de telĂ©fono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, - user: 'usuario', - users: 'usuarios', + user: 'miembro', + users: 'miembros', invited: 'invitĂł', removed: 'eliminĂł', leftWorkspace: 'saliĂł del espacio de trabajo', @@ -2671,7 +2679,7 @@ export default { inviteMessagePrompt: 'Añadir un mensaje para hacer tu invitaciĂłn destacar', personalMessagePrompt: 'Mensaje', inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar.', - genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', + genericFailureMessage: 'Se produjo un error al invitar al miembro al espacio de trabajo. Por favor, vuelva a intentarlo..', }, distanceRates: { oopsNotSoFast: 'Ups! No tan rápido...', @@ -2708,14 +2716,13 @@ export default { }, bankAccount: { continueWithSetup: 'Continuar con la configuraciĂłn', - youreAlmostDone: - 'Casi has acabado de configurar tu cuenta bancaria, que te permitirá emitir tarjetas corporativas, reembolsar gastos y cobrar pagar facturas, todo desde la misma cuenta bancaria.', + youreAlmostDone: 'Casi has acabado de configurar tu cuenta bancaria, que te permitirá emitir tarjetas corporativas, reembolsar gastos y cobrar pagar facturas.', streamlinePayments: 'Optimiza pagos', oneMoreThing: '¡Una cosa más!', allSet: '¡Todo listo!', accountDescriptionNoCards: - 'Esta cuenta bancaria se utilizará para reembolsar gastos y cobrar y pagar facturas, todo desde la misma cuenta.\n\nPor favor, añade un correo electrĂłnico de trabajo como tu nombre de usuario secundario para activar la Tarjeta Expensify.', - accountDescriptionWithCards: 'Esta cuenta bancaria se utilizará para emitir tarjetas corporativas, reembolsar gastos y cobrar y pagar facturas, todo desde la misma cuenta.', + 'Esta cuenta bancaria se utilizará para reembolsar gastos y cobrar y pagar facturas.\n\nPor favor, añade un correo electrĂłnico de trabajo como tu nombre de usuario secundario para activar la Tarjeta Expensify.', + accountDescriptionWithCards: 'Esta cuenta bancaria se utilizará para emitir tarjetas corporativas, reembolsar gastos y cobrar y pagar facturas.', addWorkEmail: 'Añadir correo electrĂłnico de trabajo', letsFinishInChat: '¡Continuemos en el chat!', almostDone: '¡Casi listo!', @@ -2757,7 +2764,7 @@ export default { subscriptionTitle: 'Asumir la suscripciĂłn anual', subscriptionButtonText: 'Transferir suscripciĂłn', subscriptionText: ({usersCount, finalCount}) => - `Al hacerse cargo de este espacio de trabajo se fusionará tu suscripciĂłn anual asociada con tu suscripciĂłn actual. Esto aumentará el tamaño de tu suscripciĂłn en ${usersCount} usuarios, lo que hará que tu nuevo tamaño de suscripciĂłn sea ${finalCount}. ÂżTe gustaria continuar?`, + `Al hacerse cargo de este espacio de trabajo se fusionará tu suscripciĂłn anual asociada con tu suscripciĂłn actual. Esto aumentará el tamaño de tu suscripciĂłn en ${usersCount} miembros, lo que hará que tu nuevo tamaño de suscripciĂłn sea ${finalCount}. ÂżTe gustaria continuar?`, duplicateSubscriptionTitle: 'Alerta de suscripciĂłn duplicada', duplicateSubscriptionButtonText: 'Continuar', duplicateSubscriptionText: ({email, workspaceName}) => @@ -2834,7 +2841,7 @@ export default { roomRenamedTo: ({newName}: RoomRenamedToParams) => `Sala renombrada a ${newName}`, social: 'social', selectAWorkspace: 'Seleccionar un espacio de trabajo', - growlMessageOnRenameError: 'No se ha podido cambiar el nombre del espacio de trabajo, por favor, comprueba tu conexiĂłn e intĂ©ntalo de nuevo.', + growlMessageOnRenameError: 'No se ha podido cambiar el nombre del espacio de trabajo. Por favor, comprueba tu conexiĂłn e intĂ©ntalo de nuevo.', visibilityOptions: { restricted: 'Espacio de trabajo', // the translation for "restricted" visibility is actually workspace. This is so we can display restricted visibility rooms as "workspace" without having to change what's stored. private: 'Privada', @@ -2844,8 +2851,8 @@ export default { }, }, roomMembersPage: { - memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro a la sala de chat, por favor, utiliza el botĂłn Invitar que está más arriba.', - notAuthorized: `No tienes acceso a esta página. ÂżEstás tratando de unirte a la sala de chat? ComunĂ­cate con el propietario de esta sala de chat para que pueda añadirte como miembro. ÂżNecesitas algo más? ComunĂ­cate con ${CONST.EMAIL.CONCIERGE}`, + memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro a la sala de chat, por favor, utiliza el botĂłn invitar que está más arriba.', + notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a esta sala, pide a un miembro de la sala que te añada. ÂżNecesitas algo más? ComunĂ­cate con ${CONST.EMAIL.CONCIERGE}`, removeMembersPrompt: 'ÂżEstás seguro de que quieres eliminar a los miembros seleccionados de la sala de chat?', error: { genericAdd: 'Hubo un problema al añadir este miembro a la sala de chat.', @@ -2865,7 +2872,7 @@ export default { task: 'Tarea', title: 'TĂ­tulo', description: 'DescripciĂłn', - assignee: 'Usuario asignado', + assignee: 'Miembro asignado', completed: 'Completada', messages: { created: ({title}: TaskCreatedActionParams) => `tarea para ${title}`, @@ -2876,8 +2883,8 @@ export default { }, markAsComplete: 'Marcar como completada', markAsIncomplete: 'Marcar como incompleta', - assigneeError: 'Hubo un error al asignar esta tarea, intĂ©ntalo con otro usuario.', - genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea, por favor, intĂ©ntalo más tarde.', + assigneeError: 'Hubo un error al asignar esta tarea. Por favor, intĂ©ntalo con otro miembro.', + genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea. Por favor, intĂ©ntalo más tarde.', deleteTask: 'Eliminar tarea', deleteConfirmation: 'ÂżEstás seguro de que quieres eliminar esta tarea?', }, @@ -2933,7 +2940,7 @@ export default { }, permissionError: { title: 'Permiso para acceder al almacenamiento', - message: 'Expensify no puede guardar los archivos adjuntos sin permiso para acceder al almacenamiento. Haz click en ConfiguraciĂłn para actualizar los permisos.', + message: 'Expensify no puede guardar los archivos adjuntos sin permiso para acceder al almacenamiento. Haz click en configuraciĂłn para actualizar los permisos.', }, }, desktopApplicationMenu: { @@ -3008,7 +3015,7 @@ export default { genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, intĂ©ntalo más tarde.', genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, intĂ©ntalo más tarde.', genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, intĂ©ntalo más tarde.', - genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Vuelva a intentarlo más tarde.', + genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Por favor, intentarlo más tarde.', noActivityYet: 'Sin actividad todavĂ­a', }, chronos: { @@ -3629,8 +3636,8 @@ export default { reasonTitle: 'ÂżPor quĂ© necesitas una tarjeta nueva?', cardDamaged: 'Mi tarjeta está dañada', cardLostOrStolen: 'He perdido o me han robado la tarjeta', - confirmAddressTitle: 'Confirma que la direcciĂłn que aparece a continuaciĂłn es a la que deseas que te enviemos tu nueva tarjeta.', - cardDamagedInfo: 'La nueva tarjeta te llegará en 2-3 dĂ­as laborables y la tarjeta actual seguirá funcionando hasta que actives la nueva.', + confirmAddressTitle: 'Por favor, confirma la direcciĂłn postal de tu nueva tarjeta.', + cardDamagedInfo: 'La nueva tarjeta te llegará en 2-3 dĂ­as laborables. La tarjeta actual seguirá funcionando hasta que actives la nueva.', cardLostOrStolenInfo: 'La tarjeta actual se desactivará permanentemente en cuanto realices el pedido. La mayorĂ­a de las tarjetas llegan en pocos dĂ­as laborables.', address: 'DirecciĂłn', deactivateCardButton: 'Desactivar tarjeta', diff --git a/src/libs/API/parameters/OpenProfileParams.ts b/src/libs/API/parameters/OpenProfileParams.ts deleted file mode 100644 index f42ea8234fc8..000000000000 --- a/src/libs/API/parameters/OpenProfileParams.ts +++ /dev/null @@ -1,5 +0,0 @@ -type OpenProfileParams = { - timezone: string; -}; - -export default OpenProfileParams; diff --git a/src/libs/API/parameters/UpdateBillingCurrencyParams.ts b/src/libs/API/parameters/UpdateBillingCurrencyParams.ts new file mode 100644 index 000000000000..957b377a2e95 --- /dev/null +++ b/src/libs/API/parameters/UpdateBillingCurrencyParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type UpdateBillingCurrencyParams = { + currency: ValueOf; + cardCVV: string; +}; + +export default UpdateBillingCurrencyParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index c43ab514b251..da4f1216016e 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -32,7 +32,6 @@ export type {default as OpenAppParams} from './OpenAppParams'; export type {default as OpenOldDotLinkParams} from './OpenOldDotLinkParams'; export type {default as OpenPlaidBankAccountSelectorParams} from './OpenPlaidBankAccountSelectorParams'; export type {default as OpenPlaidBankLoginParams} from './OpenPlaidBankLoginParams'; -export type {default as OpenProfileParams} from './OpenProfileParams'; export type {default as OpenPublicProfilePageParams} from './OpenPublicProfilePageParams'; export type {default as OpenReimbursementAccountPageParams} from './OpenReimbursementAccountPageParams'; export type {default as OpenReportParams} from './OpenReportParams'; @@ -97,6 +96,7 @@ export type {default as UpdateRoomVisibilityParams} from './UpdateRoomVisibility export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams'; export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; +export type {default as UpdateBillingCurrencyParams} from './UpdateBillingCurrencyParams'; export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; export type {default as LeaveRoomParams} from './LeaveRoomParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index d381fe0646d5..e3115a624680 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1,6 +1,5 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as Parameters from './parameters'; import type SignInUserParams from './parameters/SignInUserParams'; import type UpdateBeneficialOwnersForBankAccountParams from './parameters/UpdateBeneficialOwnersForBankAccountParams'; @@ -17,7 +16,6 @@ const WRITE_COMMANDS = { UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', OPEN_APP: 'OpenApp', RECONNECT_APP: 'ReconnectApp', - OPEN_PROFILE: 'OpenProfile', HANDLE_RESTRICTED_EVENT: 'HandleRestrictedEvent', OPEN_REPORT: 'OpenReport', DELETE_PAYMENT_BANK_ACCOUNT: 'DeletePaymentBankAccount', @@ -159,6 +157,7 @@ const WRITE_COMMANDS = { UPDATE_MONEY_REQUEST_DESCRIPTION: 'UpdateMoneyRequestDescription', UPDATE_MONEY_REQUEST_AMOUNT_AND_CURRENCY: 'UpdateMoneyRequestAmountAndCurrency', HOLD_MONEY_REQUEST: 'HoldRequest', + UPDATE_BILLING_CARD_CURRENCY: 'UpdateBillingCardCurrency', UNHOLD_MONEY_REQUEST: 'UnHoldRequest', UPDATE_DISTANCE_REQUEST: 'UpdateDistanceRequest', REQUEST_MONEY: 'RequestMoney', @@ -237,7 +236,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_PREFERRED_LOCALE]: Parameters.UpdatePreferredLocaleParams; [WRITE_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; [WRITE_COMMANDS.OPEN_APP]: Parameters.OpenAppParams; - [WRITE_COMMANDS.OPEN_PROFILE]: Parameters.OpenProfileParams; [WRITE_COMMANDS.HANDLE_RESTRICTED_EVENT]: Parameters.HandleRestrictedEventParams; [WRITE_COMMANDS.OPEN_REPORT]: Parameters.OpenReportParams; [WRITE_COMMANDS.DELETE_PAYMENT_BANK_ACCOUNT]: Parameters.DeletePaymentBankAccountParams; @@ -263,7 +261,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_AUTOMATIC_TIMEZONE]: Parameters.UpdateAutomaticTimezoneParams; [WRITE_COMMANDS.UPDATE_SELECTED_TIMEZONE]: Parameters.UpdateSelectedTimezoneParams; [WRITE_COMMANDS.UPDATE_USER_AVATAR]: Parameters.UpdateUserAvatarParams; - [WRITE_COMMANDS.DELETE_USER_AVATAR]: EmptyObject; + [WRITE_COMMANDS.DELETE_USER_AVATAR]: null; [WRITE_COMMANDS.REFER_TEACHERS_UNITE_VOLUNTEER]: Parameters.ReferTeachersUniteVolunteerParams; [WRITE_COMMANDS.ADD_SCHOOL_PRINCIPAL]: Parameters.AddSchoolPrincipalParams; [WRITE_COMMANDS.CLOSE_ACCOUNT]: Parameters.CloseAccountParams; @@ -279,7 +277,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams; [WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams; [WRITE_COMMANDS.UPDATE_STATUS]: Parameters.UpdateStatusParams; - [WRITE_COMMANDS.CLEAR_STATUS]: EmptyObject; + [WRITE_COMMANDS.CLEAR_STATUS]: null; [WRITE_COMMANDS.UPDATE_PERSONAL_DETAILS_FOR_WALLET]: Parameters.UpdatePersonalDetailsForWalletParams; [WRITE_COMMANDS.VERIFY_IDENTITY]: Parameters.VerifyIdentityParams; [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: Parameters.AcceptWalletTermsParams; @@ -294,8 +292,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK]: Parameters.SignInUserWithLinkParams; [WRITE_COMMANDS.REQUEST_UNLINK_VALIDATION_LINK]: Parameters.RequestUnlinkValidationLinkParams; [WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams; - [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: EmptyObject; - [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: EmptyObject; + [WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: null; + [WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: null; [WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams; [WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams; [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams; @@ -441,12 +439,13 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; [WRITE_COMMANDS.DISMISS_TRACK_EXPENSE_ACTIONABLE_WHISPER]: Parameters.DismissTrackExpenseActionableWhisperParams; + [WRITE_COMMANDS.UPDATE_BILLING_CARD_CURRENCY]: Parameters.UpdateBillingCurrencyParams; [WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST]: Parameters.ConvertTrackedExpenseToRequestParams; [WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE]: Parameters.CategorizeTrackedExpenseParams; [WRITE_COMMANDS.SHARE_TRACKED_EXPENSE]: Parameters.ShareTrackedExpenseParams; [WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams; [WRITE_COMMANDS.DISMISS_VIOLATION]: Parameters.DismissViolationParams; - [WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: EmptyObject; + [WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null; [WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams; [WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams; [WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams; @@ -518,9 +517,9 @@ type ReadCommandParameters = { [READ_COMMANDS.SYNC_POLICY_TO_XERO]: Parameters.SyncPolicyToXeroParams; [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams; [READ_COMMANDS.OPEN_WORKSPACE_VIEW]: Parameters.OpenWorkspaceViewParams; - [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: EmptyObject; - [READ_COMMANDS.OPEN_PAYMENTS_PAGE]: EmptyObject; - [READ_COMMANDS.OPEN_PERSONAL_DETAILS]: EmptyObject; + [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: null; + [READ_COMMANDS.OPEN_PAYMENTS_PAGE]: null; + [READ_COMMANDS.OPEN_PERSONAL_DETAILS]: null; [READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE]: Parameters.OpenPublicProfilePageParams; [READ_COMMANDS.OPEN_PLAID_BANK_LOGIN]: Parameters.OpenPlaidBankLoginParams; [READ_COMMANDS.OPEN_PLAID_BANK_ACCOUNT_SELECTOR]: Parameters.OpenPlaidBankAccountSelectorParams; @@ -535,9 +534,9 @@ type ReadCommandParameters = { [READ_COMMANDS.GET_ROUTE]: Parameters.GetRouteParams; [READ_COMMANDS.GET_ROUTE_FOR_DRAFT]: Parameters.GetRouteParams; [READ_COMMANDS.GET_STATEMENT_PDF]: Parameters.GetStatementPDFParams; - [READ_COMMANDS.OPEN_ONFIDO_FLOW]: EmptyObject; - [READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: EmptyObject; - [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: EmptyObject; + [READ_COMMANDS.OPEN_ONFIDO_FLOW]: null; + [READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE]: null; + [READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE]: null; [READ_COMMANDS.BEGIN_SIGNIN]: Parameters.BeginSignInParams; [READ_COMMANDS.SIGN_IN_WITH_SHORT_LIVED_AUTH_TOKEN]: Parameters.SignInWithShortLivedAuthTokenParams; [READ_COMMANDS.SIGN_IN_WITH_SUPPORT_AUTH_TOKEN]: Parameters.SignInWithSupportAuthTokenParams; @@ -568,6 +567,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { OPEN_OLD_DOT_LINK: 'OpenOldDotLink', OPEN_REPORT: 'OpenReport', RECONNECT_APP: 'ReconnectApp', + ADD_PAYMENT_CARD_GBR: 'AddPaymentCardGBP', REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', } as const; @@ -583,7 +583,8 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP]: Parameters.ReconnectAppParams; [SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams; - [SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: EmptyObject; + [SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBR]: Parameters.AddPaymentCardParams; + [SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 17a933766a69..dda5427e9c9f 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -7,7 +7,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {LastSelectedDistanceRates, OnyxInputOrEntry, Report} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; @@ -78,7 +77,7 @@ function getMileageRates(policy: OnyxInputOrEntry, includeDisabledRates * @returns [currency] - The currency associated with the rate. * @returns [unit] - The unit of measurement for the distance. */ -function getDefaultMileageRate(policy: OnyxInputOrEntry | EmptyObject): MileageRate | undefined { +function getDefaultMileageRate(policy: OnyxInputOrEntry): MileageRate | undefined { if (isEmptyObject(policy) || !policy?.customUnits) { return undefined; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index cb13c347d8aa..6e9782defe4f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -326,7 +326,10 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default, [SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, - [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard/AddPaymentCard').default, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, + [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default, + [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index d10511a325ed..5a7182405681 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -1,7 +1,7 @@ import {findFocusedRoute} from '@react-navigation/core'; import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native'; import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import isCentralPaneName from '@libs/NavigationUtils'; @@ -13,7 +13,6 @@ import type {HybridAppRoute, Route} from '@src/ROUTES'; import ROUTES, {HYBRID_APP_ROUTES} from '@src/ROUTES'; import {PROTECTED_SCREENS} from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import originalCloseRHPFlow from './closeRHPFlow'; import originalDismissModal from './dismissModal'; import originalDismissModalWithReport from './dismissModalWithReport'; @@ -79,7 +78,7 @@ const closeRHPFlow = (ref = navigationRef) => originalCloseRHPFlow(ref); // Re-exporting the dismissModalWithReport here to fill in default value for navigationRef. The dismissModalWithReport isn't defined in this file to avoid cyclic dependencies. // This method is needed because it allows to dismiss the modal and then open the report. Within this method is checked whether the report belongs to a specific workspace. Sometimes the report we want to check, hasn't been added to the Onyx yet. // Then we can pass the report as a param without getting it from the Onyx. -const dismissModalWithReport = (report: Report | EmptyObject, ref = navigationRef) => originalDismissModalWithReport(report, ref); +const dismissModalWithReport = (report: OnyxEntry, ref = navigationRef) => originalDismissModalWithReport(report, ref); /** Method for finding on which index in stack we are. */ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined { diff --git a/src/libs/Navigation/dismissModalWithReport.ts b/src/libs/Navigation/dismissModalWithReport.ts index 6e1f360a4f58..1bb939f5230f 100644 --- a/src/libs/Navigation/dismissModalWithReport.ts +++ b/src/libs/Navigation/dismissModalWithReport.ts @@ -2,6 +2,7 @@ import {getActionFromState} from '@react-navigation/core'; import type {NavigationContainerRef} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; import {findLastIndex} from 'lodash'; +import type {OnyxEntry} from 'react-native-onyx'; import Log from '@libs/Log'; import isCentralPaneName from '@libs/NavigationUtils'; import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils'; @@ -10,7 +11,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './getPolicyIDFromState'; import getStateFromPath from './getStateFromPath'; @@ -26,7 +26,7 @@ import type {RootStackParamList, StackNavigationAction, State} from './types'; * * @param targetReportID - The reportID to navigate to after dismissing the modal */ -function dismissModalWithReport(targetReport: Report | EmptyObject, navigationRef: NavigationContainerRef) { +function dismissModalWithReport(targetReport: OnyxEntry, navigationRef: NavigationContainerRef) { if (!navigationRef.isReady()) { return; } @@ -45,8 +45,8 @@ function dismissModalWithReport(targetReport: Report | EmptyObject, navigationRe case SCREENS.REPORT_AVATAR: case SCREENS.CONCIERGE: // If we are not in the target report, we need to navigate to it after dismissing the modal - if (targetReport.reportID !== getTopmostReportId(state)) { - const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport.reportID)); + if (targetReport?.reportID !== getTopmostReportId(state)) { + const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReport?.reportID ?? '-1')); const policyID = getPolicyIDFromState(state as State); const policyMemberAccountIDs = getPolicyEmployeeAccountIDs(policyID); const shouldOpenAllWorkspace = isEmptyObject(targetReport) ? true : !doesReportBelongToWorkspace(targetReport, policyMemberAccountIDs, policyID); diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 90e52d02163c..3c4608d6b5de 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -72,17 +72,21 @@ export default function linkTo(navigation: NavigationContainerRef | undefined, (value) => value === undefined), - omitBy(actionParams?.params as Record | undefined, (value) => value === undefined), + omitBy(targetParams as Record | undefined, (value) => value === undefined), ); // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack by default @@ -110,8 +114,8 @@ export default function linkTo(navigation: NavigationContainerRef).policyIDs = policyID; + if (targetName === SCREENS.SEARCH.CENTRAL_PANE && targetParams && policyID) { + (targetParams as Record).policyIDs = policyID; } // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index c4858d3141f1..1192e4649ea0 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -39,7 +39,13 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.SETTINGS.SAVE_THE_WORLD]: [SCREENS.I_KNOW_A_TEACHER, SCREENS.INTRO_SCHOOL_PRINCIPAL, SCREENS.I_AM_A_TEACHER], [SCREENS.SETTINGS.TROUBLESHOOT]: [SCREENS.SETTINGS.CONSOLE], [SCREENS.SEARCH.CENTRAL_PANE]: [SCREENS.SEARCH.REPORT_RHP], - [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE, SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY], + [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: [ + SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, + SCREENS.SETTINGS.SUBSCRIPTION.SIZE, + SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY, + SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY, + SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY, + ], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 5defdd9d2e08..b2a69d3aeb39 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -11,6 +11,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.OWNER_CHANGE_CHECK, SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS, SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, + SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, ], [SCREENS.WORKSPACE.WORKFLOWS]: [ SCREENS.WORKSPACE.WORKFLOWS_APPROVER, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index bba611136450..c66472abb3b4 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -126,6 +126,18 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD, exact: true, }, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY, + exact: true, + }, + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY, + exact: true, + }, + [SCREENS.SETTINGS.ADD_PAYMENT_CARD_CHANGE_CURRENCY]: { + path: ROUTES.SETTINGS_CHANGE_CURRENCY, + exact: true, + }, [SCREENS.SETTINGS.PREFERENCES.THEME]: { path: ROUTES.SETTINGS_THEME, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4f09e3a42d58..6c4e03aa2018 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -254,6 +254,8 @@ type SettingsNavigatorParamList = { canChangeSize: 0 | 1; }; [SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: undefined; + [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: undefined; [SCREENS.WORKSPACE.TAXES_SETTINGS]: { policyID: string; }; diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts index f0442e4995d2..4fdc03c3d334 100644 --- a/src/libs/NavigationUtils.ts +++ b/src/libs/NavigationUtils.ts @@ -1,7 +1,7 @@ import SCREENS from '@src/SCREENS'; import type {CentralPaneName} from './Navigation/types'; -const CENTRAL_PANE_SCREEN_NAMES = [ +const CENTRAL_PANE_SCREEN_NAMES = new Set([ SCREENS.SETTINGS.WORKSPACES, SCREENS.SETTINGS.PREFERENCES.ROOT, SCREENS.SETTINGS.SECURITY, @@ -13,14 +13,14 @@ const CENTRAL_PANE_SCREEN_NAMES = [ SCREENS.SETTINGS.SUBSCRIPTION.ROOT, SCREENS.SEARCH.CENTRAL_PANE, SCREENS.REPORT, -]; +]); function isCentralPaneName(screen: string | undefined): screen is CentralPaneName { if (!screen) { return false; } - return CENTRAL_PANE_SCREEN_NAMES.includes(screen as CentralPaneName); + return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName); } export default isCentralPaneName; diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 484104ebb881..0ac2878b6857 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,14 +1,13 @@ import {format, lastDayOfMonth, setDate} from 'date-fns'; import {Str} from 'expensify-common'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import DateUtils from './DateUtils'; import EmailUtils from './EmailUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -73,16 +72,12 @@ type BuildNextStepParameters = { * @param parameters.isPaidWithExpensify - Whether a report has been paid with Expensify or outside * @returns nextStep */ -function buildNextStep( - report: Report | EmptyObject, - predictedNextStatus: ValueOf, - {isPaidWithExpensify}: BuildNextStepParameters = {}, -): ReportNextStep | null { +function buildNextStep(report: OnyxEntry, predictedNextStatus: ValueOf, {isPaidWithExpensify}: BuildNextStepParameters = {}): ReportNextStep | null { if (!ReportUtils.isExpenseReport(report)) { return null; } - const {policyID = '', ownerAccountID = -1, managerID = -1} = report; + const {policyID = '', ownerAccountID = -1, managerID = -1} = report ?? {}; const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? ({} as Policy); const {harvesting, preventSelfApproval, autoReportingFrequency, autoReportingOffset} = policy; const submitToAccountID = PolicyUtils.getSubmitToAccountID(policy, ownerAccountID); @@ -282,7 +277,7 @@ function buildNextStep( accountID: currentUserAccountID, email: currentUserEmail, }, - report as Report, + report, ) ) { optimisticNextStep = { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 077fb5b72102..24471b7f0140 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -36,7 +36,6 @@ import type { import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -298,7 +297,7 @@ Onyx.connect({ const transactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(reportID, actions[reportActions[0]]); if (transactionThreadReportID) { const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); - sortedReportActions = ReportActionUtils.getCombinedReportActions(reportActionsArray, transactionThreadReportActionsArray, reportID); + sortedReportActions = ReportActionUtils.getCombinedReportActions(reportActionsArray, transactionThreadReportID, transactionThreadReportActionsArray, reportID); } lastReportActions[reportID] = sortedReportActions[0]; @@ -2139,23 +2138,23 @@ function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText?: string): PayeePersonalDetails { - const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: OnyxEntry, amountText?: string): PayeePersonalDetails { + const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? ''); return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), icons: [ { - source: personalDetail.avatar ?? FallbackAvatar, - name: personalDetail.login ?? '', + source: personalDetail?.avatar ?? FallbackAvatar, + name: personalDetail?.login ?? '', type: CONST.ICON_TYPE_AVATAR, - id: personalDetail.accountID, + id: personalDetail?.accountID, }, ], descriptiveText: amountText ?? '', - login: personalDetail.login ?? '', - accountID: personalDetail.accountID, - keyForList: String(personalDetail.accountID), + login: personalDetail?.login ?? '', + accountID: personalDetail?.accountID ?? -1, + keyForList: String(personalDetail?.accountID ?? -1), }; } diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 9a35dfc41b72..8ba468e87ed0 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -1,7 +1,6 @@ import {Str} from 'expensify-common'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxInputOrEntry, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; @@ -272,7 +271,7 @@ function createDisplayName(login: string, passedPersonalDetails: Pick /** * Retrieves the distance custom unit object for the given policy */ -function getCustomUnit(policy: OnyxEntry | EmptyObject) { +function getCustomUnit(policy: OnyxEntry): CustomUnit | undefined { return Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); } /** * Retrieves custom unit rate object from the given customUnitRateID */ -function getCustomUnitRate(policy: OnyxEntry | EmptyObject, customUnitRateID: string): Rate | EmptyObject { +function getCustomUnitRate(policy: OnyxEntry, customUnitRateID: string): Rate | undefined { const distanceUnit = getCustomUnit(policy); - return distanceUnit?.rates[customUnitRateID] ?? {}; + return distanceUnit?.rates[customUnitRateID]; } function getRateDisplayValue(value: number, toLocaleDigit: (arg: string) => string): string { @@ -150,26 +149,26 @@ function isExpensifyTeam(email: string | undefined): boolean { /** * Checks if the current user is an admin of the policy. */ -const isPolicyAdmin = (policy: OnyxInputOrEntry | EmptyObject, currentUserLogin?: string): boolean => +const isPolicyAdmin = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => (policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.ADMIN; /** * Checks if the current user is an user of the policy. */ -const isPolicyUser = (policy: OnyxInputOrEntry | EmptyObject, currentUserLogin?: string): boolean => +const isPolicyUser = (policy: OnyxInputOrEntry, currentUserLogin?: string): boolean => (policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.USER; /** * Checks if the policy is a free group policy. */ -const isFreeGroupPolicy = (policy: OnyxEntry | EmptyObject): boolean => policy?.type === CONST.POLICY.TYPE.FREE; +const isFreeGroupPolicy = (policy: OnyxEntry): boolean => policy?.type === CONST.POLICY.TYPE.FREE; const isPolicyEmployee = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); /** * Checks if the current user is an owner (creator) of the policy. */ -const isPolicyOwner = (policy: OnyxInputOrEntry | EmptyObject, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID; +const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID; /** * Create an object mapping member emails to their accountIDs. Filter for members without errors if includeMemberWithErrors is false, and get the login email from the personalDetail object using the accountID. @@ -294,7 +293,7 @@ function isPendingDeletePolicy(policy: OnyxEntry): boolean { return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } -function isPaidGroupPolicy(policy: OnyxEntry | EmptyObject): boolean { +function isPaidGroupPolicy(policy: OnyxEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE; } @@ -311,14 +310,14 @@ function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry | EmptyObject): boolean { +function isInstantSubmitEnabled(policy: OnyxInputOrEntry): boolean { return policy?.type === CONST.POLICY.TYPE.FREE || (policy?.autoReporting === true && policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT); } /** * Checks if policy's approval mode is "optional", a.k.a. "Submit & Close" */ -function isSubmitAndClose(policy: OnyxInputOrEntry | EmptyObject): boolean { +function isSubmitAndClose(policy: OnyxInputOrEntry): boolean { return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL; } @@ -361,7 +360,7 @@ function canEditTaxRate(policy: Policy, taxID: string): boolean { return policy.taxRates?.defaultExternalID !== taxID; } -function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, featureName: PolicyFeatureName): boolean { +function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFeatureName): boolean { if (featureName === CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED) { return !!policy?.tax?.trackingEnabled; } @@ -369,7 +368,7 @@ function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, feature return !!policy?.[featureName]; } -function getApprovalWorkflow(policy: OnyxEntry | EmptyObject): ValueOf { +function getApprovalWorkflow(policy: OnyxEntry): ValueOf { if (policy?.type === CONST.POLICY.TYPE.PERSONAL) { return CONST.POLICY.APPROVAL_MODE.OPTIONAL; } @@ -377,14 +376,14 @@ function getApprovalWorkflow(policy: OnyxEntry | EmptyObject): ValueOf | EmptyObject): string { +function getDefaultApprover(policy: OnyxEntry): string { return policy?.approver ?? policy?.owner ?? ''; } /** * Returns the accountID to whom the given employeeAccountID submits reports to in the given Policy. */ -function getSubmitToAccountID(policy: OnyxEntry | EmptyObject, employeeAccountID: number): number { +function getSubmitToAccountID(policy: OnyxEntry, employeeAccountID: number): number { const employeeLogin = getLoginsByAccountIDs([employeeAccountID])[0]; const defaultApprover = getDefaultApprover(policy); @@ -412,11 +411,11 @@ function getAdminEmployees(policy: OnyxEntry): PolicyEmployee[] { /** * Returns the policy of the report */ -function getPolicy(policyID: string | undefined): Policy | EmptyObject { +function getPolicy(policyID: string | undefined): OnyxEntry { if (!allPolicies || !policyID) { - return {}; + return undefined; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } /** Return active policies where current user is an admin */ diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index eb2cf4434342..2132b97ef555 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -13,7 +13,6 @@ import type Report from '@src/types/onyx/Report'; import type {Message, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DateUtils from './DateUtils'; import * as Environment from './Environment/Environment'; @@ -122,7 +121,7 @@ function isReversedTransaction(reportAction: OnyxInputOrEntry 0; } -function isPendingRemove(reportAction: OnyxInputOrEntry | EmptyObject): boolean { +function isPendingRemove(reportAction: OnyxInputOrEntry): boolean { if (isEmptyObject(reportAction)) { return false; } @@ -192,7 +191,7 @@ function getWhisperedTo(reportAction: OnyxInputOrEntry): number[] const originalMessage = getOriginalMessage(reportAction); const message = getReportActionMessage(reportAction); - if (!(originalMessage && 'whisperedTo' in originalMessage) && !(message && 'whisperedTo' in message)) { + if (!(originalMessage && typeof originalMessage === 'object' && 'whisperedTo' in originalMessage) && !(message && typeof message === 'object' && 'whisperedTo' in message)) { return []; } @@ -200,10 +199,14 @@ function getWhisperedTo(reportAction: OnyxInputOrEntry): number[] return message?.whisperedTo ?? []; } - if (originalMessage && 'whisperedTo' in originalMessage) { + if (originalMessage && typeof originalMessage === 'object' && 'whisperedTo' in originalMessage) { return originalMessage?.whisperedTo ?? []; } + if (typeof originalMessage !== 'object') { + Log.info('Original message is not an object for reportAction: ', true, {reportActionID: reportAction?.reportActionID, actionName: reportAction?.actionName}); + } + return []; } @@ -277,11 +280,11 @@ function isThreadParentMessage(reportAction: OnyxEntry, reportID: * * @deprecated Use Onyx.connect() or withOnyx() instead */ -function getParentReportAction(report: OnyxInputOrEntry | EmptyObject): ReportAction | EmptyObject { +function getParentReportAction(report: OnyxInputOrEntry): OnyxEntry { if (!report?.parentReportID || !report.parentReportActionID) { - return {}; + return undefined; } - return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] ?? {}; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; } /** @@ -299,7 +302,7 @@ function isSentMoneyReportAction(reportAction: OnyxEntry | EmptyObject): boolean { +function isTransactionThread(parentReportAction: OnyxInputOrEntry): boolean { if (isEmptyObject(parentReportAction) || !isMoneyRequestAction(parentReportAction)) { return false; } @@ -374,14 +377,18 @@ function shouldIgnoreGap(currentReportAction: ReportAction | undefined, nextRepo * Returns a sorted and filtered list of report actions from a report and it's associated child * transaction thread report in order to correctly display reportActions from both reports in the one-transaction report view. */ -function getCombinedReportActions(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[], reportID?: string, isCombineReport?: boolean): ReportAction[] { - if (isEmptyObject(transactionThreadReportActions) && !isCombineReport) { +function getCombinedReportActions( + reportActions: ReportAction[], + transactionThreadReportID: string | null, + transactionThreadReportActions: ReportAction[], + reportID?: string, +): ReportAction[] { + if (_.isEmpty(transactionThreadReportID)) { return reportActions; } // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const isSelfDM = report?.chatType === CONST.REPORT.CHAT_TYPE.SELF_DM; // Filter out request and send money request actions because we don't want to show any preview actions for one transaction reports @@ -1288,9 +1295,9 @@ function isReportActionUnread(reportAction: OnyxEntry, lastReadTim * Check whether the current report action of the report is unread or not * */ -function isCurrentActionUnread(report: Report | EmptyObject, reportAction: ReportAction): boolean { - const lastReadTime = report.lastReadTime ?? ''; - const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report.reportID))); +function isCurrentActionUnread(report: OnyxEntry, reportAction: ReportAction): boolean { + const lastReadTime = report?.lastReadTime ?? ''; + const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report?.reportID ?? '-1'))); const currentActionIndex = sortedReportActions.findIndex((action) => action.reportActionID === reportAction.reportActionID); if (currentActionIndex === -1) { return false; @@ -1319,14 +1326,14 @@ function isActionableJoinRequestPending(reportID: string): boolean { return !!findPendingRequest; } -function isApprovedOrSubmittedReportAction(action: OnyxEntry | EmptyObject) { +function isApprovedOrSubmittedReportAction(action: OnyxEntry) { return [CONST.REPORT.ACTIONS.TYPE.APPROVED, CONST.REPORT.ACTIONS.TYPE.SUBMITTED].some((type) => type === action?.actionName); } /** * Gets the text version of the message in a report action */ -function getReportActionMessageText(reportAction: OnyxEntry | EmptyObject): string { +function getReportActionMessageText(reportAction: OnyxEntry): string { if (!Array.isArray(reportAction?.message)) { return getReportActionText(reportAction); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a19d11928cb8..cca52ac37c9d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -45,7 +45,6 @@ import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; import type {Comment, Receipt, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import AccountUtils from './AccountUtils'; @@ -577,7 +576,7 @@ function getCurrentUserDisplayNameOrEmail(): string | undefined { return currentUserPersonalDetails?.displayName ?? currentUserEmail; } -function getChatType(report: OnyxInputOrEntry | Participant | EmptyObject): ValueOf | undefined { +function getChatType(report: OnyxInputOrEntry | Participant): ValueOf | undefined { return report?.chatType; } @@ -607,20 +606,20 @@ function isDraftReport(reportID: string | undefined): boolean { /** * Returns the parentReport if the given report is a thread */ -function getParentReport(report: OnyxEntry | EmptyObject): OnyxEntry | EmptyObject { +function getParentReport(report: OnyxEntry): OnyxEntry { if (!report?.parentReportID) { - return {}; + return undefined; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {}; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; } /** * Returns the root parentReport if the given report is nested. * Uses recursion to iterate any depth of nested reports. */ -function getRootParentReport(report: OnyxEntry | undefined | EmptyObject): OnyxEntry | EmptyObject { +function getRootParentReport(report: OnyxEntry): OnyxEntry { if (!report) { - return {}; + return undefined; } // Returns the current report as the root report, because it does not have a parentReportID @@ -637,11 +636,11 @@ function getRootParentReport(report: OnyxEntry | undefined | EmptyObject /** * Returns the policy of the report */ -function getPolicy(policyID: string | undefined): Policy | EmptyObject { +function getPolicy(policyID: string | undefined): OnyxEntry { if (!allPolicies || !policyID) { - return {}; + return undefined; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } /** @@ -655,7 +654,7 @@ function getPolicyType(report: OnyxInputOrEntry, policies: OnyxCollectio /** * Get the policy name from a given report */ -function getPolicyName(report: OnyxInputOrEntry | undefined | EmptyObject, returnEmptyIfNotFound = false, policy?: OnyxInputOrEntry): string { +function getPolicyName(report: OnyxInputOrEntry, returnEmptyIfNotFound = false, policy?: OnyxInputOrEntry): string { const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (isEmptyObject(report)) { return noPolicyFound; @@ -687,25 +686,25 @@ function getReportParticipantsTitle(accountIDs: number[]): string { /** * Checks if a report is a chat report. */ -function isChatReport(report: OnyxEntry | EmptyObject): boolean { +function isChatReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } -function isInvoiceReport(report: OnyxInputOrEntry | EmptyObject): boolean { +function isInvoiceReport(report: OnyxInputOrEntry): boolean { return report?.type === CONST.REPORT.TYPE.INVOICE; } /** * Checks if a report is an Expense report. */ -function isExpenseReport(report: OnyxInputOrEntry | EmptyObject): boolean { +function isExpenseReport(report: OnyxInputOrEntry): boolean { return report?.type === CONST.REPORT.TYPE.EXPENSE; } /** * Checks if a report is an IOU report using report or reportID */ -function isIOUReport(reportOrID: OnyxInputOrEntry | string | EmptyObject): boolean { +function isIOUReport(reportOrID: OnyxInputOrEntry | string): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return report?.type === CONST.REPORT.TYPE.IOU; } @@ -713,7 +712,7 @@ function isIOUReport(reportOrID: OnyxInputOrEntry | string | EmptyObject /** * Checks if a report is an IOU report using report */ -function isIOUReportUsingReport(report: OnyxEntry | EmptyObject): report is Report { +function isIOUReportUsingReport(report: OnyxEntry): report is Report { return report?.type === CONST.REPORT.TYPE.IOU; } /** @@ -730,7 +729,7 @@ function isTaskReport(report: OnyxInputOrEntry): boolean { * There's another situation where you don't have access to the parentReportAction (because it was created in a chat you don't have access to) * In this case, we have added the key to the report itself */ -function isCanceledTaskReport(report: OnyxInputOrEntry | EmptyObject = {}, parentReportAction: OnyxInputOrEntry | EmptyObject = {}): boolean { +function isCanceledTaskReport(report: OnyxInputOrEntry, parentReportAction: OnyxInputOrEntry = null): boolean { if (!isEmptyObject(parentReportAction) && (ReportActionsUtils.getReportActionMessage(parentReportAction)?.isDeletedParentAction ?? false)) { return true; } @@ -747,7 +746,7 @@ function isCanceledTaskReport(report: OnyxInputOrEntry | EmptyObject = { * * @param parentReportAction - The parent report action of the report (Used to check if the task has been canceled) */ -function isOpenTaskReport(report: OnyxInputOrEntry, parentReportAction: OnyxInputOrEntry | EmptyObject = {}): boolean { +function isOpenTaskReport(report: OnyxInputOrEntry, parentReportAction: OnyxInputOrEntry = null): boolean { return ( isTaskReport(report) && !isCanceledTaskReport(report, parentReportAction) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN ); @@ -770,15 +769,18 @@ function isReportManager(report: OnyxEntry): boolean { /** * Checks if the supplied report has been approved */ -function isReportApproved(reportOrID: OnyxInputOrEntry | string | EmptyObject): boolean { +function isReportApproved(reportOrID: OnyxInputOrEntry | string, parentReportAction: OnyxEntry = undefined): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; + if (!report) { + return parentReportAction?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && parentReportAction?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; + } return report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && report?.statusNum === CONST.REPORT.STATUS_NUM.APPROVED; } /** * Checks if the supplied report is an expense report in Open state and status. */ -function isOpenExpenseReport(report: OnyxInputOrEntry | EmptyObject): boolean { +function isOpenExpenseReport(report: OnyxInputOrEntry): boolean { return isExpenseReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; } @@ -808,7 +810,7 @@ function isSettled(reportID: string | undefined): boolean { if (!allReports || !reportID) { return false; } - const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; + const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; if (isEmptyObject(report) || report.isWaitingOnBankAccount) { return false; } @@ -878,11 +880,11 @@ function isUserCreatedPolicyRoom(report: OnyxEntry): boolean { /** * Whether the provided report is a Policy Expense chat. */ -function isPolicyExpenseChat(report: OnyxInputOrEntry | Participant | EmptyObject): boolean { +function isPolicyExpenseChat(report: OnyxInputOrEntry | Participant): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); } -function isInvoiceRoom(report: OnyxEntry | EmptyObject): boolean { +function isInvoiceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; } @@ -961,7 +963,7 @@ function isPaidGroupPolicyExpenseReport(report: OnyxEntry): boolean { /** * Checks if the supplied report is an invoice report in Open state and status. */ -function isOpenInvoiceReport(report: OnyxEntry | EmptyObject): boolean { +function isOpenInvoiceReport(report: OnyxEntry): boolean { return isInvoiceReport(report) && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; } @@ -1099,7 +1101,7 @@ function sortReportsByLastRead(reports: Array>, reportMetadata /** * Returns true if report is still being processed */ -function isProcessingReport(report: OnyxEntry | EmptyObject): boolean { +function isProcessingReport(report: OnyxEntry): boolean { return report?.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED && report?.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED; } @@ -1179,7 +1181,7 @@ function findLastAccessedReport( let sortedReports = sortReportsByLastRead(reportsValues, reportMetadata); - let adminReport: OnyxEntry | undefined; + let adminReport: OnyxEntry; if (openOnAdminRoom) { adminReport = sortedReports.find((report) => { const chatType = getChatType(report); @@ -1233,7 +1235,7 @@ function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean /** * Whether the provided report is an archived room */ -function isArchivedRoom(report: OnyxInputOrEntry | EmptyObject, reportNameValuePairs?: OnyxInputOrEntry | EmptyObject): boolean { +function isArchivedRoom(report: OnyxInputOrEntry, reportNameValuePairs?: OnyxInputOrEntry): boolean { if (reportNameValuePairs) { return reportNameValuePairs.isArchived; } @@ -1253,7 +1255,7 @@ function isArchivedRoomWithID(reportID?: string) { /** * Whether the provided report is a closed report */ -function isClosedReport(report: OnyxEntry | EmptyObject): boolean { +function isClosedReport(report: OnyxEntry): boolean { return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; } @@ -1269,7 +1271,7 @@ function isJoinRequestInAdminRoom(report: OnyxEntry): boolean { // since they are not a part of the company, and should not action it on their behalf. if (report.policyID) { const policy = getPolicy(report.policyID); - if (!PolicyUtils.isExpensifyTeam(policy.owner) && PolicyUtils.isExpensifyTeam(currentUserPersonalDetails?.login)) { + if (!PolicyUtils.isExpensifyTeam(policy?.owner) && PolicyUtils.isExpensifyTeam(currentUserPersonalDetails?.login)) { return false; } } @@ -1420,7 +1422,7 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report is an IOU or expense report. */ -function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | EmptyObject | string): boolean { +function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | string): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOUReport(report) || isExpenseReport(report); } @@ -1946,11 +1948,11 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.avatar ?? FallbackAvatar, - id: parentReportAction.actorAccountID, + source: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.avatar ?? FallbackAvatar, + id: parentReportAction?.actorAccountID, type: CONST.ICON_TYPE_AVATAR, - name: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.displayName ?? '', - fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, + name: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.displayName ?? '', + fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon, }; return [memberIcon, workspaceIcon]; @@ -1958,14 +1960,14 @@ function getIcons( if (isChatThread(report)) { const parentReportAction = ReportActionsUtils.getParentReportAction(report); - const actorAccountID = parentReportAction.actorAccountID; + const actorAccountID = parentReportAction?.actorAccountID; const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false); const actorIcon = { id: actorAccountID, source: personalDetails?.[actorAccountID ?? -1]?.avatar ?? FallbackAvatar, name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, - fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, + fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon, }; if (isWorkspaceThread(report)) { @@ -2194,7 +2196,7 @@ function getReimbursementQueuedActionMessage( */ function getReimbursementDeQueuedActionMessage( reportAction: OnyxEntry>, - reportOrID: OnyxEntry | EmptyObject | string, + reportOrID: OnyxEntry | string, isLHNPreview = false, ): string { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; @@ -2276,7 +2278,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep * * @param [parentReportAction] - The parent report action of the report (Used to check if the task has been canceled) */ -function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry | EmptyObject = {}): boolean { +function isWaitingForAssigneeToCompleteTask(report: OnyxEntry, parentReportAction: OnyxEntry): boolean { return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction); } @@ -2300,7 +2302,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo * @param option (report or optionItem) * @param parentReportAction (the report action the current report is a thread of) */ -function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | OptionData, parentReportAction: EmptyObject | OnyxEntry = {}) { +function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | OptionData, parentReportAction?: OnyxEntry) { if (!optionOrReport) { return false; } @@ -2650,7 +2652,7 @@ function canEditMoneyRequest(reportAction: OnyxInputOrEntry if ((fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT || fieldToEdit === CONST.EDIT_REQUEST_FIELD.CURRENCY) && TransactionUtils.isDistanceRequest(transaction)) { const policy = getPolicy(moneyRequestReport?.reportID ?? '-1'); - const isAdmin = isExpenseReport(moneyRequestReport) && policy.role === CONST.POLICY.ROLE.ADMIN; + const isAdmin = isExpenseReport(moneyRequestReport) && policy?.role === CONST.POLICY.ROLE.ADMIN; const isManager = isExpenseReport(moneyRequestReport) && currentUserAccountID === moneyRequestReport?.managerID; return isAdmin || isManager; @@ -2840,14 +2842,14 @@ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewActio * * NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use withOnyx or Onyx.connect() instead. */ -function getLinkedTransaction(reportAction: OnyxEntry): Transaction | EmptyObject { +function getLinkedTransaction(reportAction: OnyxEntry): OnyxEntry { let transactionID = ''; if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID ?? '-1'; } - return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; } /** @@ -2930,13 +2932,13 @@ function getTransactionReportName(reportAction: OnyxEntry | EmptyObject | string, - iouReportAction: OnyxInputOrEntry | EmptyObject = {}, + reportOrID: OnyxInputOrEntry | string, + iouReportAction: OnyxInputOrEntry = null, shouldConsiderScanningReceiptOrPendingRoute = false, isPreviewMessageForParentChatReport = false, policy?: OnyxInputOrEntry, isForListPreview = false, - originalReportAction: OnyxInputOrEntry | EmptyObject = iouReportAction, + originalReportAction: OnyxInputOrEntry = iouReportAction, ): string { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; const reportActionMessage = ReportActionsUtils.getReportActionHtml(iouReportAction); @@ -3170,7 +3172,7 @@ function isChangeLogObject(originalMessage?: OriginalMessageChangeLog): Original * @param parentReportAction * @param parentReportActionMessage */ -function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | EmptyObject, parentReportActionMessage: string) { +function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry, parentReportActionMessage: string) { if (isEmptyObject(parentReportAction)) { return parentReportActionMessage || Localize.translateLocal('parentReportAction.deletedMessage'); } @@ -3270,7 +3272,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry, repo /** * Get the report action message for a report action. */ -function getReportActionMessage(reportAction: ReportAction | EmptyObject, reportID?: string, childReportID?: string) { +function getReportActionMessage(reportAction: OnyxEntry, reportID?: string, childReportID?: string) { if (isEmptyObject(reportAction)) { return ''; } @@ -3855,7 +3857,7 @@ function getHumanReadableStatus(statusNum: number): string { * If after all replacements the formula is empty, the original formula is returned. * See {@link https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates} */ -function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport, policy: Policy | EmptyObject): string { +function populateOptimisticReportFormula(formula: string, report: OptimisticExpenseReport, policy: OnyxEntry): string { const createdDate = report.lastVisibleActionCreated ? new Date(report.lastVisibleActionCreated) : undefined; const result = formula // We don't translate because the server response is always in English @@ -3863,7 +3865,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe .replaceAll('{report:startdate}', createdDate ? format(createdDate, CONST.DATE.FNS_FORMAT_STRING) : '') .replaceAll('{report:total}', report.total !== undefined ? CurrencyUtils.convertToDisplayString(Math.abs(report.total), report.currency).toString() : '') .replaceAll('{report:currency}', report.currency ?? '') - .replaceAll('{report:policyname}', policy.name ?? '') + .replaceAll('{report:policyname}', policy?.name ?? '') .replaceAll('{report:created}', createdDate ? format(createdDate, CONST.DATE.FNS_DATE_TIME_FORMAT_STRING) : '') .replaceAll('{report:created:yyyy-MM-dd}', createdDate ? format(createdDate, CONST.DATE.FNS_FORMAT_STRING) : '') .replaceAll('{report:status}', report.statusNum !== undefined ? getHumanReadableStatus(report.statusNum) : '') @@ -3954,7 +3956,7 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa function getIOUSubmittedMessage(report: OnyxEntry) { const policy = getPolicy(report?.policyID); - if (report?.ownerAccountID !== currentUserAccountID && policy.role === CONST.POLICY.ROLE.ADMIN) { + if (report?.ownerAccountID !== currentUserAccountID && policy?.role === CONST.POLICY.ROLE.ADMIN) { const ownerPersonalDetail = getPersonalDetailsForAccountID(report?.ownerAccountID ?? -1); const ownerDisplayName = `${ownerPersonalDetail.displayName ?? ''}${ownerPersonalDetail.displayName !== ownerPersonalDetail.login ? ` (${ownerPersonalDetail.login})` : ''}`; @@ -4120,7 +4122,7 @@ function buildOptimisticIOUReportAction( receipt: Receipt = {}, isOwnPolicyExpenseChat = false, created = DateUtils.getDBTime(), - linkedExpenseReportAction: ReportAction | EmptyObject = {}, + linkedExpenseReportAction?: OnyxEntry, ): OptimisticIOUReportAction { const IOUReportID = iouReportID || generateReportID(); @@ -5177,7 +5179,7 @@ function buildOptimisticMoneyRequestEntities( isPersonalTrackingExpense?: boolean, existingTransactionThreadReportID?: string, linkedTrackedExpenseReportAction?: ReportAction, -): [OptimisticCreatedReportAction, OptimisticCreatedReportAction, OptimisticIOUReportAction, OptimisticChatReport, OptimisticCreatedReportAction | EmptyObject] { +): [OptimisticCreatedReportAction, OptimisticCreatedReportAction, OptimisticIOUReportAction, OptimisticChatReport, OptimisticCreatedReportAction | null] { const createdActionForChat = buildOptimisticCreatedReportAction(payeeEmail); // The `CREATED` action must be optimistically generated before the IOU action so that it won't appear after the IOU action in the chat. @@ -5203,7 +5205,7 @@ function buildOptimisticMoneyRequestEntities( // Create optimistic transactionThread and the `CREATED` action for it, if existingTransactionThreadReportID is undefined const transactionThread = buildTransactionThread(iouAction, iouReport, existingTransactionThreadReportID); - const createdActionForTransactionThread = existingTransactionThreadReportID ? {} : buildOptimisticCreatedReportAction(payeeEmail); + const createdActionForTransactionThread = existingTransactionThreadReportID ? null : buildOptimisticCreatedReportAction(payeeEmail); // The IOU action and the transactionThread are co-dependent as parent-child, so we need to link them together iouAction.childReportID = existingTransactionThreadReportID ?? transactionThread.reportID; @@ -5521,7 +5523,7 @@ function getSystemChat(): OnyxEntry { /** * Attempts to find a report in onyx with the provided list of participants. Does not include threads, task, expense, room, and policy expense chat. */ -function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports): OnyxEntry { +function getChatByParticipants(newParticipantList: number[], reports: OnyxCollection = allReports, shouldIncludeGroupChats = false): OnyxEntry { const sortedNewParticipantList = newParticipantList.sort(); return Object.values(reports ?? {}).find((report) => { const participantAccountIDs = Object.keys(report?.participants ?? {}); @@ -5534,7 +5536,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec isMoneyRequestReport(report) || isChatRoom(report) || isPolicyExpenseChat(report) || - isGroupChat(report) + (isGroupChat(report) && !shouldIncludeGroupChats) ) { return false; } @@ -5585,7 +5587,7 @@ function getAllPolicyReports(policyID: string): Array> { /** * Returns true if Chronos is one of the chat participants (1:1) */ -function chatIncludesChronos(report: OnyxInputOrEntry | EmptyObject): boolean { +function chatIncludesChronos(report: OnyxInputOrEntry): boolean { const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number); return participantAccountIDs.includes(CONST.ACCOUNT_ID.CHRONOS); } @@ -6085,7 +6087,7 @@ function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Error /** * Return true if the expense report is marked for deletion. */ -function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | EmptyObject | string): boolean { +function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | string): boolean { const report = typeof reportOrID === 'string' ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] : reportOrID; if (!isMoneyRequestReport(report)) { return false; @@ -6460,7 +6462,7 @@ function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { ); } -function getRoom(type: ValueOf, policyID: string): OnyxEntry | undefined { +function getRoom(type: ValueOf, policyID: string): OnyxEntry { const room = Object.values(allReports ?? {}).find((report) => report?.policyID === policyID && report?.chatType === type && !isThread(report)); return room; } @@ -6468,7 +6470,7 @@ function getRoom(type: ValueOf, policyID: string) /** * We only want policy members who are members of the report to be able to modify the report description, but not in thread chat. */ -function canEditReportDescription(report: OnyxEntry, policy: OnyxEntry | undefined): boolean { +function canEditReportDescription(report: OnyxEntry, policy: OnyxEntry): boolean { return ( !isMoneyRequestReport(report) && !isArchivedRoom(report) && @@ -6536,7 +6538,7 @@ function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry TransactionUtils.isOnHold(transaction)); } @@ -6554,7 +6556,7 @@ function hasHeldExpenses(iouReportID?: string): boolean { */ function hasOnlyHeldExpenses(iouReportID: string, transactions?: OnyxCollection): boolean { const reportTransactions = TransactionUtils.getAllReportTransactions(iouReportID, transactions); - return !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction)); + return reportTransactions.length > 0 && !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction)); } /** @@ -6649,7 +6651,7 @@ function getAllAncestorReportActions(report: Report | null | undefined): Ancesto break; } - const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport ?? {}, parentReportAction); + const isParentReportActionUnread = ReportActionsUtils.isCurrentActionUnread(parentReport, parentReportAction); allAncestors.push({ report: currentReport, reportAction: parentReportAction, @@ -6716,7 +6718,7 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined, includ * @param lastVisibleActionCreated Last visible action created of the child report * @param type The type of action in the child report */ -function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): Array { +function getOptimisticDataForParentReportAction(reportID: string, lastVisibleActionCreated: string, type: string): Array { const report = getReportOrDraftReport(reportID); if (!report || isEmptyObject(report)) { @@ -6730,13 +6732,13 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct const ancestorReport = getReportOrDraftReport(ancestors.reportIDs[index]); if (!ancestorReport || isEmptyObject(ancestorReport)) { - return {} as EmptyObject; + return null; } const ancestorReportAction = ReportActionsUtils.getReportAction(ancestorReport.reportID, ancestors.reportActionsIDs[index]); if (!ancestorReportAction || isEmptyObject(ancestorReportAction)) { - return {} as EmptyObject; + return null; } return { @@ -6749,7 +6751,7 @@ function getOptimisticDataForParentReportAction(reportID: string, lastVisibleAct }); } -function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | EmptyObject): boolean { +function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry): boolean { if (isEmptyObject(policy)) { return false; } @@ -6772,11 +6774,8 @@ function isReportOwner(report: OnyxInputOrEntry): boolean { function isAllowedToApproveExpenseReport(report: OnyxEntry, approverAccountID?: number): boolean { const policy = getPolicy(report?.policyID); - const {preventSelfApproval} = policy; - const isOwner = (approverAccountID ?? currentUserAccountID) === report?.ownerAccountID; - - return !(preventSelfApproval && isOwner); + return !(policy?.preventSelfApproval && isOwner); } function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean { @@ -6905,7 +6904,7 @@ function canLeaveChat(report: OnyxEntry, policy: OnyxEntry): boo function getReportActionActorAccountID(reportAction: OnyxInputOrEntry, iouReport: OnyxInputOrEntry | undefined): number | undefined { switch (reportAction?.actionName) { case CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW: - return iouReport ? iouReport.managerID : reportAction?.actorAccountID; + return !isEmptyObject(iouReport) ? iouReport.managerID : reportAction?.childManagerAccountID; case CONST.REPORT.ACTIONS.TYPE.SUBMITTED: return reportAction?.adminAccountID ?? reportAction?.actorAccountID; @@ -6979,7 +6978,7 @@ function createDraftTransactionAndNavigateToParticipantSelector(transactionID: s /** * @returns the object to update `report.hasOutstandingChildRequest` */ -function getOutstandingChildRequest(iouReport: OnyxInputOrEntry | EmptyObject): OutstandingChildRequest { +function getOutstandingChildRequest(iouReport: OnyxInputOrEntry): OutstandingChildRequest { if (!iouReport || isEmptyObject(iouReport)) { return {}; } @@ -7013,7 +7012,7 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: s } function shouldShowMerchantColumn(transactions: Transaction[]) { - return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? {})); + return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? null)); } /** diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 6e6a541ccdff..460a686766a7 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -26,7 +26,7 @@ const columnNamesToSortingProperty = { [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'category' as const, [CONST.SEARCH.TABLE_COLUMNS.TYPE]: 'type' as const, [CONST.SEARCH.TABLE_COLUMNS.ACTION]: 'action' as const, - [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: null, + [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: 'comment' as const, [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: null, [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: null, }; @@ -254,8 +254,8 @@ function getSortedTransactionData(data: TransactionListItemType[], sortBy?: Sear } return data.sort((a, b) => { - const aValue = a[sortingProperty]; - const bValue = b[sortingProperty]; + const aValue = sortingProperty === 'comment' ? a.comment.comment : a[sortingProperty]; + const bValue = sortingProperty === 'comment' ? b.comment.comment : b[sortingProperty]; if (aValue === undefined || bValue === undefined) { return 0; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b752328e2ccf..b28e5b782965 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -114,7 +114,7 @@ function buildOptimisticTransaction( source = '', originalTransactionID = '', merchant = '', - receipt: Receipt = {}, + receipt?: OnyxEntry, filename = '', existingTransactionID: string | null = null, category = '', @@ -749,7 +749,7 @@ function getRateID(transaction: OnyxInputOrEntry): string | undefin * If it is distance request, then returns the tax code corresponding to the custom unit rate * Else returns policy default tax rate if transaction is in policy default currency, otherwise foreign default tax rate */ -function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry, currency?: string | undefined) { +function getDefaultTaxCode(policy: OnyxEntry, transaction: OnyxEntry, currency?: string | undefined): string | undefined { if (isDistanceRequest(transaction)) { const customUnitRateID = getRateID(transaction) ?? ''; const customUnitRate = getCustomUnitRate(policy, customUnitRateID); diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts index 6c57c2a6f99d..62c034145d4b 100644 --- a/src/libs/WorkspacesSettingsUtils.ts +++ b/src/libs/WorkspacesSettingsUtils.ts @@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, ReimbursementAccount, Report, ReportActions} from '@src/types/onyx'; +import type {Policy, ReimbursementAccount, Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Unit} from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; import type {Phrase, PhraseParameters} from './Localize'; @@ -66,10 +66,10 @@ const getBrickRoadForPolicy = (report: Report, altReportActions?: OnyxCollection } // To determine if the report requires attention from the current user, we need to load the parent report action - let itemParentReportAction = {}; + let itemParentReportAction: OnyxEntry; if (report.parentReportID) { const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`] ?? {}; - itemParentReportAction = report.parentReportActionID ? itemParentReportActions[report.parentReportActionID] : {}; + itemParentReportAction = report.parentReportActionID ? itemParentReportActions[report.parentReportActionID] : undefined; } const reportOption = {...report, isUnread: ReportUtils.isUnread(report), isUnreadWithMention: ReportUtils.isUnreadWithMention(report)}; const shouldShowGreenDotIndicator = ReportUtils.requiresAttentionFromCurrentUser(reportOption, itemParentReportAction); diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 846d19b25857..988de759d763 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -7,18 +7,9 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import {importEmojiLocale} from '@assets/emojis'; import * as API from '@libs/API'; -import type { - GetMissingOnyxMessagesParams, - HandleRestrictedEventParams, - OpenAppParams, - OpenOldDotLinkParams, - OpenProfileParams, - ReconnectAppParams, - UpdatePreferredLocaleParams, -} from '@libs/API/parameters'; +import type {GetMissingOnyxMessagesParams, HandleRestrictedEventParams, OpenAppParams, OpenOldDotLinkParams, ReconnectAppParams, UpdatePreferredLocaleParams} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as Browser from '@libs/Browser'; -import DateUtils from '@libs/DateUtils'; import {buildEmojisTrie} from '@libs/EmojiTrie'; import Log from '@libs/Log'; import getCurrentUrl from '@libs/Navigation/currentUrl'; @@ -32,7 +23,6 @@ import type {OnyxKey} from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import type {OnyxData} from '@src/types/onyx/Request'; import * as Policy from './Policy/Policy'; import * as Session from './Session'; @@ -457,52 +447,6 @@ function redirectThirdPartyDesktopSignIn() { } } -function openProfile(personalDetails: OnyxTypes.PersonalDetails) { - const oldTimezoneData = personalDetails.timezone ?? {}; - let newTimezoneData = oldTimezoneData; - - if (oldTimezoneData?.automatic ?? true) { - newTimezoneData = { - automatic: true, - selected: Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone, - }; - } - - newTimezoneData = DateUtils.formatToSupportedTimezone(newTimezoneData); - - const parameters: OpenProfileParams = { - timezone: JSON.stringify(newTimezoneData), - }; - - // We expect currentUserAccountID to be a number because it doesn't make sense to open profile if currentUserAccountID is not set - if (typeof currentUserAccountID === 'number') { - API.write(WRITE_COMMANDS.OPEN_PROFILE, parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - timezone: newTimezoneData, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - timezone: oldTimezoneData, - }, - }, - }, - ], - }); - } -} - /** * @param shouldAuthenticateWithCurrentAccount Optional, indicates whether default authentication method (shortLivedAuthToken) should be used */ @@ -558,7 +502,6 @@ export { setLocaleAndNavigate, setSidebarLoaded, setUpPoliciesAndNavigate, - openProfile, redirectThirdPartyDesktopSignIn, openApp, reconnectApp, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 911e1d3cf99e..44f92d73e8dc 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -58,7 +58,6 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {Comment, Receipt, ReceiptSource, Routes, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; import * as Category from './Policy/Category'; @@ -244,11 +243,11 @@ Onyx.connect({ }, }); -let currentUserPersonalDetails: OnyxTypes.PersonalDetails | EmptyObject = {}; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - currentUserPersonalDetails = value?.[userAccountID] ?? {}; + currentUserPersonalDetails = value?.[userAccountID] ?? undefined; }, }); @@ -354,13 +353,13 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, amount: 0, comment, created, - currency: policy?.outputCurrency ?? currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD, + currency: policy?.outputCurrency ?? currentUserPersonalDetails?.localCurrencyCode ?? CONST.CURRENCY.USD, iouRequestType, reportID, transactionID: newTransactionID, isFromGlobalCreate, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - splitPayerAccountIDs: [currentUserPersonalDetails.accountID], + splitPayerAccountIDs: currentUserPersonalDetails ? [currentUserPersonalDetails.accountID] : undefined, }); } @@ -464,7 +463,7 @@ function updateDistanceRequestRate(transactionID: string, rateID: string, policy } /** Helper function to get the receipt error for expenses, or the generic error if there's no receipt */ -function getReceiptError(receipt?: Receipt, filename?: string, isScanRequest = true, errorKey?: number): Errors | ErrorFields { +function getReceiptError(receipt: OnyxEntry, filename?: string, isScanRequest = true, errorKey?: number): Errors | ErrorFields { return isEmptyObject(receipt) || !isScanRequest ? ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage', errorKey) : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source?.toString() ?? '', filename: filename ?? ''}, errorKey); @@ -483,8 +482,8 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedCategories: string[], optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, isNewChatReport: boolean, - transactionThreadReport: OptimisticChatReport | EmptyObject, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction | EmptyObject, + transactionThreadReport: OptimisticChatReport | null, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction | null, shouldCreateNewMoneyRequestReport: boolean, policy?: OnyxTypes.OnyxInputOrEntry, policyTagList?: OnyxTypes.OnyxInputOrEntry, @@ -573,7 +572,7 @@ function buildOnyxDataForMoneyRequest( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { ...transactionThreadReport, pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, @@ -590,7 +589,7 @@ function buildOnyxDataForMoneyRequest( if (!isEmptyObject(transactionThreadCreatedReportAction)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, }, @@ -682,7 +681,7 @@ function buildOnyxDataForMoneyRequest( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { participants: redundantParticipants, pendingFields: null, @@ -739,7 +738,7 @@ function buildOnyxDataForMoneyRequest( if (!isEmptyObject(transactionThreadCreatedReportAction)) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { pendingAction: null, @@ -781,7 +780,7 @@ function buildOnyxDataForMoneyRequest( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { pendingFields: null, errorFields: existingTransactionThreadReport @@ -830,7 +829,7 @@ function buildOnyxDataForMoneyRequest( if (!isEmptyObject(transactionThreadCreatedReportAction)) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), @@ -880,7 +879,7 @@ function buildOnyxDataForInvoice( optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, isNewChatReport: boolean, transactionThreadReport: OptimisticChatReport, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction | EmptyObject, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction | null, policy?: OnyxEntry, policyTagList?: OnyxEntry, policyCategories?: OnyxEntry, @@ -937,7 +936,7 @@ function buildOnyxDataForInvoice( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + [transactionThreadCreatedReportAction?.reportActionID ?? '-1']: transactionThreadCreatedReportAction, }, }, // Remove the temporary transaction used during the creation flow @@ -1067,7 +1066,7 @@ function buildOnyxDataForInvoice( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { + [transactionThreadCreatedReportAction?.reportActionID ?? '-1']: { pendingAction: null, errors: null, }, @@ -1154,7 +1153,7 @@ function buildOnyxDataForInvoice( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { + [transactionThreadCreatedReportAction?.reportActionID ?? '-1']: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage', errorKey), }, }, @@ -1196,8 +1195,8 @@ function buildOnyxDataForTrackExpense( iouCreatedAction: OptimisticCreatedReportAction, iouAction: OptimisticIOUReportAction, reportPreviewAction: OnyxInputValue, - transactionThreadReport: OptimisticChatReport | EmptyObject, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction | EmptyObject, + transactionThreadReport: OptimisticChatReport | null, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction | null, shouldCreateNewMoneyRequestReport: boolean, policy?: OnyxInputValue, policyTagList?: OnyxInputValue, @@ -1331,7 +1330,7 @@ function buildOnyxDataForTrackExpense( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { ...transactionThreadReport, pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, @@ -1348,7 +1347,7 @@ function buildOnyxDataForTrackExpense( if (!isEmptyObject(transactionThreadCreatedReportAction)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, }, @@ -1408,7 +1407,7 @@ function buildOnyxDataForTrackExpense( successData.push( { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { pendingFields: null, errorFields: null, @@ -1427,7 +1426,7 @@ function buildOnyxDataForTrackExpense( if (!isEmptyObject(transactionThreadCreatedReportAction)) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { [transactionThreadCreatedReportAction.reportActionID]: { pendingAction: null, @@ -1506,7 +1505,7 @@ function buildOnyxDataForTrackExpense( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.reportID}`, value: { pendingFields: null, errorFields: existingTransactionThreadReport @@ -1528,9 +1527,9 @@ function buildOnyxDataForTrackExpense( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport?.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { + [transactionThreadCreatedReportAction?.reportActionID ?? '-1']: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, @@ -1883,7 +1882,7 @@ function getSendInvoiceInformation( * it creates optimistic versions of them and uses those instead */ function getMoneyRequestInformation( - parentChatReport: OnyxEntry | EmptyObject, + parentChatReport: OnyxEntry, participant: Participant, comment: string, amount: number, @@ -2066,7 +2065,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, optimisticTransactionThread ?? {}, - optimisticCreatedActionForTransactionThread ?? {}, + optimisticCreatedActionForTransactionThread, shouldCreateNewMoneyRequestReport, policy, policyTagList, @@ -2099,14 +2098,14 @@ function getMoneyRequestInformation( * it creates optimistic versions of them and uses those instead */ function getTrackExpenseInformation( - parentChatReport: OnyxEntry | EmptyObject, + parentChatReport: OnyxEntry, participant: Participant, comment: string, amount: number, currency: string, created: string, merchant: string, - receipt: Receipt | undefined, + receipt: OnyxEntry, category: string | undefined, tag: string | undefined, taxCode: string | undefined, @@ -2120,7 +2119,7 @@ function getTrackExpenseInformation( moneyRequestReportID = '', linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction, existingTransactionID?: string, -): TrackExpenseInformation | EmptyObject { +): TrackExpenseInformation | null { const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; @@ -2138,7 +2137,7 @@ function getTrackExpenseInformation( // If we still don't have a report, it likely doesn't exist, and we will early return here as it should not happen // Maybe later, we can build an optimistic selfDM chat. if (!chatReport) { - return {}; + return null; } // Check if the report is a draft @@ -2276,7 +2275,7 @@ function getTrackExpenseInformation( iouAction, reportPreviewAction, optimisticTransactionThread ?? {}, - (optimisticCreatedActionForTransactionThread as OptimisticCreatedReportAction) ?? {}, // Add type assertion here + optimisticCreatedActionForTransactionThread, shouldCreateNewMoneyRequestReport, policy, policyTagList, @@ -2295,7 +2294,7 @@ function getTrackExpenseInformation( createdIOUReportActionID: shouldCreateNewMoneyRequestReport ? optimisticCreatedActionForIOUReport.reportActionID : '-1', reportPreviewAction: reportPreviewAction ?? undefined, transactionThreadReportID: optimisticTransactionThread.reportID, - createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1', actionableWhisperReportActionIDParam: actionableTrackExpenseWhisper?.reportActionID ?? '', onyxData: { optimisticData: optimisticData.concat(trackExpenseOnyxData[0]), @@ -2576,9 +2575,9 @@ function getUpdateMoneyRequestParams( // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. const diff = calculateDiffAmount(iouReport, updatedTransaction, transaction); - let updatedMoneyRequestReport: OnyxTypes.Report | EmptyObject; + let updatedMoneyRequestReport: OnyxTypes.OnyxInputOrEntry; if (!iouReport) { - updatedMoneyRequestReport = {}; + updatedMoneyRequestReport = null; } else if ((ReportUtils.isExpenseReport(iouReport) || ReportUtils.isInvoiceReport(iouReport)) && typeof iouReport.total === 'number') { // For expense report, the amount is negative, so we should subtract total from diff updatedMoneyRequestReport = { @@ -2592,7 +2591,9 @@ function getUpdateMoneyRequestParams( updatedMoneyRequestReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID ?? -1, diff, TransactionUtils.getCurrency(transaction), false, true); } - updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, transactionDetails?.currency); + if (updatedMoneyRequestReport) { + updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, transactionDetails?.currency); + } optimisticData.push( { @@ -3265,7 +3266,7 @@ function categorizeTrackedExpense( linkedTrackedExpenseReportID: string, transactionThreadReportID: string, reportPreviewReportActionID: string, - onyxData: OnyxData, + onyxData: OnyxData | undefined, amount: number, currency: string, comment: string, @@ -3279,7 +3280,7 @@ function categorizeTrackedExpense( receipt?: Receipt, createdWorkspaceParams?: CreateWorkspaceParams, ) { - const {optimisticData, successData, failureData} = onyxData; + const {optimisticData, successData, failureData} = onyxData ?? {}; const { optimisticData: moveTransactionOptimisticData, @@ -3342,7 +3343,7 @@ function shareTrackedExpense( linkedTrackedExpenseReportID: string, transactionThreadReportID: string, reportPreviewReportActionID: string, - onyxData: OnyxData, + onyxData: OnyxData | undefined, amount: number, currency: string, comment: string, @@ -3356,7 +3357,7 @@ function shareTrackedExpense( receipt?: Receipt, createdWorkspaceParams?: CreateWorkspaceParams, ) { - const {optimisticData, successData, failureData} = onyxData; + const {optimisticData, successData, failureData} = onyxData ?? {}; const { optimisticData: moveTransactionOptimisticData, @@ -3456,7 +3457,7 @@ function requestMoney( createdReportActionIDForThread, onyxData, } = getMoneyRequestInformation( - isMovingTransactionFromTrackExpense ? {} : currentChatReport, + isMovingTransactionFromTrackExpense ? undefined : currentChatReport, participant, comment, amount, @@ -3635,32 +3636,33 @@ function trackExpense( createdReportActionIDForThread, actionableWhisperReportActionIDParam, onyxData, - } = getTrackExpenseInformation( - currentChatReport, - participant, - comment, - amount, - currency, - created, - merchant, - receipt, - category, - tag, - taxCode, - taxAmount, - billable, - policy, - policyTagList, - policyCategories, - payeeEmail, - payeeAccountID, - moneyRequestReportID, - linkedTrackedExpenseReportAction, - isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && ReportActionsUtils.isMoneyRequestAction(linkedTrackedExpenseReportAction) - ? ReportActionsUtils.getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID - : undefined, - ); - const activeReportID = isMoneyRequestReport ? report.reportID : chatReport.reportID; + } = + getTrackExpenseInformation( + currentChatReport, + participant, + comment, + amount, + currency, + created, + merchant, + receipt, + category, + tag, + taxCode, + taxAmount, + billable, + policy, + policyTagList, + policyCategories, + payeeEmail, + payeeAccountID, + moneyRequestReportID, + linkedTrackedExpenseReportAction, + isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && ReportActionsUtils.isMoneyRequestAction(linkedTrackedExpenseReportAction) + ? ReportActionsUtils.getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID + : undefined, + ) ?? {}; + const activeReportID = isMoneyRequestReport ? report.reportID : chatReport?.reportID; switch (action) { case CONST.IOU.ACTION.CATEGORIZE: { @@ -3668,15 +3670,15 @@ function trackExpense( return; } categorizeTrackedExpense( - chatReport.policyID ?? '-1', - transaction.transactionID, - iouAction.reportActionID, + chatReport?.policyID ?? '-1', + transaction?.transactionID ?? '-1', + iouAction?.reportActionID ?? '-1', iouReport?.reportID ?? '-1', createdIOUReportActionID ?? '-1', actionableWhisperReportActionID, linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID, - transactionThreadReportID, + transactionThreadReportID ?? '-1', reportPreviewAction?.reportActionID ?? '-1', onyxData, amount, @@ -3699,15 +3701,15 @@ function trackExpense( return; } shareTrackedExpense( - chatReport.policyID ?? '-1', - transaction.transactionID, - iouAction.reportActionID, + chatReport?.policyID ?? '-1', + transaction?.transactionID ?? '-1', + iouAction?.reportActionID ?? '-1', iouReport?.reportID ?? '-1', createdIOUReportActionID ?? '-1', actionableWhisperReportActionID, linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID, - transactionThreadReportID, + transactionThreadReportID ?? '-1', reportPreviewAction?.reportActionID ?? '-1', onyxData, amount, @@ -3733,10 +3735,10 @@ function trackExpense( created, merchant, iouReportID: iouReport?.reportID, - chatReportID: chatReport.reportID, - transactionID: transaction.transactionID, - reportActionID: iouAction.reportActionID, - createdChatReportActionID, + chatReportID: chatReport?.reportID ?? '-1', + transactionID: transaction?.transactionID ?? '-1', + reportActionID: iouAction?.reportActionID ?? '-1', + createdChatReportActionID: createdChatReportActionID ?? '-1', createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, @@ -3748,8 +3750,8 @@ function trackExpense( billable, // This needs to be a string of JSON because of limitations with the fetch() API and nested objects receiptGpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, - transactionThreadReportID, - createdReportActionIDForThread, + transactionThreadReportID: transactionThreadReportID ?? '-1', + createdReportActionIDForThread: createdReportActionIDForThread ?? '-1', waypoints: validWaypoints ? JSON.stringify(validWaypoints) : undefined, }; if (actionableWhisperReportActionIDParam) { @@ -3764,7 +3766,7 @@ function trackExpense( Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(activeReportID ?? '-1', CONST.IOU.SHARE.ROLE.ACCOUNTANT)); } - Report.notifyNewAction(activeReportID, payeeAccountID); + Report.notifyNewAction(activeReportID ?? '', payeeAccountID); } function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, participants: Participant[], participantAccountIDs: number[], currentUserAccountID: number) { @@ -3774,11 +3776,9 @@ function getOrCreateOptimisticSplitChatReport(existingSplitChatReportID: string, // Check if the report is available locally if we do have one let existingSplitChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${existingChatReportID}`]; - // If we do not have one locally then we will search for a chat with the same participants (only for 1:1 chats). - const shouldGetOrCreateOneOneDM = participants.length < 2; const allParticipantsAccountIDs = [...participantAccountIDs, currentUserAccountID]; - if (!existingSplitChatReport && shouldGetOrCreateOneOneDM) { - existingSplitChatReport = ReportUtils.getChatByParticipants(allParticipantsAccountIDs); + if (!existingSplitChatReport) { + existingSplitChatReport = ReportUtils.getChatByParticipants(allParticipantsAccountIDs, undefined, participantAccountIDs.length > 1); } // We found an existing chat report we are done... @@ -4188,7 +4188,7 @@ function createSplitsAndOnyxData( createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID, reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID, transactionThreadReportID: optimisticTransactionThread.reportID, - createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID, taxAmount: splitTaxAmount, }; @@ -4787,7 +4787,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA // In case this is still the optimistic accountID saved in the splits array, return early as we cannot know // if there is an existing chat between the split creator and this participant // Instead, we will rely on Auth generating the report IDs and the user won't see any optimistic chats or reports created - const participantPersonalDetails: OnyxTypes.PersonalDetails | EmptyObject = allPersonalDetails[participant?.accountID ?? -1] ?? {}; + const participantPersonalDetails: OnyxTypes.PersonalDetails | null = allPersonalDetails[participant?.accountID ?? -1]; if (!participantPersonalDetails || participantPersonalDetails.isOptimisticPersonalDetail) { splits.push({ email: participant.email, @@ -4796,11 +4796,11 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA } } - let oneOnOneChatReport: OnyxTypes.Report | null; + let oneOnOneChatReport: OnyxEntry; let isNewOneOnOneChatReport = false; if (isPolicyExpenseChat) { // The workspace chat reportID is saved in the splits array when starting a split expense with a workspace - oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`] ?? null; + oneOnOneChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`]; } else { const existingChatReport = ReportUtils.getChatByParticipants(participant.accountID ? [participant.accountID, sessionAccountID] : []); isNewOneOnOneChatReport = !existingChatReport; @@ -4896,7 +4896,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID, reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID, transactionThreadReportID: optimisticTransactionThread.reportID, - createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID, }); optimisticData.push(...oneOnOneOptimisticData); @@ -5315,7 +5315,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor iouReportLastMessageText.length === 0 && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && (!transactionThreadID || shouldDeleteTransactionThread); // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted - let updatedIOUReport: OnyxTypes.Report | null; + let updatedIOUReport: OnyxInputValue; const currency = TransactionUtils.getCurrency(transaction); const updatedReportPreviewAction: OnyxTypes.ReportAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; @@ -5417,7 +5417,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor }, ); - if (!shouldDeleteIOUReport && updatedReportPreviewAction.childMoneyRequestCount === 0) { + if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, @@ -5546,7 +5546,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor }); } - if (!shouldDeleteIOUReport && updatedReportPreviewAction.childMoneyRequestCount === 0) { + if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, @@ -5602,7 +5602,7 @@ function deleteTrackExpense(chatReportID: string, transactionID: string, reportA * @param recipient - The user receiving the money */ function getSendMoneyParams( - report: OnyxEntry | EmptyObject, + report: OnyxEntry, amount: number, currency: string, comment: string, @@ -5721,14 +5721,14 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, value: { - [optimisticCreatedActionForTransactionThread.reportActionID]: optimisticCreatedActionForTransactionThread, + [optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1']: optimisticCreatedActionForTransactionThread, }, }; const successData: OnyxUpdate[] = []; // Add optimistic personal details for recipient - let optimisticPersonalDetailListData: OnyxUpdate | EmptyObject = {}; + let optimisticPersonalDetailListData: OnyxUpdate | null = null; const optimisticPersonalDetailListAction = isNewChat ? { [recipientAccountID]: { @@ -5806,7 +5806,7 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, value: { - [optimisticCreatedActionForTransactionThread.reportActionID]: { + [optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1']: { pendingAction: null, }, }, @@ -5834,7 +5834,7 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticTransactionThread.reportID}`, value: { - [optimisticCreatedActionForTransactionThread.reportActionID]: { + [optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1']: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateFailureMessage'), }, }, @@ -5912,7 +5912,7 @@ function getSendMoneyParams( reportPreviewReportActionID: reportPreviewAction.reportActionID, createdIOUReportActionID: optimisticCreatedActionForIOUReport.reportActionID, transactionThreadReportID: optimisticTransactionThread.reportID, - createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1', }, optimisticData, successData, @@ -6117,9 +6117,9 @@ function sendMoneyWithWallet(report: OnyxEntry, amount: number } function canApproveIOU( - iouReport: OnyxTypes.OnyxInputOrEntry | EmptyObject, - chatReport: OnyxTypes.OnyxInputOrEntry | EmptyObject, - policy: OnyxTypes.OnyxInputOrEntry | EmptyObject, + iouReport: OnyxTypes.OnyxInputOrEntry, + chatReport: OnyxTypes.OnyxInputOrEntry, + policy: OnyxTypes.OnyxInputOrEntry, ) { if (isEmptyObject(chatReport)) { return false; @@ -6148,9 +6148,9 @@ function canApproveIOU( } function canIOUBePaid( - iouReport: OnyxTypes.OnyxInputOrEntry | EmptyObject, - chatReport: OnyxTypes.OnyxInputOrEntry | EmptyObject, - policy: OnyxTypes.OnyxInputOrEntry | EmptyObject, + iouReport: OnyxTypes.OnyxInputOrEntry, + chatReport: OnyxTypes.OnyxInputOrEntry, + policy: OnyxTypes.OnyxInputOrEntry, ) { const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const isChatReportArchived = ReportUtils.isArchivedRoom(chatReport); @@ -6171,7 +6171,7 @@ function canIOUBePaid( if (chatReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return chatReport?.invoiceReceiver?.accountID === userAccountID; } - return PolicyUtils.getPolicy(chatReport?.invoiceReceiver?.policyID).role === CONST.POLICY.ROLE.ADMIN; + return PolicyUtils.getPolicy(chatReport?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN; } const isPayer = ReportUtils.isPayer( @@ -6193,7 +6193,7 @@ function canIOUBePaid( ); } -function hasIOUToApproveOrPay(chatReport: OnyxEntry | EmptyObject, excludedIOUReportID: string): boolean { +function hasIOUToApproveOrPay(chatReport: OnyxEntry, excludedIOUReportID: string): boolean { const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; return Object.values(chatReportActions).some((action) => { @@ -6204,20 +6204,20 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry | EmptyObj }); } -function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full?: boolean) { - const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; - let total = expenseReport.total ?? 0; - const hasHeldExpenses = ReportUtils.hasHeldExpenses(expenseReport.reportID); - if (hasHeldExpenses && !full && !!expenseReport.unheldTotal) { - total = expenseReport.unheldTotal; +function approveMoneyRequest(expenseReport: OnyxEntry, full?: boolean) { + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`] ?? null; + let total = expenseReport?.total ?? 0; + const hasHeldExpenses = ReportUtils.hasHeldExpenses(expenseReport?.reportID); + if (hasHeldExpenses && !full && !!expenseReport?.unheldTotal) { + total = expenseReport?.unheldTotal; } - const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(total, expenseReport.currency ?? '', expenseReport.reportID); + const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(total, expenseReport?.currency ?? '', expenseReport?.reportID ?? '-1'); const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED); - const chatReport = getReportOrDraftReport(expenseReport.chatReportID); + const chatReport = getReportOrDraftReport(expenseReport?.chatReportID); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { ...(optimisticApprovedReportAction as OnyxTypes.ReportAction), @@ -6227,7 +6227,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full }; const optimisticIOUReportData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport?.reportID}`, value: { ...expenseReport, lastMessageText: ReportActionsUtils.getReportActionText(optimisticApprovedReportAction), @@ -6250,7 +6250,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full const optimisticNextStepData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, value: optimisticNextStep, }; const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData, optimisticChatReportData]; @@ -6258,7 +6258,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { pendingAction: null, @@ -6267,7 +6267,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport?.reportID}`, value: { pendingFields: { partial: null, @@ -6279,7 +6279,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.other'), @@ -6288,7 +6288,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.chatReportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport?.chatReportID}`, value: { hasOutstandingChildRequest: chatReport?.hasOutstandingChildRequest, pendingFields: { @@ -6298,14 +6298,14 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, value: currentNextStep, }, ]; // Clear hold reason of all transactions if we approve all requests if (full && hasHeldExpenses) { - const heldTransactions = ReportUtils.getAllHeldTransactions(expenseReport.reportID); + const heldTransactions = ReportUtils.getAllHeldTransactions(expenseReport?.reportID); heldTransactions.forEach((heldTransaction) => { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -6329,7 +6329,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject, full } const parameters: ApproveMoneyRequestParams = { - reportID: expenseReport.reportID, + reportID: expenseReport?.reportID ?? '-1', approvedReportActionID: optimisticApprovedReportAction.reportActionID, full, }; @@ -6341,9 +6341,9 @@ function submitReport(expenseReport: OnyxTypes.Report) { const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; const parentReport = getReportOrDraftReport(expenseReport.parentReportID); const policy = PolicyUtils.getPolicy(expenseReport.policyID); - const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; + const isCurrentUserManager = currentUserPersonalDetails?.accountID === expenseReport.managerID; const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); - const adminAccountID = policy.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails.accountID : undefined; + const adminAccountID = policy?.role === CONST.POLICY.ROLE.ADMIN ? currentUserPersonalDetails?.accountID : undefined; const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport?.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID, adminAccountID); const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, isSubmitAndClosePolicy ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.SUBMITTED); @@ -6466,7 +6466,7 @@ function cancelPayment(expenseReport: OnyxTypes.Report, chatReport: OnyxTypes.Re const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID, -(expenseReport.total ?? 0), expenseReport.currency ?? ''); const policy = PolicyUtils.getPolicy(chatReport.policyID); const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE; - const approvalMode = policy.approvalMode ?? CONST.POLICY.APPROVAL_MODE.BASIC; + const approvalMode = policy?.approvalMode ?? CONST.POLICY.APPROVAL_MODE.BASIC; let stateNum: ValueOf = CONST.REPORT.STATE_NUM.SUBMITTED; let statusNum: ValueOf = CONST.REPORT.STATUS_NUM.SUBMITTED; if (!isFree) { @@ -6672,7 +6672,7 @@ function replaceReceipt(transactionID: string, file: File, source: string) { function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry): Participant[] { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? getReportOrDraftReport(report?.chatReportID) : report; - const currentUserAccountID = currentUserPersonalDetails.accountID; + const currentUserAccountID = currentUserPersonalDetails?.accountID; const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); let participants: Participant[] = []; diff --git a/src/libs/actions/MapboxToken.ts b/src/libs/actions/MapboxToken.ts index 923f5590b2bd..4a52bbf4e4a6 100644 --- a/src/libs/actions/MapboxToken.ts +++ b/src/libs/actions/MapboxToken.ts @@ -39,7 +39,7 @@ const setExpirationTimer = () => { return; } console.debug(`[MapboxToken] Fetching a new token after waiting ${REFRESH_INTERVAL / 1000 / 60} minutes`); - API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, null, {}); }, REFRESH_INTERVAL); }; @@ -52,7 +52,7 @@ const clearToken = () => { }; const fetchToken = () => { - API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, {}, {}); + API.read(READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN, null, {}); isCurrentlyFetchingToken = true; }; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index c12f7a042659..60efc55f37a4 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -5,15 +5,23 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; -import type {AddPaymentCardParams, DeletePaymentCardParams, MakeDefaultPaymentMethodParams, PaymentCardParams, TransferWalletBalanceParams} from '@libs/API/parameters'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import type { + AddPaymentCardParams, + DeletePaymentCardParams, + MakeDefaultPaymentMethodParams, + PaymentCardParams, + TransferWalletBalanceParams, + UpdateBillingCurrencyParams, +} from '@libs/API/parameters'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/AddDebitCardForm'; +import INPUT_IDS from '@src/types/form/AddPaymentCardForm'; import type {BankAccountList, FundList} from '@src/types/onyx'; +import type {AccountData} from '@src/types/onyx/Fund'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type PaymentMethod from '@src/types/onyx/PaymentMethod'; import type {FilterMethodPaymentType} from '@src/types/onyx/WalletTransfer'; @@ -64,15 +72,11 @@ function openWalletPage() { }, ]; - return API.read( - READ_COMMANDS.OPEN_PAYMENTS_PAGE, - {}, - { - optimisticData, - successData, - failureData, - }, - ); + return API.read(READ_COMMANDS.OPEN_PAYMENTS_PAGE, null, { + optimisticData, + successData, + failureData, + }); } function getMakeDefaultPaymentOnyxData( @@ -171,7 +175,7 @@ function addPaymentCard(params: PaymentCardParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: true}, }, ]; @@ -179,7 +183,7 @@ function addPaymentCard(params: PaymentCardParams) { const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: false}, }, ]; @@ -187,7 +191,7 @@ function addPaymentCard(params: PaymentCardParams) { const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: false}, }, ]; @@ -228,7 +232,7 @@ function addSubscriptionPaymentCard(cardData: { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: true}, }, ]; @@ -236,7 +240,7 @@ function addSubscriptionPaymentCard(cardData: { const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: false}, }, ]; @@ -244,24 +248,37 @@ function addSubscriptionPaymentCard(cardData: { const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, + key: ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, value: {isLoading: false}, }, ]; - // TODO integrate API for subscription card as a follow up - API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { - optimisticData, - successData, - failureData, - }); + if (currency === CONST.CURRENCY.GBP) { + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBR, parameters, {optimisticData, successData, failureData}).then((response) => { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + return; + } + // TODO 3ds flow will be done as a part https://github.com/Expensify/App/issues/42432 + // We will use this onyx key to open Modal and preview iframe. Potentially we can save the whole object which come from side effect + Onyx.set(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION, (response as {authenticationLink: string}).authenticationLink); + }); + } else { + // eslint-disable-next-line rulesdir/no-multiple-api-calls + API.write(WRITE_COMMANDS.ADD_PAYMENT_CARD, parameters, { + optimisticData, + successData, + failureData, + }); + Navigation.goBack(); + } } /** - * Resets the values for the add debit card form back to their initial states + * Resets the values for the add payment card form back to their initial states */ -function clearDebitCardFormErrorAndSubmit() { - Onyx.set(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, { +function clearPaymentCardFormErrorAndSubmit() { + Onyx.set(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, { isLoading: false, errors: undefined, [INPUT_IDS.SETUP_COMPLETE]: false, @@ -273,6 +290,25 @@ function clearDebitCardFormErrorAndSubmit() { [INPUT_IDS.ADDRESS_ZIP_CODE]: '', [INPUT_IDS.ADDRESS_STATE]: '', [INPUT_IDS.ACCEPT_TERMS]: '', + [INPUT_IDS.CURRENCY]: CONST.CURRENCY.USD, + }); +} + +/** + * Clear 3ds flow - when verification will be finished + * + */ +function clearPaymentCard3dsVerification() { + Onyx.set(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION, ''); +} + +/** + * Set currency for payments + * + */ +function setPaymentMethodCurrency(currency: ValueOf) { + Onyx.merge(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, { + [INPUT_IDS.CURRENCY]: currency, }); } @@ -425,6 +461,69 @@ function deletePaymentCard(fundID: number) { }); } +/** + * Call the API to change billing currency. + * + */ +function updateBillingCurrency(currency: ValueOf, cardCVV: string) { + const parameters: UpdateBillingCurrencyParams = { + cardCVV, + currency, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM, + value: { + isLoading: false, + }, + }, + ]; + + API.write(WRITE_COMMANDS.UPDATE_BILLING_CARD_CURRENCY, parameters, { + optimisticData, + successData, + failureData, + }); +} + +/** + * Set payment card form with API data + * + */ +function setPaymentCardForm(values: AccountData) { + Onyx.merge(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM, { + [INPUT_IDS.CARD_NUMBER]: values.cardNumber, + [INPUT_IDS.EXPIRATION_DATE]: `${values.cardMonth}${values.cardYear?.toString()?.substring(2)}`, + [INPUT_IDS.ADDRESS_STREET]: values.addressStreet, + [INPUT_IDS.ADDRESS_ZIP_CODE]: values.addressZip?.toString(), + [INPUT_IDS.ADDRESS_STATE]: values.addressState, + [INPUT_IDS.CURRENCY]: values.currency, + }); +} + export { deletePaymentCard, addPaymentCard, @@ -433,15 +532,19 @@ export { kycWallRef, continueSetup, addSubscriptionPaymentCard, - clearDebitCardFormErrorAndSubmit, + clearPaymentCardFormErrorAndSubmit, dismissSuccessfulTransferBalancePage, transferWalletBalance, resetWalletTransferData, saveWalletTransferAccountTypeAndID, saveWalletTransferMethodType, hasPaymentMethodError, + updateBillingCurrency, clearDeletePaymentMethodError, clearAddPaymentMethodError, clearWalletError, + setPaymentMethodCurrency, + clearPaymentCard3dsVerification, clearWalletTermsError, + setPaymentCardForm, }; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index daeb4ad58802..5870d642d8cd 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -397,7 +397,7 @@ function deleteAvatar() { }, ]; - API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, {}, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_USER_AVATAR, null, {optimisticData, failureData}); } /** diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 7c87dd597d51..9874a175d0a2 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -22,7 +22,6 @@ import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmplo import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {Attributes, Rate} from '@src/types/onyx/Policy'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {createPolicyExpenseChats} from './Policy'; @@ -105,11 +104,11 @@ Onyx.connect({ /** * Returns the policy of the report */ -function getPolicy(policyID: string | undefined): Policy | EmptyObject { +function getPolicy(policyID: string | undefined): OnyxEntry { if (!allPolicies || !policyID) { - return {}; + return undefined; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } /** @@ -229,9 +228,11 @@ function removeMembers(accountIDs: number[], policyID: string) { const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs); const emailList = accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).filter((login) => !!login) as string[]; - const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy.name, CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY)); + const optimisticClosedReportActions = workspaceChats.map(() => + ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy?.name ?? '', CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY), + ); - const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy.id, policy.name, accountIDs); + const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy?.id ?? '-1', policy?.name ?? '', accountIDs); const optimisticMembersState: OnyxCollectionInputValue = {}; const successMembersState: OnyxCollectionInputValue = {}; @@ -278,7 +279,7 @@ function removeMembers(accountIDs: number[], policyID: string) { value: { statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, - oldPolicyName: policy.name, + oldPolicyName: policy?.name, pendingChatMembers, }, }); @@ -314,7 +315,7 @@ function removeMembers(accountIDs: number[], policyID: string) { const remainingLogins = employeeListEmails.filter((email) => !emailList.includes(email)); const invitedPrimaryToSecondaryLogins: Record = {}; - if (policy.primaryLoginsInvited) { + if (policy?.primaryLoginsInvited) { Object.keys(policy.primaryLoginsInvited).forEach((key) => (invitedPrimaryToSecondaryLogins[policy.primaryLoginsInvited?.[key] ?? ''] = key)); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 88aae0dcb149..d1326ee8f733 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -55,7 +55,6 @@ import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyCateg import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, Rate, TaxRate, Unit} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {buildOptimisticPolicyCategories} from './Category'; @@ -174,11 +173,11 @@ function isCurrencySupportedForDirectReimbursement(currency: string) { /** * Returns the policy of the report */ -function getPolicy(policyID: string | undefined): Policy | EmptyObject { +function getPolicy(policyID: string | undefined): OnyxEntry { if (!allPolicies || !policyID) { - return {}; + return undefined; } - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; } /** @@ -358,7 +357,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - autoReportingFrequency: policy.autoReportingFrequency ?? null, + autoReportingFrequency: policy?.autoReportingFrequency ?? null, pendingFields: {autoReportingFrequency: null}, errorFields: {autoReportingFrequency: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage')}, }, @@ -399,7 +398,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - autoReportingOffset: policy.autoReportingOffset ?? null, + autoReportingOffset: policy?.autoReportingOffset ?? null, pendingFields: {autoReportingOffset: null}, errorFields: {autoReportingOffset: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.monthlyOffsetErrorMessage')}, }, @@ -444,8 +443,8 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - approver: policy.approver ?? null, - approvalMode: policy.approvalMode ?? null, + approver: policy?.approver, + approvalMode: policy?.approvalMode, pendingFields: {approvalMode: null}, errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsApprovalPage.genericErrorMessage')}, }, @@ -504,7 +503,7 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - achAccount: {reimburser: policy.achAccount?.reimburser ?? null}, + achAccount: {reimburser: policy?.achAccount?.reimburser ?? null}, errorFields: {reimburser: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsPayerPage.genericErrorMessage')}, pendingFields: {reimburser: null}, }, @@ -567,8 +566,8 @@ function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueO key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { isLoadingWorkspaceReimbursement: false, - reimbursementChoice: policy.reimbursementChoice ?? null, - achAccount: {reimburser: policy.achAccount?.reimburser ?? null}, + reimbursementChoice: policy?.reimbursementChoice ?? null, + achAccount: {reimburser: policy?.achAccount?.reimburser ?? null}, errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {reimbursementChoice: null}, }, @@ -1976,7 +1975,7 @@ function dismissAddedWithPrimaryLoginMessages(policyID: string) { * * @returns policyID of the workspace we have created */ -function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string | undefined { +function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): string | undefined { // This flow only works for IOU reports if (!ReportUtils.isIOUReportUsingReport(iouReport)) { return; @@ -2191,6 +2190,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string }, }, ]; + successData.push(...employeeWorkspaceChat.onyxSuccessData); successData.push(...employeeWorkspaceChat.onyxSuccessData); @@ -2316,12 +2316,12 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview.reportActionID]: null}, + value: {[reportPreview?.reportActionID ?? '-1']: null}, }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview.reportActionID]: reportPreview}, + value: {[reportPreview?.reportActionID ?? '-1']: reportPreview}, }); // To optimistically remove the GBR from the DM we need to update the hasOutstandingChildRequest param to false @@ -2351,7 +2351,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string message: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, - text: ReportUtils.getReportPreviewMessage(expenseReport, {}, false, false, newWorkspace), + text: ReportUtils.getReportPreviewMessage(expenseReport, null, false, false, newWorkspace), }, ], created: DateUtils.getDBTime(), @@ -2363,7 +2363,7 @@ function createWorkspaceFromIOUPayment(iouReport: Report | EmptyObject): string failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${memberData.workspaceChatReportID}`, - value: {[reportPreview.reportActionID]: null}, + value: {[reportPreview?.reportActionID ?? '-1']: null}, }); // Create the MOVED report action and add it to the DM chat which indicates to the user where the report has been moved @@ -2683,10 +2683,10 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { areWorkflowsEnabled: !enabled, ...(!enabled ? { - approvalMode: policy.approvalMode, - autoReporting: policy.autoReporting, - harvesting: policy.harvesting, - reimbursementChoice: policy.reimbursementChoice, + approvalMode: policy?.approvalMode, + autoReporting: policy?.autoReporting, + harvesting: policy?.harvesting, + reimbursementChoice: policy?.reimbursementChoice, } : {}), pendingFields: { @@ -2751,7 +2751,7 @@ function enableDistanceRequestTax(policyID: string, customUnitName: string, cust value: { customUnits: { [customUnitID]: { - attributes: policy.customUnits ? policy.customUnits[customUnitID].attributes : null, + attributes: policy?.customUnits ? policy?.customUnits[customUnitID].attributes : null, }, }, }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d6fb70ee177a..e528fde34ffe 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -97,7 +97,6 @@ import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; import * as Modal from './Modal'; @@ -743,7 +742,7 @@ function openReport( reportID: string, reportActionID?: string, participantLoginList: string[] = [], - newReportObject: Partial = {}, + newReportObject?: ReportUtils.OptimisticChatReport, parentReportActionID = '-1', isFromDeepLink = false, participantAccountIDList: number[] = [], @@ -817,8 +816,8 @@ function openReport( if (ReportUtils.isGroupChat(newReportObject)) { parameters.chatType = CONST.REPORT.CHAT_TYPE.GROUP; parameters.groupChatAdminLogins = currentUserEmail; - parameters.optimisticAccountIDList = Object.keys(newReportObject.participants ?? {}).join(','); - parameters.reportName = newReportObject.reportName ?? ''; + parameters.optimisticAccountIDList = Object.keys(newReportObject?.participants ?? {}).join(','); + parameters.reportName = newReportObject?.reportName ?? ''; // If we have an avatar then include it with the parameters if (avatar) { @@ -966,8 +965,8 @@ function navigateToAndOpenReport( optimisticReportID?: string, isGroupChat = false, ) { - let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; - let chat: OnyxEntry | EmptyObject = {}; + let newChat: ReportUtils.OptimisticChatReport | undefined; + let chat: OnyxEntry; const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); // If we are not creating a new Group Chat then we are creating a 1:1 DM and will look for an existing chat @@ -997,12 +996,12 @@ function navigateToAndOpenReport( const report = isEmptyObject(chat) ? newChat : chat; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport(report.reportID, '', userLogins, newChat, undefined, undefined, undefined, avatarFile); + openReport(report?.reportID ?? '', '', userLogins, newChat, undefined, undefined, undefined, avatarFile); if (shouldDismissModal) { Navigation.dismissModalWithReport(report); } else { Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '-1')); } } @@ -1012,7 +1011,7 @@ function navigateToAndOpenReport( * @param participantAccountIDs of user logins to start a chat report with. */ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) { - let newChat: ReportUtils.OptimisticChatReport | EmptyObject = {}; + let newChat: ReportUtils.OptimisticChatReport | undefined; const chat = ReportUtils.getChatByParticipants([...participantAccountIDs, currentUserAccountID]); if (!chat) { newChat = ReportUtils.buildOptimisticChatReport([...participantAccountIDs, currentUserAccountID]); @@ -1020,7 +1019,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) const report = chat ?? newChat; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport(report.reportID, '', [], newChat, '0', false, participantAccountIDs); + openReport(report?.reportID ?? '', '', [], newChat, '0', false, participantAccountIDs); Navigation.dismissModalWithReport(report); } @@ -1617,7 +1616,7 @@ function updateNotificationPreference( navigate: boolean, parentReportID?: string, parentReportActionID?: string, - report: OnyxEntry | EmptyObject = {}, + report?: OnyxEntry, ) { if (previousValue === newValue) { if (navigate && !isEmptyObject(report) && report.reportID) { @@ -1663,7 +1662,7 @@ function updateNotificationPreference( } } -function updateRoomVisibility(reportID: string, previousValue: RoomVisibility | undefined, newValue: RoomVisibility, navigate: boolean, report: OnyxEntry | EmptyObject = {}) { +function updateRoomVisibility(reportID: string, previousValue: RoomVisibility | undefined, newValue: RoomVisibility, navigate: boolean, report?: OnyxEntry) { if (previousValue === newValue) { if (navigate && !isEmptyObject(report) && report.reportID) { ReportUtils.goBackToDetailsPage(report); @@ -2512,7 +2511,7 @@ function openReportFromDeepLink(url: string, shouldNavigate = true) { if (reportID && !isAuthenticated) { // Call the OpenReport command to check in the server if it's a public room. If so, we'll open it as an anonymous user - openReport(reportID, '', [], {}, '0', true); + openReport(reportID, '', [], undefined, '0', true); // Show the sign-in page if the app is offline if (networkStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE) { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index bf94de9c52b2..ec45298c3910 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -48,7 +48,7 @@ function search({hash, query, policyIDs, offset, sortBy, sortOrder}: SearchParam * In that case, when users select the search result row, we need to create the transaction thread on the fly and update the search result with the new transactionThreadReport */ function createTransactionThread(hash: number, transactionID: string, reportID: string, moneyRequestReportActionID: string) { - Report.openReport(reportID, '', [currentUserEmail], {}, moneyRequestReportActionID); + Report.openReport(reportID, '', [currentUserEmail], undefined, moneyRequestReportActionID); const onyxUpdate: Record>> = { data: { diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index e85fdc9d1531..b3e77e9fe66e 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -901,7 +901,7 @@ function toggleTwoFactorAuth(enable: boolean) { }, ]; - API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, {}, {optimisticData, successData, failureData}); + API.write(enable ? WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH : WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH, null, {optimisticData, successData, failureData}); } function validateTwoFactorAuth(twoFactorAuthCode: string) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 8dfbc152a3ea..15522a84da62 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -22,7 +22,6 @@ import type {Icon} from '@src/types/onyx/OnyxCommon'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as Report from './Report'; type OptimisticReport = Pick; @@ -795,22 +794,22 @@ function getShareDestination(reportID: string, reports: OnyxCollection): ReportAction | Record { +function getParentReportAction(report: OnyxEntry): OnyxEntry { // If the report is not a thread report, then it won't have a parent and an empty object can be returned. if (!report?.parentReportID || !report.parentReportActionID) { - return {}; + return undefined; } - return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID] ?? {}; + return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; } /** * Returns the parentReport if the given report is a thread */ -function getParentReport(report: OnyxEntry | EmptyObject): OnyxEntry | EmptyObject { +function getParentReport(report: OnyxEntry): OnyxEntry { if (!report?.parentReportID) { - return {}; + return undefined; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {}; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`]; } /** @@ -830,7 +829,7 @@ function deleteTask(report: OnyxEntry) { const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1'); const optimisticReportAction: Partial = { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - previousMessage: parentReportAction.message, + previousMessage: parentReportAction?.message, message: [ { translationKey: '', @@ -845,7 +844,7 @@ function deleteTask(report: OnyxEntry) { linkMetadata: [], }; const optimisticReportActions = { - [parentReportAction.reportActionID]: optimisticReportAction, + [parentReportAction?.reportActionID ?? '-1']: optimisticReportAction, }; const optimisticData: OnyxUpdate[] = [ @@ -911,7 +910,7 @@ function deleteTask(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, value: { - [parentReportAction.reportActionID]: { + [parentReportAction?.reportActionID ?? '-1']: { pendingAction: null, }, }, @@ -938,7 +937,7 @@ function deleteTask(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, value: { - [parentReportAction.reportActionID]: { + [parentReportAction?.reportActionID ?? '-1']: { pendingAction: null, }, }, @@ -979,7 +978,7 @@ function getTaskAssigneeAccountID(taskReport: OnyxEntry): numb } const reportAction = getParentReportAction(taskReport); - return reportAction.childManagerAccountID; + return reportAction?.childManagerAccountID; } /** diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index f39102d46ef3..3166d0dfcb8f 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -4,7 +4,6 @@ import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import * as API from '@libs/API'; import type {DismissViolationParams, GetRouteParams, MarkAsCashParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -289,7 +288,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i * Dismisses the duplicate transaction violation for the provided transactionIDs * and updates the transaction to include the dismissed violation in the comment. */ -function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmissedPersonalDetails: PersonalDetails | CurrentUserPersonalDetails) { +function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmissedPersonalDetails: PersonalDetails) { const currentTransactionViolations = transactionIDs.map((id) => ({transactionID: id, violations: allTransactionViolation?.[id] ?? []})); const currentTransactions = transactionIDs.map((id) => allTransactions?.[id]); const transactionsReportActions = currentTransactions.map((transaction) => ReportActionsUtils.getIOUActionForReportID(transaction.reportID ?? '', transaction.transactionID ?? '')); diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 38a8ce347b3f..e379139fccee 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -21,7 +21,7 @@ function acceptSpotnanaTerms() { asyncOpenURL( // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS, {}, {optimisticData}) + API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS, null, {optimisticData}) .then((response) => (response?.spotnanaToken ? buildTravelDotURL(response.spotnanaToken) : buildTravelDotURL())) .catch(() => buildTravelDotURL()), (travelDotURL) => travelDotURL, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 83a73033d204..fbeed3cd72e9 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -44,7 +44,6 @@ import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type ReportAction from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably'; import * as Link from './Link'; import * as Report from './Report'; @@ -60,7 +59,7 @@ Onyx.connect({ }, }); -let myPersonalDetails: OnyxEntry | EmptyObject = {}; +let myPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { @@ -68,7 +67,7 @@ Onyx.connect({ return; } - myPersonalDetails = value[currentUserAccountID] ?? {}; + myPersonalDetails = value[currentUserAccountID] ?? undefined; }, }); @@ -964,7 +963,7 @@ function clearCustomStatus() { }, }, ]; - API.write(WRITE_COMMANDS.CLEAR_STATUS, {}, {optimisticData}); + API.write(WRITE_COMMANDS.CLEAR_STATUS, null, {optimisticData}); } /** diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index c12c44c1ed74..7ba0fd8d474e 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -49,7 +49,7 @@ function openOnfidoFlow() { }, ]; - API.read(READ_COMMANDS.OPEN_ONFIDO_FLOW, {}, {optimisticData, finallyData}); + API.read(READ_COMMANDS.OPEN_ONFIDO_FLOW, null, {optimisticData, finallyData}); } function setAdditionalDetailsQuestions(questions: WalletAdditionalQuestionDetails[] | null, idNumber?: string) { @@ -208,14 +208,14 @@ function acceptWalletTerms(parameters: AcceptWalletTermsParams) { * Fetches data when the user opens the InitialSettingsPage */ function openInitialSettingsPage() { - API.read(READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE, {}); + API.read(READ_COMMANDS.OPEN_INITIAL_SETTINGS_PAGE, null); } /** * Fetches data when the user opens the EnablePaymentsPage */ function openEnablePaymentsPage() { - API.read(READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE, {}); + API.read(READ_COMMANDS.OPEN_ENABLE_PAYMENTS_PAGE, null); } function updateCurrentStep(currentStep: ValueOf | null) { diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index 3f70dc0d962d..82af0765e179 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -4,7 +4,6 @@ import type {OnboardingPurposeType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type Onboarding from '@src/types/onyx/Onboarding'; import type OnyxPolicy from '@src/types/onyx/Policy'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; let onboarding: Onboarding | [] | undefined; let isLoadingReportData = true; @@ -99,7 +98,7 @@ Onyx.connect({ }, }); -const allPolicies: OnyxCollection | EmptyObject = {}; +const allPolicies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, callback: (val, key) => { diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 3d2b5814684b..03744b397597 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -11,7 +11,6 @@ const { setLocaleAndNavigate, setSidebarLoaded, setUpPoliciesAndNavigate, - openProfile, redirectThirdPartyDesktopSignIn, openApp, reconnectApp, @@ -59,7 +58,6 @@ export { setLocaleAndNavigate, setSidebarLoaded, setUpPoliciesAndNavigate, - openProfile, redirectThirdPartyDesktopSignIn, openApp, reconnectApp, diff --git a/src/libs/actions/navigateFromNotification/index.desktop.ts b/src/libs/actions/navigateFromNotification/index.native.ts similarity index 57% rename from src/libs/actions/navigateFromNotification/index.desktop.ts rename to src/libs/actions/navigateFromNotification/index.native.ts index f710a16a3e70..488ec8ac74e8 100644 --- a/src/libs/actions/navigateFromNotification/index.desktop.ts +++ b/src/libs/actions/navigateFromNotification/index.native.ts @@ -1,9 +1,8 @@ import Navigation from '@libs/Navigation/Navigation'; -import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; const navigateFromNotification = (reportID: string) => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, CONST.REFERRER.NOTIFICATION)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }; export default navigateFromNotification; diff --git a/src/libs/actions/navigateFromNotification/index.ts b/src/libs/actions/navigateFromNotification/index.ts index 488ec8ac74e8..f710a16a3e70 100644 --- a/src/libs/actions/navigateFromNotification/index.ts +++ b/src/libs/actions/navigateFromNotification/index.ts @@ -1,8 +1,9 @@ import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; const navigateFromNotification = (reportID: string) => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, CONST.REFERRER.NOTIFICATION)); }; export default navigateFromNotification; diff --git a/src/libs/migrations/CheckForPreviousReportActionID.ts b/src/libs/migrations/CheckForPreviousReportActionID.ts index 7e4bbe9ffb3e..83658ff961c0 100644 --- a/src/libs/migrations/CheckForPreviousReportActionID.ts +++ b/src/libs/migrations/CheckForPreviousReportActionID.ts @@ -3,6 +3,7 @@ import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {ReportActionsCollectionDataSet} from '@src/types/onyx/ReportAction'; function getReportActionsFromOnyx(): Promise> { return new Promise((resolve) => { @@ -60,6 +61,6 @@ export default function (): Promise { onyxData[onyxKey] = {}; }); - return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); + return Onyx.multiSet(onyxData as ReportActionsCollectionDataSet); }); } diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 4ddffa3d802b..f088de064cc7 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -39,7 +39,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Report} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type ProfilePageProps = StackScreenProps; @@ -47,7 +46,8 @@ type ProfilePageProps = StackScreenProps { +const getPhoneNumber = (details: OnyxEntry): string | undefined => { + const {login = '', displayName = ''} = details ?? {}; // If the user hasn't set a displayName, it is set to their phone number const parsedPhoneNumber = parsePhoneNumber(displayName); @@ -99,14 +99,14 @@ function ProfilePage({route}: ProfilePageProps) { const isValidAccountID = ValidationUtils.isValidAccountRoute(accountID); const loginParams = route.params?.login; - const details = useMemo((): PersonalDetails | EmptyObject => { + const details = useMemo((): OnyxEntry => { // Check if we have the personal details already in Onyx if (personalDetails?.[accountID]) { - return personalDetails?.[accountID] ?? {}; + return personalDetails?.[accountID] ?? undefined; } // Check if we have the login param if (!loginParams) { - return isValidAccountID ? {} : {accountID: 0}; + return isValidAccountID ? undefined : {accountID: 0}; } // Look up the personal details by login const foundDetails = Object.values(personalDetails ?? {}).find((personalDetail) => personalDetail?.login === loginParams?.toLowerCase()); @@ -139,7 +139,7 @@ function ProfilePage({route}: ProfilePageProps) { const phoneNumber = getPhoneNumber(details); const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : login; - const hasAvatar = !!details.avatar; + const hasAvatar = !!details?.avatar; const isLoading = !!personalDetailsMetadata?.[accountID]?.isLoading || isEmptyObject(details); const shouldShowBlockingView = (!isValidAccountID && !isLoading) || CONST.RESTRICTED_ACCOUNT_IDS.includes(accountID); @@ -196,7 +196,7 @@ function ProfilePage({route}: ProfilePageProps) { - + {isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 32ed282f9331..8dcb5e199ba1 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -103,7 +103,7 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, - currentUserPersonalDetails = {}, + currentUserPersonalDetails, disabled = false, isComposerFullSize = false, onSubmit, diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index ac2228bf272a..7b0db3e0d844 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -85,9 +85,10 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); - const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); + const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, [action?.actionName]); + const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? null); const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); + const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID; let avatarSource = avatar; let avatarId: number | string | undefined = actorAccountID; @@ -111,8 +112,8 @@ function ReportActionItemSingle({ let secondaryAvatar: Icon; const primaryDisplayName = displayName; if (displayAllActors) { - // The ownerAccountID and actorAccountID can be the same if the user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; + // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice + const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID; const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 239ab12f0d8f..9945eeb7fb4f 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -29,7 +29,6 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import FloatingMessageCounter from './FloatingMessageCounter'; import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader'; @@ -601,7 +600,7 @@ function ReportActionsList({ [isLoadingNewerReportActions, styles.chatContentScrollView, styles.chatContentScrollViewWithHeaderLoader, canShowHeader], ); - const lastReportAction: OnyxTypes.ReportAction | EmptyObject = useMemo(() => sortedReportActions.at(-1) ?? {}, [sortedReportActions]); + const lastReportAction: OnyxTypes.ReportAction | undefined = useMemo(() => sortedReportActions.at(-1) ?? undefined, [sortedReportActions]); const retryLoadOlderChatsError = useCallback(() => { loadOlderChats(true); @@ -622,12 +621,12 @@ function ReportActionsList({ type={CONST.LIST_COMPONENTS.FOOTER} isLoadingOlderReportActions={isLoadingOlderReportActions} isLoadingInitialReportActions={isLoadingInitialReportActions} - lastReportActionName={lastReportAction.actionName} + lastReportActionName={lastReportAction?.actionName} hasError={hasLoadingOlderReportActionsError} onRetry={retryLoadOlderChatsError} /> ); - }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction.actionName, isOffline, hasLoadingOlderReportActionsError, retryLoadOlderChatsError]); + }, [isLoadingInitialReportActions, isLoadingOlderReportActions, lastReportAction?.actionName, isOffline, hasLoadingOlderReportActionsError, retryLoadOlderChatsError]); const onLayoutInner = useCallback( (event: LayoutChangeEvent) => { diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index c095f3fa0cb0..e433ff00e1a1 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -108,6 +108,7 @@ function ReportActionsListItemRenderer({ childReportName: reportAction.childReportName, childManagerAccountID: reportAction.childManagerAccountID, childMoneyRequestCount: reportAction.childMoneyRequestCount, + childOwnerAccountID: reportAction.childOwnerAccountID, } as ReportAction), [ reportAction.reportActionID, @@ -137,6 +138,7 @@ function ReportActionsListItemRenderer({ reportAction.childReportName, reportAction.childManagerAccountID, reportAction.childMoneyRequestCount, + reportAction.childOwnerAccountID, ], ); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 8935f5804009..1692b67a3812 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -219,8 +219,8 @@ function ReportActionsView({ // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) // so that we display transaction-level and report-level report actions in order in the one-transaction view const combinedReportActions = useMemo( - () => ReportActionsUtils.getCombinedReportActions(reportActionsToDisplay, transactionThreadReportActions, undefined, !!transactionThreadReportID), - [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], + () => ReportActionsUtils.getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions), + [allReportActions, transactionThreadReportActions, transactionThreadReportID], ); const parentReportActionForTransactionThread = useMemo( diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 2fc13af7040c..3e8b7643bdef 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -26,7 +26,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; @@ -137,18 +136,18 @@ function ReportFooter({ const mention = match[1] ? match[1].trim() : undefined; const mentionWithDomain = ReportUtils.addDomainToShortMention(mention ?? '') ?? mention; - let assignee: OnyxTypes.PersonalDetails | EmptyObject = {}; + let assignee: OnyxEntry; let assigneeChatReport; if (mentionWithDomain) { - assignee = Object.values(allPersonalDetails).find((value) => value?.login === mentionWithDomain) ?? {}; - if (!Object.keys(assignee).length) { + assignee = Object.values(allPersonalDetails).find((value) => value?.login === mentionWithDomain) ?? undefined; + if (!Object.keys(assignee ?? {}).length) { const assigneeAccountID = UserUtils.generateAccountID(mentionWithDomain); const optimisticDataForNewAssignee = Task.setNewOptimisticAssignee(mentionWithDomain, assigneeAccountID); assignee = optimisticDataForNewAssignee.assignee; assigneeChatReport = optimisticDataForNewAssignee.assigneeReport; } } - Task.createTaskAndNavigate(report.reportID, title, '', assignee?.login ?? '', assignee.accountID, assigneeChatReport, report.policyID); + Task.createTaskAndNavigate(report.reportID, title, '', assignee?.login ?? '', assignee?.accountID, assigneeChatReport, report.policyID); return true; }, [allPersonalDetails, report.policyID, report.reportID], diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index cd85939e8bec..9ffb8c227e7e 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -52,11 +52,11 @@ export default function > { function WithReportOrNotFound(props: TProps, ref: ForwardedRef) { const getReportAction = useCallback(() => { - let reportAction: OnyxTypes.ReportAction | Record | undefined = props.reportActions?.[`${props.route.params.reportActionID}`]; + let reportAction: OnyxEntry = props.reportActions?.[`${props.route.params.reportActionID}`]; // Handle threads if needed if (!reportAction?.reportActionID) { - reportAction = props?.parentReportAction ?? {}; + reportAction = props?.parentReportAction ?? undefined; } return reportAction; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 46c0d10e08ac..58e69485c1b3 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -34,8 +34,9 @@ type MoneyRequestParticipantsSelectorProps = { /** Callback to add participants in MoneyRequestModal */ onParticipantsAdded: (value: Participant[]) => void; + /** Selected participants from MoneyRequestModal with login */ - participants?: Participant[]; + participants?: Participant[] | typeof CONST.EMPTY_ARRAY; /** The type of IOU report, i.e. split, request, send, track */ iouType: IOUType; @@ -47,7 +48,7 @@ type MoneyRequestParticipantsSelectorProps = { action: IOUAction; }; -function MoneyRequestParticipantsSelector({participants = [], onFinish, onParticipantsAdded, iouType, iouRequestType, action}: MoneyRequestParticipantsSelectorProps) { +function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, iouRequestType, action}: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -94,7 +95,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic options.personalDetails, betas, '', - participants, + participants as Participant[], CONST.EXPENSIFY_EMAILS, // If we are using this component in the "Submit expense" flow then we pass the includeOwnedWorkspaceChats argument so that the current user @@ -153,7 +154,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { betas, - selectedOptions: participants, + selectedOptions: participants as Participant[], excludeLogins: CONST.EXPENSIFY_EMAILS, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); @@ -378,6 +379,17 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic onFinish, ]); + const onSelectRow = useCallback( + (item: Participant) => { + if (isIOUSplit) { + addParticipantToSelection(item); + return; + } + addSingleParticipant(item); + }, + [isIOUSplit, addParticipantToSelection, addSingleParticipant], + ); + return ( (isIOUSplit ? addParticipantToSelection(item) : addSingleParticipant(item))} + onSelectRow={onSelectRow} shouldDebounceRowSelect footerContent={footerContent} headerMessage={header} diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index ea03c9ae3b06..53bbebec7b02 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -81,7 +81,7 @@ function IOURequestStepConfirmation({ const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const [receiptFile, setReceiptFile] = useState(); + const [receiptFile, setReceiptFile] = useState>(); const requestType = TransactionUtils.getRequestType(transaction); const isDistanceRequest = requestType === CONST.IOU.REQUEST_TYPE.DISTANCE; @@ -257,7 +257,7 @@ function IOURequestStepConfirmation({ ); const trackExpense = useCallback( - (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: Receipt, gpsPoints?: IOU.GpsPoint) => { + (selectedParticipants: Participant[], trimmedComment: string, receiptObj?: OnyxEntry, gpsPoints?: IOU.GpsPoint) => { if (!report || !transaction) { return; } diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 0f2d0cc0da61..9399a2a65d9f 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -78,7 +78,7 @@ function IOURequestStepDistanceRate({ function selectDistanceRate(customUnitRateID: string) { if (shouldShowTax) { const policyCustomUnitRate = getCustomUnitRate(policy, customUnitRateID); - const taxRateExternalID = policyCustomUnitRate.attributes?.taxRateExternalID ?? '-1'; + const taxRateExternalID = policyCustomUnitRate?.attributes?.taxRateExternalID ?? '-1'; const taxableAmount = DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, TransactionUtils.getDistance(transaction)); const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxRateExternalID) ?? ''; const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, taxableAmount)); diff --git a/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx b/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx new file mode 100644 index 000000000000..affa5fde3706 --- /dev/null +++ b/src/pages/settings/PaymentCard/ChangeCurrency/index.tsx @@ -0,0 +1,43 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import PaymentCardChangeCurrencyForm from '@components/AddPaymentCard/PaymentCardChangeCurrencyForm'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import type CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function ChangeCurrency() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [debitCardForm] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); + + const changeCurrency = useCallback((currency?: ValueOf) => { + if (currency) { + PaymentMethods.setPaymentMethodCurrency(currency); + } + + Navigation.goBack(); + }, []); + + return ( + + + + + + + ); +} + +ChangeCurrency.displayName = 'ChangeCurrency'; + +export default ChangeCurrency; diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 4c5ed88e6898..a31945d40f12 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -21,12 +21,11 @@ import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as UserUtils from '@libs/UserUtils'; -import * as App from '@userActions/App'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {LoginList, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; +import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx'; type ProfilePageOnyxProps = { loginList: OnyxEntry; @@ -101,10 +100,6 @@ function ProfilePage({ }, ]; - useEffect(() => { - App.openProfile(currentUserPersonalDetails as PersonalDetails); - }, [currentUserPersonalDetails]); - const privateOptions = [ { description: translate('privatePersonalDetails.legalName'), diff --git a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx index 09f8e525014d..6bcc5822c186 100644 --- a/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSectionActions/index.tsx @@ -31,7 +31,7 @@ function CardSectionActions() { { icon: Expensicons.MoneyCircle, text: translate('subscription.cardSection.changeCurrency'), - onSelected: () => {}, // TODO: update with navigation to "change currency" screen (https://github.com/Expensify/App/issues/38629) + onSelected: () => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY), }, ], [translate], diff --git a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx index a20ad3c0a96c..627556e7b5bd 100644 --- a/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSectionDataEmpty/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import Button from '@components/Button'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,10 +9,14 @@ function CardSectionDataEmpty() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const openAddPaymentCardScreen = useCallback(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }, []); + return (