From 858a2f089a7006180aa54866b05c18b6c5dcde5d Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 30 Sep 2024 23:34:45 +0100 Subject: [PATCH 01/17] chore(debug): initial implementation for LHN and settings GBR/RBR debug --- src/CONST.ts | 127 +++++----- src/components/Indicator.tsx | 102 +------- src/hooks/useSettingsStatus.ts | 67 ++++++ src/languages/en.ts | 219 +++++++++--------- .../BottomTabBar.tsx | 101 ++++---- .../DebugTabView.tsx | 176 ++++++++++++++ src/libs/WorkspacesSettingsUtils.ts | 25 +- 7 files changed, 496 insertions(+), 321 deletions(-) create mode 100644 src/hooks/useSettingsStatus.ts create mode 100644 src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 70db2e661056..29951d55f16e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3,13 +3,13 @@ import dateAdd from 'date-fns/add'; import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; -import type {ValueOf} from 'type-fest'; -import type {Video} from './libs/actions/Report'; +import type { ValueOf } from 'type-fest'; +import type { Video } from './libs/actions/Report'; import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; -import type {Unit} from './types/onyx/Policy'; +import type { Unit } from './types/onyx/Policy'; type RateAndUnit = { unit: Unit; @@ -32,8 +32,8 @@ const PLATFORM_IOS = 'iOS'; const ANDROID_PACKAGE_NAME = 'com.expensify.chat'; const CURRENT_YEAR = new Date().getFullYear(); const PULL_REQUEST_NUMBER = Config?.PULL_REQUEST_NUMBER ?? ''; -const MAX_DATE = dateAdd(new Date(), {years: 1}); -const MIN_DATE = dateSubtract(new Date(), {years: 20}); +const MAX_DATE = dateAdd(new Date(), { years: 1 }); +const MIN_DATE = dateSubtract(new Date(), { years: 20 }); const EXPENSIFY_POLICY_DOMAIN = 'expensify-policy'; const EXPENSIFY_POLICY_DOMAIN_EXTENSION = '.exfy'; @@ -142,7 +142,7 @@ type OnboardingTaskType = { type: string; autoCompleted: boolean; title: string; - description: string | ((params: Partial<{adminsRoomLink: string; workspaceCategoriesLink: string; workspaceMoreFeaturesLink: string; workspaceMembersLink: string}>) => string); + description: string | ((params: Partial<{ adminsRoomLink: string; workspaceCategoriesLink: string; workspaceMoreFeaturesLink: string; workspaceMembersLink: string }>) => string); }; type OnboardingMessageType = { @@ -523,9 +523,9 @@ const CONST = { shortcutKey: 'K', modifiers: ['CTRL'], trigger: { - DEFAULT: {input: 'k', modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierCommand}, + DEFAULT: { input: 'k', modifierFlags: keyModifierControl }, + [PLATFORM_OS_MACOS]: { input: 'k', modifierFlags: keyModifierCommand }, + [PLATFORM_IOS]: { input: 'k', modifierFlags: keyModifierCommand }, }, type: KEYBOARD_SHORTCUT_NAVIGATION_TYPE, }, @@ -534,9 +534,9 @@ const CONST = { shortcutKey: 'K', modifiers: ['CTRL', 'SHIFT'], trigger: { - DEFAULT: {input: 'k', modifierFlags: keyModifierShiftControl}, - [PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierShiftCommand}, - [PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierShiftCommand}, + DEFAULT: { input: 'k', modifierFlags: keyModifierShiftControl }, + [PLATFORM_OS_MACOS]: { input: 'k', modifierFlags: keyModifierShiftCommand }, + [PLATFORM_IOS]: { input: 'k', modifierFlags: keyModifierShiftCommand }, }, type: KEYBOARD_SHORTCUT_NAVIGATION_TYPE, }, @@ -545,9 +545,9 @@ const CONST = { shortcutKey: 'J', modifiers: ['CTRL'], trigger: { - DEFAULT: {input: 'j', modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: 'j', modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: 'j', modifierFlags: keyModifierCommand}, + DEFAULT: { input: 'j', modifierFlags: keyModifierControl }, + [PLATFORM_OS_MACOS]: { input: 'j', modifierFlags: keyModifierCommand }, + [PLATFORM_IOS]: { input: 'j', modifierFlags: keyModifierCommand }, }, }, ESCAPE: { @@ -555,9 +555,9 @@ const CONST = { shortcutKey: 'Escape', modifiers: [], trigger: { - DEFAULT: {input: keyInputEscape}, - [PLATFORM_OS_MACOS]: {input: keyInputEscape}, - [PLATFORM_IOS]: {input: keyInputEscape}, + DEFAULT: { input: keyInputEscape }, + [PLATFORM_OS_MACOS]: { input: keyInputEscape }, + [PLATFORM_IOS]: { input: keyInputEscape }, }, }, ENTER: { @@ -565,9 +565,9 @@ const CONST = { shortcutKey: 'Enter', modifiers: [], trigger: { - DEFAULT: {input: keyInputEnter}, - [PLATFORM_OS_MACOS]: {input: keyInputEnter}, - [PLATFORM_IOS]: {input: keyInputEnter}, + DEFAULT: { input: keyInputEnter }, + [PLATFORM_OS_MACOS]: { input: keyInputEnter }, + [PLATFORM_IOS]: { input: keyInputEnter }, }, }, CTRL_ENTER: { @@ -575,9 +575,9 @@ const CONST = { shortcutKey: 'Enter', modifiers: ['CTRL'], trigger: { - DEFAULT: {input: keyInputEnter, modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand}, + DEFAULT: { input: keyInputEnter, modifierFlags: keyModifierControl }, + [PLATFORM_OS_MACOS]: { input: keyInputEnter, modifierFlags: keyModifierCommand }, + [PLATFORM_IOS]: { input: keyInputEnter, modifierFlags: keyModifierCommand }, }, }, COPY: { @@ -585,9 +585,9 @@ const CONST = { shortcutKey: 'C', modifiers: ['CTRL'], trigger: { - DEFAULT: {input: 'c', modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: 'c', modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: 'c', modifierFlags: keyModifierCommand}, + DEFAULT: { input: 'c', modifierFlags: keyModifierControl }, + [PLATFORM_OS_MACOS]: { input: 'c', modifierFlags: keyModifierCommand }, + [PLATFORM_IOS]: { input: 'c', modifierFlags: keyModifierCommand }, }, }, ARROW_UP: { @@ -595,9 +595,9 @@ const CONST = { shortcutKey: 'ArrowUp', modifiers: [], trigger: { - DEFAULT: {input: keyInputUpArrow}, - [PLATFORM_OS_MACOS]: {input: keyInputUpArrow}, - [PLATFORM_IOS]: {input: keyInputUpArrow}, + DEFAULT: { input: keyInputUpArrow }, + [PLATFORM_OS_MACOS]: { input: keyInputUpArrow }, + [PLATFORM_IOS]: { input: keyInputUpArrow }, }, }, ARROW_DOWN: { @@ -605,9 +605,9 @@ const CONST = { shortcutKey: 'ArrowDown', modifiers: [], trigger: { - DEFAULT: {input: keyInputDownArrow}, - [PLATFORM_OS_MACOS]: {input: keyInputDownArrow}, - [PLATFORM_IOS]: {input: keyInputDownArrow}, + DEFAULT: { input: keyInputDownArrow }, + [PLATFORM_OS_MACOS]: { input: keyInputDownArrow }, + [PLATFORM_IOS]: { input: keyInputDownArrow }, }, }, ARROW_LEFT: { @@ -615,9 +615,9 @@ const CONST = { shortcutKey: 'ArrowLeft', modifiers: [], trigger: { - DEFAULT: {input: keyInputLeftArrow}, - [PLATFORM_OS_MACOS]: {input: keyInputLeftArrow}, - [PLATFORM_IOS]: {input: keyInputLeftArrow}, + DEFAULT: { input: keyInputLeftArrow }, + [PLATFORM_OS_MACOS]: { input: keyInputLeftArrow }, + [PLATFORM_IOS]: { input: keyInputLeftArrow }, }, }, ARROW_RIGHT: { @@ -625,9 +625,9 @@ const CONST = { shortcutKey: 'ArrowRight', modifiers: [], trigger: { - DEFAULT: {input: keyInputRightArrow}, - [PLATFORM_OS_MACOS]: {input: keyInputRightArrow}, - [PLATFORM_IOS]: {input: keyInputRightArrow}, + DEFAULT: { input: keyInputRightArrow }, + [PLATFORM_OS_MACOS]: { input: keyInputRightArrow }, + [PLATFORM_IOS]: { input: keyInputRightArrow }, }, }, TAB: { @@ -640,9 +640,9 @@ const CONST = { shortcutKey: 'D', modifiers: ['CTRL'], trigger: { - DEFAULT: {input: 'd', modifierFlags: keyModifierControl}, - [PLATFORM_OS_MACOS]: {input: 'd', modifierFlags: keyModifierCommand}, - [PLATFORM_IOS]: {input: 'd', modifierFlags: keyModifierCommand}, + DEFAULT: { input: 'd', modifierFlags: keyModifierControl }, + [PLATFORM_OS_MACOS]: { input: 'd', modifierFlags: keyModifierCommand }, + [PLATFORM_IOS]: { input: 'd', modifierFlags: keyModifierCommand }, }, }, }, @@ -1215,10 +1215,10 @@ const CONST = { }, }, WEEK_STARTS_ON: 1, // Monday - DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_NETWORK_DATA: {isOffline: false}, + DEFAULT_TIME_ZONE: { automatic: true, selected: 'America/Los_Angeles' }, + DEFAULT_ACCOUNT_DATA: { errors: null, success: '', isLoading: false }, + DEFAULT_CLOSE_ACCOUNT_DATA: { errors: null, success: '', isLoading: false }, + DEFAULT_NETWORK_DATA: { isOffline: false }, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -4432,7 +4432,7 @@ const CONST = { NATIVE: 32, NORMAL: 8, }, - DEFAULT_VIDEO_DIMENSIONS: {width: 1900, height: 1400}, + DEFAULT_VIDEO_DIMENSIONS: { width: 1900, height: 1400 }, }, INTRO_CHOICES: { @@ -4460,9 +4460,9 @@ const CONST = { WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', - ONBOARDING_CHOICES: {...onboardingChoices}, - SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices}, - ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, + ONBOARDING_CHOICES: { ...onboardingChoices }, + SELECTABLE_ONBOARDING_CHOICES: { ...selectableOnboardingChoices }, + ONBOARDING_INVITE_TYPES: { ...onboardingInviteTypes }, ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', ONBOARDING_CONCIERGE: { [onboardingChoices.EMPLOYER]: @@ -4535,7 +4535,7 @@ const CONST = { type: 'meetGuide', autoCompleted: false, title: 'Meet your setup specialist', - description: ({adminsRoomLink}) => + description: ({ adminsRoomLink }) => `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, @@ -4544,7 +4544,7 @@ const CONST = { type: 'setupCategories', autoCompleted: false, title: 'Set up categories', - description: ({workspaceCategoriesLink}) => + description: ({ workspaceCategoriesLink }) => '*Set up categories* so your team can code expenses for easy reporting.\n' + '\n' + 'Here’s how to set up categories:\n' + @@ -4563,7 +4563,7 @@ const CONST = { type: 'setupTags', autoCompleted: false, title: 'Set up tags', - description: ({workspaceMoreFeaturesLink}) => + description: ({ workspaceMoreFeaturesLink }) => 'Tags can be used if you want more details with every expense. Use tags for projects, clients, locations, departments, and more. If you need multiple levels of tags you can upgrade to a control plan.\n' + '\n' + '*Here’s how to set up tags:*\n' + @@ -4582,7 +4582,7 @@ const CONST = { type: 'addExpenseApprovals', autoCompleted: false, title: 'Add expense approvals', - description: ({workspaceMoreFeaturesLink}) => + description: ({ workspaceMoreFeaturesLink }) => '*Add expense approvals* to review your team’s spend and keep it under control.\n' + '\n' + 'Here’s how to add expense approvals:\n' + @@ -4601,7 +4601,7 @@ const CONST = { type: 'inviteTeam', autoCompleted: false, title: 'Invite your team', - description: ({workspaceMembersLink}) => + description: ({ workspaceMembersLink }) => '*Invite your team* to Expensify so they can start tracking expenses today.\n' + '\n' + 'Here’s how to invite your team:\n' + @@ -5774,6 +5774,21 @@ const CONST = { TAGS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags#import-a-spreadsheet-1', }, + SETTINGS_STATUS: { + HAS_USER_WALLET_ERRORS: 'hasUserWalletErrors', + HAS_PAYMENT_METHOD_ERROR: 'hasPaymentMethodError', + HAS_POLICY_ERRORS: 'hasPolicyError', + HAS_CUSTOM_UNITS_ERROR: 'hasCustomUnitsError', + HAS_EMPLOYEE_LIST_ERROR: 'hasEmployeeListError', + HAS_SYNC_ERRORS: 'hasSyncError', + HAS_SUBSCRIPTION_ERRORS: 'hasSubscriptionError', + HAS_REIMBURSEMENT_ACCOUNT_ERRORS: 'hasReimbursementAccountErrors', + HAS_LOGIN_LIST_ERROR: 'hasLoginListError', + HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors', + HAS_LOGIN_LIST_INFO: 'hasLoginListInfo', + HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo', + }, + DEBUG: { DETAILS: 'details', JSON: 'json', @@ -5792,6 +5807,6 @@ type FeedbackSurveyOptionID = ValueOf; type CancellationType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType, OnboardingInviteType}; +export type { Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType, OnboardingInviteType }; export default CONST; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 4d352b6a6cde..ec02a4523b51 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,109 +1,17 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; -import useTheme from '@hooks/useTheme'; +import useSettingsStatus from '@hooks/useSettingsStatus'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isConnectionInProgress} from '@libs/actions/connections'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as SubscriptionUtils from '@libs/SubscriptionUtils'; -import * as UserUtils from '@libs/UserUtils'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, FundList, LoginList, Policy, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; -type CheckingMethod = () => boolean; - -type IndicatorOnyxProps = { - /** All the user's policies (from Onyx via withFullPolicy) */ - policies: OnyxCollection; - - /** List of bank accounts */ - bankAccountList: OnyxEntry; - - /** List of user cards */ - fundList: OnyxEntry; - - /** The user's wallet (coming from Onyx) */ - userWallet: OnyxEntry; - - /** Bank account attached to free plan */ - reimbursementAccount: OnyxEntry; - - /** Information about the user accepting the terms for payments */ - walletTerms: OnyxEntry; - - /** Login list for the user that is signed in */ - loginList: OnyxEntry; -}; - -type IndicatorProps = IndicatorOnyxProps; - -function Indicator({reimbursementAccount, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { - const theme = useTheme(); +function Indicator() { const styles = useThemeStyles(); - const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); - - // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and - // those should be cleaned out before doing any error checking - const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); - - // All of the error & info-checking methods are put into an array. This is so that using _.some() will return - // early as soon as the first error / info condition is returned. This makes the checks very efficient since - // we only care if a single error / info condition exists anywhere. - const errorCheckingMethods: CheckingMethod[] = [ - () => Object.keys(userWallet?.errors ?? {}).length > 0, - () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), - () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), - () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), - () => Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError), - () => - Object.values(cleanPolicies).some((cleanPolicy) => - PolicyUtils.hasSyncError( - cleanPolicy, - isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy), - ), - ), - () => SubscriptionUtils.hasSubscriptionRedDotError(), - () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, - () => !!loginList && UserUtils.hasLoginListError(loginList), - - // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) - () => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID, - ]; - const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList), () => SubscriptionUtils.hasSubscriptionGreenDotInfo()]; - const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod()); - const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()); + const {indicatorColor, status} = useSettingsStatus(); - const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success; const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)]; - return (shouldShowErrorIndicator || shouldShowInfoIndicator) && ; + return !!status && ; } Indicator.displayName = 'Indicator'; -export default withOnyx({ - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - bankAccountList: { - key: ONYXKEYS.BANK_ACCOUNT_LIST, - }, - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - fundList: { - key: ONYXKEYS.FUND_LIST, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, -})(Indicator); +export default Indicator; diff --git a/src/hooks/useSettingsStatus.ts b/src/hooks/useSettingsStatus.ts new file mode 100644 index 000000000000..eca0e0c7190b --- /dev/null +++ b/src/hooks/useSettingsStatus.ts @@ -0,0 +1,67 @@ +import { useOnyx } from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as SubscriptionUtils from '@libs/SubscriptionUtils'; +import * as UserUtils from '@libs/UserUtils'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import { isConnectionInProgress } from '@libs/actions/connections'; +import useTheme from './useTheme'; +import CONST from '@src/CONST'; +import { ValueOf } from 'type-fest'; + +type SettingsStatus = { + indicatorColor: string; + status: ValueOf | undefined +} + +function useSettingsStatus(): SettingsStatus { + const theme = useTheme(); + const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); + const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + + // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and + // those should be cleaned out before doing any error checking + const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); + + // All of the error & info-checking methods are put into an array. This is so that using _.some() will return + // early as soon as the first error / info condition is returned. This makes the checks very efficient since + // we only care if a single error / info condition exists anywhere. + const errorChecking = { + [CONST.SETTINGS_STATUS.HAS_USER_WALLET_ERRORS]: Object.keys(userWallet?.errors ?? {}).length > 0, + [CONST.SETTINGS_STATUS.HAS_PAYMENT_METHOD_ERROR]: PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), + [CONST.SETTINGS_STATUS.HAS_POLICY_ERRORS]: Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), + [CONST.SETTINGS_STATUS.HAS_CUSTOM_UNITS_ERROR]: Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), + [CONST.SETTINGS_STATUS.HAS_EMPLOYEE_LIST_ERROR]: Object.values(cleanPolicies).some(PolicyUtils.hasEmployeeListError), + [CONST.SETTINGS_STATUS.HAS_SYNC_ERRORS]: Object.values(cleanPolicies).some((cleanPolicy) => + PolicyUtils.hasSyncError( + cleanPolicy, + isConnectionInProgress(allConnectionSyncProgresses?.[`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${cleanPolicy?.id}`], cleanPolicy), + )), + [CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_ERRORS]: SubscriptionUtils.hasSubscriptionRedDotError(), + [CONST.SETTINGS_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS]: Object.keys(reimbursementAccount?.errors ?? {}).length > 0, + [CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_ERROR]: !!loginList && UserUtils.hasLoginListError(loginList), + // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) + [CONST.SETTINGS_STATUS.HAS_WALLET_TERMS_ERRORS]: Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID, + } + + const infoChecking = { + [CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_INFO]: !!loginList && UserUtils.hasLoginListInfo(loginList), + [CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_INFO]: SubscriptionUtils.hasSubscriptionGreenDotInfo() + }; + + const [error] = Object.entries(errorChecking).find(([, value]) => value) || [] + const [info] = Object.entries(infoChecking).find(([, value]) => value) || [] + + const status = (error || info) as ValueOf | undefined; + const indicatorColor = error ? theme.danger : theme.success; + + return { indicatorColor, status }; +} + +export default useSettingsStatus; diff --git a/src/languages/en.ts b/src/languages/en.ts index cb9fde8c053d..c24a4c023399 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -291,7 +291,7 @@ const translations = { currentMonth: 'Current month', ssnLast4: 'Last 4 digits of SSN', ssnFull9: 'Full 9 digits of SSN', - addressLine: ({lineNumber}: AddressLineParams) => `Address line ${lineNumber}`, + addressLine: ({ lineNumber }: AddressLineParams) => `Address line ${lineNumber}`, personalAddress: 'Personal address', companyAddress: 'Company address', noPO: 'PO boxes and mail drop addresses are not allowed', @@ -377,7 +377,7 @@ const translations = { verify: 'Verify', yesContinue: 'Yes, continue', websiteExample: 'e.g. https://www.expensify.com', - zipCodeExampleFormat: ({zipSampleFormat}: ZipCodeExampleFormatParams) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), + zipCodeExampleFormat: ({ zipSampleFormat }: ZipCodeExampleFormatParams) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), description: 'Description', with: 'with', shareCode: 'Share code', @@ -513,7 +513,7 @@ const translations = { composer: { noExtensionFoundForMimeType: 'No extension found for mime type', problemGettingImageYouPasted: 'There was a problem getting the image you pasted', - commentExceededMaxLength: ({formattedMaxLength}: FormattedMaxLengthParams) => `The maximum comment length is ${formattedMaxLength} characters.`, + commentExceededMaxLength: ({ formattedMaxLength }: FormattedMaxLengthParams) => `The maximum comment length is ${formattedMaxLength} characters.`, }, baseUpdateAppModal: { updateApp: 'Update app', @@ -526,7 +526,7 @@ const translations = { redirectedToDesktopApp: "We've redirected you to the desktop app.", youCanAlso: 'You can also', openLinkInBrowser: 'open this link in your browser', - loggedInAs: ({email}: LoggedInAsParams) => `You're logged in as ${email}. Click "Open link" in the prompt to log into the desktop app with this account.`, + loggedInAs: ({ email }: LoggedInAsParams) => `You're logged in as ${email}. Click "Open link" in the prompt to log into the desktop app with this account.`, doNotSeePrompt: "Can't see the prompt?", tryAgain: 'Try again', or: ', or', @@ -588,8 +588,8 @@ const translations = { phrase2: "Money talks. And now that chat and payments are in one place, it's also easy.", phrase3: 'Your payments get to you as fast as you can get your point across.', enterPassword: 'Please enter your password', - welcomeNewFace: ({login}: SignUpNewFaceCodeParams) => `${login}, it's always great to see a new face around here!`, - welcomeEnterMagicCode: ({login}: WelcomeEnterMagicCodeParams) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, + welcomeNewFace: ({ login }: SignUpNewFaceCodeParams) => `${login}, it's always great to see a new face around here!`, + welcomeEnterMagicCode: ({ login }: WelcomeEnterMagicCodeParams) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`, }, login: { hero: { @@ -598,8 +598,8 @@ const translations = { }, }, thirdPartySignIn: { - alreadySignedIn: ({email}: AlreadySignedInParams) => `You're already signed in as ${email}.`, - goBackMessage: ({provider}: GoBackMessageParams) => `Don't want to sign in with ${provider}?`, + 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.", signInAgreementMessage: 'By logging in, you agree to the', @@ -621,7 +621,7 @@ const translations = { writeSomething: 'Write something...', blockedFromConcierge: 'Communication is barred', fileUploadFailed: 'Upload failed. File is not supported.', - localTime: ({user, time}: LocalTimeParams) => `It's ${time} for ${user}`, + localTime: ({ user, time }: LocalTimeParams) => `It's ${time} for ${user}`, edited: '(edited)', emoji: 'Emoji', collapse: 'Collapse', @@ -639,9 +639,9 @@ const translations = { copyEmailToClipboard: 'Copy email to clipboard', markAsUnread: 'Mark as unread', markAsRead: 'Mark as read', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, - deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, - deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}?`, + editAction: ({ action }: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, + deleteAction: ({ action }: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}`, + deleteConfirmation: ({ action }: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'expense' : 'comment'}?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', joinThread: 'Join thread', @@ -657,13 +657,13 @@ const translations = { reportActionsView: { beginningOfArchivedRoomPartOne: 'You missed the party in ', beginningOfArchivedRoomPartTwo: ", there's nothing to see here.", - beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, + beginningOfChatHistoryDomainRoomPartOne: ({ domainRoom }: BeginningOfChatHistoryDomainRoomPartOneParams) => `Collaboration with everyone at ${domainRoom} starts here! 🎉\nUse `, beginningOfChatHistoryDomainRoomPartTwo: ' to chat with colleagues, share tips, and ask questions.', - beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, + beginningOfChatHistoryAdminRoomPartOne: ({ workspaceName }: BeginningOfChatHistoryAdminRoomPartOneParams) => `Collaboration among ${workspaceName} admins starts here! 🎉\nUse `, beginningOfChatHistoryAdminRoomPartTwo: ' to chat about topics such as workspace configurations and more.', - beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => + beginningOfChatHistoryAnnounceRoomPartOne: ({ workspaceName }: BeginningOfChatHistoryAnnounceRoomPartOneParams) => `Collaboration between all ${workspaceName} members starts here! 🎉\nUse `, - beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, + beginningOfChatHistoryAnnounceRoomPartTwo: ({ workspaceName }: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', beginningOfChatHistoryUserRoomPartTwo: ' related.', beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 Use this room to view, discuss, and pay invoices.', @@ -676,8 +676,8 @@ const translations = { chatWithAccountManager: 'Chat with your account manager here', sayHello: 'Say hello!', yourSpace: 'Your space', - welcomeToRoom: ({roomName}: WelcomeToRoomParams) => `Welcome to ${roomName}!`, - usePlusButton: ({additionalText}: UsePlusButtonParams) => `\nYou can also use the + button to ${additionalText}, or assign a task!`, + welcomeToRoom: ({ roomName }: WelcomeToRoomParams) => `Welcome to ${roomName}!`, + usePlusButton: ({ additionalText }: UsePlusButtonParams) => `\nYou can also use the + button to ${additionalText}, or assign a task!`, iouTypes: { pay: 'pay expenses', split: 'split an expense', @@ -702,16 +702,16 @@ const translations = { }, reportArchiveReasons: { [CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'This chat room has been archived.', - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}: ReportArchiveReasonsClosedParams) => `This chat is no longer active because ${displayName} closed their account.`, - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}: ReportArchiveReasonsMergedParams) => + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({ displayName }: ReportArchiveReasonsClosedParams) => `This chat is no longer active because ${displayName} closed their account.`, + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({ displayName, oldDisplayName }: ReportArchiveReasonsMergedParams) => `This chat is no longer active because ${oldDisplayName} has merged their account with ${displayName}.`, - [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName, shouldUseYou = false}: ReportArchiveReasonsRemovedFromPolicyParams) => + [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({ displayName, policyName, shouldUseYou = false }: ReportArchiveReasonsRemovedFromPolicyParams) => shouldUseYou ? `This chat is no longer active because you are no longer a member of the ${policyName} workspace.` : `This chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`, [CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `This chat is no longer active because ${policyName} is no longer an active workspace.`, - [CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({policyName}: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => + [CONST.REPORT.ARCHIVE_REASON.INVOICE_RECEIVER_POLICY_DELETED]: ({ policyName }: ReportArchiveReasonsInvoiceReceiverPolicyDeletedParams) => `This chat is no longer active because ${policyName} is no longer an active workspace.`, [CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED]: 'This booking is archived.', }, @@ -886,11 +886,11 @@ const translations = { settledElsewhere: 'Paid elsewhere', individual: 'Individual', business: 'Business', - settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), - settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), - 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`), + settleExpensify: ({ formattedAmount }: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), + settlePersonal: ({ formattedAmount }: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), + 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', finished: 'Finished', sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, @@ -915,33 +915,33 @@ const translations = { approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`, forwardedAmount: ({amount}: ForwardedAmountParams) => `approved ${amount}`, rejectedThisReport: 'rejected this report', - waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} adds a bank account.`, - adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}cancelled the ${amount} payment.`, - canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => + waitingOnBankAccount: ({ submitterDisplayName }: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} adds a bank account.`, + adminCanceledRequest: ({ manager, amount }: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}cancelled the ${amount} payment.`, + canceledRequest: ({ amount, submitterDisplayName }: CanceledRequestParams) => `canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, - settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => + settledAfterAddedBankAccount: ({ submitterDisplayName, amount }: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, - paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} elsewhere`, - paidWithExpensifyWithAmount: ({payer, amount}: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} with Expensify`, + paidElsewhereWithAmount: ({ payer, amount }: PaidElsewhereWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} elsewhere`, + paidWithExpensifyWithAmount: ({ payer, amount }: PaidWithExpensifyWithAmountParams) => `${payer ? `${payer} ` : ''}paid ${amount} with Expensify`, noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", changedTheExpense: 'changed the expense', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, - setTheDistanceMerchant: ({translatedChangedField, newMerchant, newAmountToDisplay}: SetTheDistanceMerchantParams) => + setTheRequest: ({ valueName, newValueToDisplay }: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, + setTheDistanceMerchant: ({ translatedChangedField, newMerchant, newAmountToDisplay }: SetTheDistanceMerchantParams) => `set the ${translatedChangedField} to ${newMerchant}, which set the amount to ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, - updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, - updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => + removedTheRequest: ({ valueName, oldValueToDisplay }: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, + updatedTheRequest: ({ valueName, newValueToDisplay, oldValueToDisplay }: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + updatedTheDistanceMerchant: ({ translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay }: UpdatedTheDistanceMerchantParams) => `changed the ${translatedChangedField} to ${newMerchant} (previously ${oldMerchant}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, - threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`, - threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`, - threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, + threadExpenseReportName: ({ formattedAmount, comment }: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`, + threadTrackReportName: ({ formattedAmount, comment }: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`, + threadPaySomeoneReportName: ({ formattedAmount, comment }: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, tagSelection: 'Select a tag to better organize your spend.', categorySelection: 'Select a category to better organize your spend.', error: { 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}`, + invalidTaxAmount: ({ amount }: RequestAmountParams) => `Maximum tax amount is ${amount}`, invalidSplit: 'The sum of splits must equal the total amount.', invalidSplitParticipants: 'Please enter an amount greater than zero for at least two participants.', invalidSplitYourself: 'Please enter a non-zero amount for your split.', @@ -963,7 +963,7 @@ const translations = { splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.', invalidMerchant: 'Please enter a correct merchant.', }, - waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`, + waitingOnEnabledWallet: ({ submitterDisplayName }: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`, enableWallet: 'Enable wallet', hold: 'Hold', unhold: 'Unhold', @@ -1005,7 +1005,7 @@ const translations = { changed: 'changed', removed: 'removed', transactionPending: 'Transaction pending.', - chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, + chooseARate: ({ unit }: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, unapprove: 'Unapprove', unapproveReport: 'Unapprove report', headsUp: 'Heads up!', @@ -1039,10 +1039,10 @@ const translations = { viewPhoto: 'View photo', imageUploadFailed: 'Image upload failed', 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) => + 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.`, - notAllowedExtension: ({allowedExtensions}: NotAllowedExtensionParams) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`, + notAllowedExtension: ({ allowedExtensions }: NotAllowedExtensionParams) => `Profile picture must be one of the following types: ${allowedExtensions.join(', ')}.`, }, profilePage: { profile: 'Profile', @@ -1087,7 +1087,7 @@ const translations = { helpTextAfterEmail: ' from multiple email addresses.', pleaseVerify: 'Please verify this contact method', 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}`, + enterMagicCode: ({ contactMethod }: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod}`, setAsDefault: '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', @@ -1196,7 +1196,7 @@ const translations = { enterCommand: 'Enter command', execute: 'Execute', noLogsAvailable: 'No logs available', - logSizeTooLarge: ({size}: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`, + logSizeTooLarge: ({ size }: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`, logs: 'Logs', viewConsole: 'View console', }, @@ -1422,7 +1422,7 @@ const translations = { }, cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.', validateCardTitle: "Let's make sure it's you", - enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details`, + enterMagicCode: ({ contactMethod }: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details`, }, workflowsPage: { workflowTitle: 'Spend', @@ -1472,7 +1472,7 @@ const translations = { }, }, approverInMultipleWorkflows: 'This member already belongs to another approval workflow. Any updates here will reflect there too.', - approverCircularReference: ({name1, name2}: ApprovalWorkflowErrorParams) => + approverCircularReference: ({ name1, name2 }: ApprovalWorkflowErrorParams) => `${name1} already approves reports to ${name2}. Please choose a different approver to avoid a circular workflow.`, emptyContent: { title: 'No members to display', @@ -1549,9 +1549,9 @@ const translations = { shipCard: 'Ship card', }, transferAmountPage: { - transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, + transfer: ({ amount }: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, instant: 'Instant (Debit card)', - instantSummary: ({rate, minAmount}: InstantSummaryParams) => `${rate}% fee (${minAmount} minimum)`, + instantSummary: ({ rate, minAmount }: InstantSummaryParams) => `${rate}% fee (${minAmount} minimum)`, ach: '1-3 Business days (Bank account)', achSummary: 'No fee', whichAccount: 'Which account?', @@ -1691,7 +1691,7 @@ const translations = { }, cannotGetAccountDetails: "Couldn't retrieve account details. Please try to sign in again.", loginForm: 'Login form', - notYou: ({user}: NotYouParams) => `Not ${user}?`, + notYou: ({ user }: NotYouParams) => `Not ${user}?`, }, onboarding: { welcome: 'Welcome!', @@ -1744,21 +1744,21 @@ const translations = { legalLastName: 'Legal last name', address: 'Address', error: { - dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, - dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, + dateShouldBeBefore: ({ dateString }: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, + dateShouldBeAfter: ({ dateString }: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, hasInvalidCharacter: 'Name can only include Latin characters.', incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams = {}) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, }, }, resendValidationForm: { linkHasBeenResent: 'Link has been re-sent', - weSentYouMagicSignInLink: ({login, loginType}: WeSentYouMagicSignInLinkParams) => `I've sent a magic sign-in link to ${login}. Please check your ${loginType} to sign in.`, + weSentYouMagicSignInLink: ({ login, loginType }: WeSentYouMagicSignInLinkParams) => `I've sent a magic sign-in link to ${login}. Please check your ${loginType} to sign in.`, resendLink: 'Resend link', }, unlinkLoginForm: { - toValidateLogin: ({primaryLogin, secondaryLogin}: ToValidateLoginParams) => + toValidateLogin: ({ primaryLogin, secondaryLogin }: ToValidateLoginParams) => `To validate ${secondaryLogin}, please resend the magic code from the Account Settings of ${primaryLogin}.`, - noLongerHaveAccess: ({primaryLogin}: NoLongerHaveAccessParams) => `If you no longer have access to ${primaryLogin}, please unlink your accounts.`, + noLongerHaveAccess: ({ primaryLogin }: NoLongerHaveAccessParams) => `If you no longer have access to ${primaryLogin}, please unlink your accounts.`, unlink: 'Unlink', linkSent: 'Link sent!', succesfullyUnlinkedLogin: 'Secondary login successfully unlinked!', @@ -1827,13 +1827,13 @@ const translations = { custom: 'Custom', }, untilTomorrow: 'Until tomorrow', - untilTime: ({time}: UntilTimeParams) => `Until ${time}`, + untilTime: ({ time }: UntilTimeParams) => `Until ${time}`, date: 'Date', time: 'Time', clearAfter: 'Clear after', whenClearStatus: 'When should we clear your status?', }, - stepCounter: ({step, total, text}: StepCounterParams) => { + stepCounter: ({ step, total, text }: StepCounterParams) => { let result = `Step ${step}`; if (total) { @@ -1928,7 +1928,7 @@ const translations = { 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}).`, errorMessageInvalidEmail: 'Invalid email', - userIsAlreadyMember: ({login, name}: UserIsAlreadyMemberParams) => `${login} is already a member of ${name}`, + userIsAlreadyMember: ({ login, name }: UserIsAlreadyMemberParams) => `${login} is already a member of ${name}`, }, onfidoStep: { acceptTerms: 'By continuing with the request to activate your Expensify Wallet, you confirm that you have read, understand, and accept', @@ -1982,7 +1982,7 @@ const translations = { checkTheBoxes: 'Please check the boxes below.', agreeToTerms: 'Agree to the terms and you’ll be good to go!', shortTermsForm: { - expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `The Expensify Wallet is issued by ${walletProgram}.`, + expensifyPaymentsAccount: ({ walletProgram }: WalletProgramParams) => `The Expensify Wallet is issued by ${walletProgram}.`, perPurchase: 'Per purchase', atmWithdrawal: 'ATM withdrawal', cashReload: 'Cash reload', @@ -1999,7 +1999,7 @@ const translations = { 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})`, + electronicFundsInstantFeeMin: ({ amount }: TermsParams) => `(min ${amount})`, }, longTermsForm: { listOfAllFees: 'A list of all Expensify Wallet fees', @@ -2018,11 +2018,11 @@ const translations = { "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) => + electronicFundsInstantDetails: ({ percentage, amount }: ElectronicFundsParams) => "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) => + fdicInsuranceBancorp: ({ amount }: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, @@ -2035,7 +2035,7 @@ const translations = { automated: 'Automated', liveAgent: 'Live agent', instant: 'Instant', - electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Min ${amount}`, + electronicFundsInstantFeeMin: ({ amount }: TermsParams) => `Min ${amount}`, }, }, activateStep: { @@ -2291,7 +2291,7 @@ const translations = { unavailable: 'Unavailable workspace', 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`, + goToRoom: ({ roomName }: GoToRoomParams) => `Go to ${roomName} room`, workspaceName: 'Workspace name', workspaceOwner: 'Owner', workspaceType: 'Workspace type', @@ -2300,7 +2300,7 @@ const translations = { moreFeatures: 'More features', requested: 'Requested', distanceRates: 'Distance rates', - welcomeNote: ({workspaceName}: WelcomeNoteParams) => + welcomeNote: ({ workspaceName }: WelcomeNoteParams) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, subscription: 'Subscription', markAsExported: 'Mark as manually entered', @@ -2957,7 +2957,7 @@ const translations = { customFeed: 'Custom feed', whoNeedsCardAssigned: 'Who needs a card assigned?', chooseCard: 'Choose a card', - chooseCardFor: ({assignee, feed}: AssignCardParams) => `Choose a card for ${assignee} from the ${feed} cards feed.`, + chooseCardFor: ({ assignee, feed }: AssignCardParams) => `Choose a card for ${assignee} from the ${feed} cards feed.`, noActiveCards: 'No active cards on this feed', somethingMightBeBroken: 'Or something might be broken. Either way, if you have any questions, just', contactConcierge: 'contact Concierge', @@ -3148,7 +3148,7 @@ const translations = { cardFeedRestrictDeletingTransaction: 'Restrict deleting transactions', cardFeedAllowDeletingTransaction: 'Allow deleting transactions', removeCardFeed: 'Remove card feed', - removeCardFeedTitle: ({feedName}: CompanyCardFeedNameParams) => `Remove ${feedName} feed`, + removeCardFeedTitle: ({ feedName }: CompanyCardFeedNameParams) => `Remove ${feedName} feed`, removeCardFeedDescription: 'Are you sure you want to remove this card feed? This will unassign all cards.', error: { feedNameRequired: 'Card feed name is required.', @@ -3345,7 +3345,7 @@ const translations = { people: { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', removeMembersPrompt: 'Are you sure you want to remove these members?', - removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => + removeMembersWarningPrompt: ({ memberName, ownerName }: RemoveMembersWarningPrompt) => `${memberName} is an approver in this workspace. When you unshare this workspace with them, we’ll replace them in the approval workflow with the workspace owner, ${ownerName}`, removeMembersTitle: 'Remove members', removeWorkspaceMemberButtonTitle: 'Remove from workspace', @@ -3994,12 +3994,12 @@ const translations = { public_announceDescription: 'Anyone can find this room', 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.`, + 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.', 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}`, + 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 workspace room. Please check your connection and try again.', @@ -4053,7 +4053,7 @@ const translations = { assignee: 'Assignee', completed: 'Completed', messages: { - created: ({title}: TaskCreatedActionParams) => `task for ${title}`, + created: ({ title }: TaskCreatedActionParams) => `task for ${title}`, completed: 'marked as complete', canceled: 'deleted task', reopened: 'marked as incomplete', @@ -4215,7 +4215,7 @@ const translations = { checkForUpdatesModal: { available: { title: 'Update available', - message: ({isSilentUpdating}: {isSilentUpdating: boolean}) => + 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', }, @@ -4237,24 +4237,24 @@ const translations = { noActivityYet: 'No activity yet', actions: { type: { - changeField: ({oldValue, newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} from ${oldValue} to ${newValue}`, - changeFieldEmpty: ({newValue, fieldName}: ChangeFieldParams) => `changed ${fieldName} to ${newValue}`, - changePolicy: ({fromPolicy, toPolicy}: ChangePolicyParams) => `changed workspace from ${fromPolicy} to ${toPolicy}`, - changeType: ({oldType, newType}: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`, - delegateSubmit: ({delegateUser, originalManager}: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`, + changeField: ({ oldValue, newValue, fieldName }: ChangeFieldParams) => `changed ${fieldName} from ${oldValue} to ${newValue}`, + changeFieldEmpty: ({ newValue, fieldName }: ChangeFieldParams) => `changed ${fieldName} to ${newValue}`, + changePolicy: ({ fromPolicy, toPolicy }: ChangePolicyParams) => `changed workspace from ${fromPolicy} to ${toPolicy}`, + changeType: ({ oldType, newType }: ChangeTypeParams) => `changed type from ${oldType} to ${newType}`, + delegateSubmit: ({ delegateUser, originalManager }: DelegateSubmitParams) => `sent this report to ${delegateUser} since ${originalManager} is on vacation`, exportedToCSV: `exported this report to CSV`, exportedToIntegration: { - automatic: ({label}: ExportedToIntegrationParams) => `exported this report to ${label}.`, - manual: ({label}: ExportedToIntegrationParams) => `marked this report as manually exported to ${label}.`, + automatic: ({ label }: ExportedToIntegrationParams) => `exported this report to ${label}.`, + manual: ({ label }: ExportedToIntegrationParams) => `marked this report as manually exported to ${label}.`, reimburseableLink: 'View out-of-pocket expenses.', nonReimbursableLink: 'View company card expenses.', - pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`, + pending: ({ label }: ExportedToIntegrationParams) => `started exporting this report to ${label}...`, }, integrationsMessage: ({errorMessage, label}: IntegrationSyncFailedParams) => `failed to export this report to ${label} ("${errorMessage}").`, managerAttachReceipt: `added a receipt`, managerDetachReceipt: `removed a receipt`, - markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`, - markedReimbursedFromIntegration: ({amount, currency}: MarkReimbursedFromIntegrationParams) => `paid ${currency}${amount} via integration`, + markedReimbursed: ({ amount, currency }: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`, + markedReimbursedFromIntegration: ({ amount, currency }: MarkReimbursedFromIntegrationParams) => `paid ${currency}${amount} via integration`, outdatedBankAccount: `couldn’t process the payment due to a problem with the payer’s bank account`, reimbursementACHBounce: `couldn’t process the payment, as the payer doesn’t have sufficient funds`, reimbursementACHCancelled: `canceled the payment`, @@ -4262,9 +4262,9 @@ const translations = { reimbursementDelayed: `processed the payment but it’s delayed by 1-2 more business days`, selectedForRandomAudit: `randomly selected for review`, selectedForRandomAuditMarkdown: `[randomly selected](https://help.expensify.com/articles/expensify-classic/reports/Set-a-random-report-audit-schedule) for review`, - share: ({to}: ShareParams) => `invited member ${to}`, - unshare: ({to}: UnshareParams) => `removed user ${to}`, - stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`, + share: ({ to }: ShareParams) => `invited member ${to}`, + unshare: ({ to }: UnshareParams) => `removed user ${to}`, + stripePaid: ({ amount, currency }: StripePaidParams) => `paid ${currency}${amount}`, takeControl: `took control`, unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`, integrationSyncFailed: ({label, errorMessage}: IntegrationSyncFailedParams) => `failed to sync with ${label} ("${errorMessage}")`, @@ -4275,8 +4275,8 @@ const translations = { }, }, chronos: { - oooEventSummaryFullDay: ({summary, dayCount, date}: OOOEventSummaryFullDayParams) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`, - oooEventSummaryPartialDay: ({summary, timePeriod, date}: OOOEventSummaryPartialDayParams) => `${summary} from ${timePeriod} on ${date}`, + oooEventSummaryFullDay: ({ summary, dayCount, date }: OOOEventSummaryFullDayParams) => `${summary} for ${dayCount} ${dayCount === 1 ? 'day' : 'days'} until ${date}`, + oooEventSummaryPartialDay: ({ summary, timePeriod, date }: OOOEventSummaryPartialDayParams) => `${summary} from ${timePeriod} on ${date}`, }, footer: { features: 'Features', @@ -4336,7 +4336,7 @@ const translations = { reply: 'Reply', from: 'From', in: 'in', - parentNavigationSummary: ({reportName, workspaceName}: ParentNavigationSummaryParams) => `From ${reportName}${workspaceName ? ` in ${workspaceName}` : ''}`, + parentNavigationSummary: ({ reportName, workspaceName }: ParentNavigationSummaryParams) => `From ${reportName}${workspaceName ? ` in ${workspaceName}` : ''}`, }, qrCodes: { copy: 'Copy URL', @@ -4489,17 +4489,17 @@ const translations = { }, violations: { allTagLevelsRequired: 'All tags required', - autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`, + autoReportedRejectedExpense: ({ rejectReason, rejectedBy }: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`, billableExpense: 'Billable no longer valid', cashExpenseWithNoReceipt: ({formattedLimit}: ViolationsCashExpenseWithNoReceiptParams = {}) => `Receipt required${formattedLimit ? ` over ${formattedLimit}` : ''}`, categoryOutOfPolicy: 'Category no longer valid', - conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `Applied ${surcharge}% conversion surcharge`, + conversionSurcharge: ({ surcharge }: ViolationsConversionSurchargeParams) => `Applied ${surcharge}% conversion surcharge`, customUnitOutOfPolicy: 'Rate not valid for this workspace', duplicatedTransaction: 'Duplicate', fieldRequired: 'Report fields are required', futureDate: 'Future date not allowed', - invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Marked up by ${invoiceMarkup}%`, - maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, + invoiceMarkup: ({ invoiceMarkup }: ViolationsInvoiceMarkupParams) => `Marked up by ${invoiceMarkup}%`, + maxAge: ({ maxAge }: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, missingCategory: 'Missing category', missingComment: 'Description required for selected category', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`, @@ -4518,13 +4518,13 @@ const translations = { }, modifiedDate: 'Date differs from scanned receipt', nonExpensiworksExpense: 'Non-Expensiworks expense', - overAutoApprovalLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Expense exceeds auto approval limit of ${formattedLimit}`, - overCategoryLimit: ({formattedLimit}: ViolationsOverCategoryLimitParams) => `Amount over ${formattedLimit}/person category limit`, - overLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, - overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, - perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`, + overAutoApprovalLimit: ({ formattedLimit }: ViolationsOverLimitParams) => `Expense exceeds auto approval limit of ${formattedLimit}`, + overCategoryLimit: ({ formattedLimit }: ViolationsOverCategoryLimitParams) => `Amount over ${formattedLimit}/person category limit`, + overLimit: ({ formattedLimit }: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, + overLimitAttendee: ({ formattedLimit }: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, + perDayLimit: ({ formattedLimit }: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', - receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => { + receiptRequired: ({ formattedLimit, category }: ViolationsReceiptRequiredParams) => { let message = 'Receipt required'; if (formattedLimit ?? category) { message += ' over'; @@ -4538,7 +4538,7 @@ const translations = { return message; }, reviewRequired: 'Review required', - rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { + rter: ({ brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member }: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin ? `Can't auto-match receipt due to broken bank connection which ${email} needs to fix` @@ -4863,9 +4863,9 @@ const translations = { } }, makeSureItIsYou: "Let's make sure it's you", - enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to add a copilot.`, + enterMagicCode: ({ contactMethod }: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to add a copilot.`, notAllowed: 'Not so fast...', - notAllowedMessageStart: ({accountOwnerEmail}: AccountOwnerParams) => `You don't have permission to take this action for ${accountOwnerEmail} as a`, + notAllowedMessageStart: ({ accountOwnerEmail }: AccountOwnerParams) => `You don't have permission to take this action for ${accountOwnerEmail} as a`, notAllowedMessageHyperLinked: ' limited access', notAllowedMessageEnd: ' copilot', }, @@ -4894,6 +4894,13 @@ const translations = { date: 'Date', time: 'Time', none: 'None', + settingsStatus: { + theresAReportAwaitingAction: "There's a report awaiting action", + theresAReportWithErrors: "There's a report with errors", + workspaceHasCustomUnitsErrors: 'Policy has custom units errors', + workspaceHasEmployeeListErrors: 'Policy has employee list errors', + profileHasErrors: 'Profile has errors', + }, }, }; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index d718de95861a..fb07c4c57fad 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -5,6 +5,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithFeedback} from '@components/Pressable'; import type {SearchQueryString} from '@components/Search/types'; +import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; @@ -26,6 +27,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import DebugTabView from './DebugTabView'; type BottomTabBarProps = { selectedTab: string | undefined; @@ -64,12 +66,14 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {activeWorkspaceID} = useActiveWorkspace(); + const reports = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const reportActions = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const transactionViolations = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [chatTabBrickRoad, setChatTabBrickRoad] = useState(getChatTabBrickRoad(activeWorkspaceID)); useEffect(() => { setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID)); - }, [activeWorkspaceID, transactionViolations]); + }, [activeWorkspaceID, transactionViolations, reports, reportActions]); const navigateToChats = useCallback(() => { if (selectedTab === SCREENS.HOME) { @@ -108,51 +112,58 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { }, [activeWorkspaceID, selectedTab]); return ( - - - - - - {chatTabBrickRoad && ( - - )} - - - - - - - - - - - - - + <> + + + + + + + {chatTabBrickRoad && ( + + )} + + + + + + + + + + + + + + - + ); } diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx new file mode 100644 index 000000000000..3fda0d4e4dbe --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx @@ -0,0 +1,176 @@ +import React, { useCallback, useMemo } from 'react'; +import { Text, View } from 'react-native'; +import { ValueOf } from 'type-fest'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import useLocalize from '@hooks/useLocalize'; +import useSettingsStatus from '@hooks/useSettingsStatus'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import { BrickRoad, getChatTabBrickRoadReport } from '@libs/WorkspacesSettingsUtils'; +import CONST from '@src/CONST'; +import { TranslationPaths } from '@src/languages/types'; +import ROUTES, { Route } from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +type DebugTabViewProps = { + selectedTab?: string; + chatTabBrickRoad: BrickRoad; + activeWorkspaceID?: string; +}; + +function getSettingsMessage(status: ValueOf | undefined): TranslationPaths { + switch (status) { + case CONST.SETTINGS_STATUS.HAS_CUSTOM_UNITS_ERROR: + return 'debug.settingsStatus.workspaceHasCustomUnitsErrors'; + case CONST.SETTINGS_STATUS.HAS_EMPLOYEE_LIST_ERROR: + return 'debug.settingsStatus.workspaceHasEmployeeListErrors'; + case CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_ERROR: + return 'debug.settingsStatus.profileHasErrors'; + case CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_INFO: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_PAYMENT_METHOD_ERROR: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_POLICY_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_INFO: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_SYNC_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_USER_WALLET_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + case CONST.SETTINGS_STATUS.HAS_WALLET_TERMS_ERRORS: + //TODO: CREATE TRANSLATION + return ''; + default: + //TODO: CREATE TRANSLATION + return ''; + } +} + +function getSettingsRoute(status: ValueOf | undefined): Route | undefined { + switch (status) { + case CONST.SETTINGS_STATUS.HAS_CUSTOM_UNITS_ERROR: + return ROUTES.SETTINGS_SUBSCRIPTION; + case CONST.SETTINGS_STATUS.HAS_EMPLOYEE_LIST_ERROR: + //TODO: USE POLICY ID + return ROUTES.WORKSPACE_MEMBERS.route; + case CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_ERROR: + return ROUTES.SETTINGS_PROFILE; + case CONST.SETTINGS_STATUS.HAS_LOGIN_LIST_INFO: + return ROUTES.SETTINGS_PROFILE; + case CONST.SETTINGS_STATUS.HAS_PAYMENT_METHOD_ERROR: + return ROUTES.SETTINGS_WALLET; + case CONST.SETTINGS_STATUS.HAS_POLICY_ERRORS: + //TODO: USE POLICY ID + return ROUTES.SETTINGS_WORKSPACES; + case CONST.SETTINGS_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS: + //TODO: USE POLICY ID + return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route as Route; + case CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_ERRORS: + return ROUTES.SETTINGS_SUBSCRIPTION; + case CONST.SETTINGS_STATUS.HAS_SUBSCRIPTION_INFO: + return ROUTES.SETTINGS_SUBSCRIPTION; + case CONST.SETTINGS_STATUS.HAS_SYNC_ERRORS: + //TODO: USE POLICY ID + return ROUTES.SETTINGS_WORKSPACES; + case CONST.SETTINGS_STATUS.HAS_USER_WALLET_ERRORS: + return ROUTES.SETTINGS_WALLET; + case CONST.SETTINGS_STATUS.HAS_WALLET_TERMS_ERRORS: + return ROUTES.SETTINGS_WALLET; + default: + return undefined; + } +} + +const DebugTabView = ({ selectedTab = '', chatTabBrickRoad, activeWorkspaceID }: DebugTabViewProps) => { + const StyleUtils = useStyleUtils(); + const theme = useTheme(); + const styles = useThemeStyles(); + const { translate } = useLocalize(); + const { status, indicatorColor } = useSettingsStatus(); + + const message = useMemo((): TranslationPaths | undefined => { + if (selectedTab === SCREENS.HOME) { + if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) { + return 'debug.settingsStatus.theresAReportAwaitingAction'; + } + if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) { + return 'debug.settingsStatus.theresAReportWithErrors'; + } + } + if (selectedTab === SCREENS.SETTINGS.ROOT) { + return getSettingsMessage(status); + } + }, [selectedTab, chatTabBrickRoad]); + + const indicator = useMemo(() => { + if (selectedTab === SCREENS.HOME) { + if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) { + return theme.success; + } + if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) { + return theme.danger; + } + } + if (selectedTab === SCREENS.SETTINGS.ROOT) { + if (status) { + return indicatorColor; + } + } + }, [selectedTab, chatTabBrickRoad]); + + const navigateTo = useCallback(() => { + if (selectedTab === SCREENS.HOME && !!chatTabBrickRoad) { + const report = getChatTabBrickRoadReport(activeWorkspaceID); + + if (report) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + } + } + if (selectedTab === SCREENS.SETTINGS.ROOT) { + const route = getSettingsRoute(status); + + if (route) { + Navigation.navigate(route); + } + } + }, [selectedTab, chatTabBrickRoad]); + + if (!([SCREENS.HOME, SCREENS.SETTINGS.ROOT] as string[]).includes(selectedTab) || !indicator) { + return null; + } + + return ( + + + + {message && translate(message)} + +