diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml index 47e13f6313a0..8995c7681d40 100644 --- a/.github/actions/composite/buildAndroidE2EAPK/action.yml +++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml @@ -74,7 +74,7 @@ runs: shell: bash - name: Upload APK - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 + uses: actions/upload-artifact@v4 with: name: ${{ inputs.ARTIFACT_NAME }} path: ${{ inputs.APP_OUTPUT_PATH }} diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 0d5879217ea0..add4879d8de1 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -156,7 +156,7 @@ jobs: run: mkdir zip - name: Download baseline APK - uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d + uses: actions/download-artifact@v4 id: downloadBaselineAPK with: name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }} @@ -170,7 +170,7 @@ jobs: run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" - name: Download delta APK - uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d + uses: actions/download-artifact@v4 id: downloadDeltaAPK with: name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }} @@ -184,7 +184,7 @@ jobs: - name: Copy e2e code into zip folder run: cp tests/e2e/dist/index.js zip/testRunner.ts - + - name: Copy profiler binaries into zip folder run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 640d1eaa1172..e1534bfba1c7 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -94,14 +94,14 @@ jobs: VERSION: ${{ env.VERSION_CODE }} - name: Archive Android sourcemaps - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: android-sourcemap-${{ github.ref_name }} path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map - name: Upload Android version to GitHub artifacts if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: app-production-release.aab path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab @@ -246,14 +246,14 @@ jobs: APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} - name: Archive iOS sourcemaps - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ios-sourcemap-${{ github.ref_name }} path: main.jsbundle.map - name: Upload iOS version to GitHub artifacts if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: New Expensify.ipa path: /Users/runner/work/App/App/New Expensify.ipa diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 10912aaeb436..5d70c16c28e0 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -125,7 +125,7 @@ jobs: MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }} - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: android path: ./android_paths.json @@ -217,7 +217,7 @@ jobs: S3_REGION: us-east-1 - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ios path: ./ios_paths.json @@ -321,7 +321,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} - name: Download Artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} - name: Read JSONs with android paths diff --git a/android/app/build.gradle b/android/app/build.gradle index f722d8426b7e..d6de6cf4fae0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,6 +15,7 @@ fullstory { org 'o-1WN56P-na1' enabledVariants 'all' logcatLevel 'debug' + recordOnStart false } react { @@ -107,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000602 - versionName "9.0.6-2" + versionCode 1009000603 + versionName "9.0.6-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/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index b65c66c986ad..1f412665fc2f 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -5,7 +5,8 @@ description: Details on requesting the Expensify Card as an employee To start using the Expensify Card, do the following: 1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card. 2. **Request the Card:** - - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” Use this task to message your admin team. + - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” + - Completing that task will send an in-product notification to your admin team that you requested the card. - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage. - Enter your address, and the physical card will be shipped within 3-5 business days. 3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage. diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md index 178621a62d90..35d232d2df67 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md @@ -24,7 +24,7 @@ Egencia controls the feed, so to connect Expensify you will need to: # How to Connect to a Central Purchasing Account Once your Egencia account manager has established the feed, you can automatically forward all Egencia booking receipts to a single Expensify account. To do this: 1. Open a chat with Concierge. -2. Tell Concierge “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”. +2. Tell Concierge the address of your central purchasing account, “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”. -The receipt the traveler receives is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. The reservation sent to the traveler's account is added to their mobile app Trips feature so that the traveler can easily keep tabs on upcoming travel and receive trip notifications. +A receipt will be sent to both the traveler and the central account. The receipt sent to the traveler is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. diff --git a/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md new file mode 100644 index 000000000000..ceb40254c607 --- /dev/null +++ b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md @@ -0,0 +1,8 @@ +--- +title: Expensify Travel demo video +description: Check out a demo of Expensify Travel +--- + +Check out a video of how Expensify Travel works below: + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3473c2bfcec8..3e0b46a292a7 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,11 +40,13 @@ CFBundleVersion - 9.0.6.2 + 9.0.6.3 FullStory OrgId o-1WN56P-na1 + RecordOnStart + ITSAppUsesNonExemptEncryption diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f413f0d1ae99..d13eca4d1cad 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.6.2 + 9.0.6.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 212ae9c1aa43..5125a598997f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.6 CFBundleVersion - 9.0.6.2 + 9.0.6.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ea83407c543f..45813001d07c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.6-2", + "version": "9.0.6-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.6-2", + "version": "9.0.6-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4d6f7f702ab0..1cd32c974031 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.6-2", + "version": "9.0.6-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.", diff --git a/src/CONST.ts b/src/CONST.ts index 3b879e10c345..ca2651a51a7c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -335,6 +335,8 @@ const CONST = { VERIFICATION_MAX_ATTEMPTS: 7, STATE: { VERIFYING: 'VERIFYING', + VALIDATING: 'VALIDATING', + SETUP: 'SETUP', PENDING: 'PENDING', OPEN: 'OPEN', }, @@ -361,7 +363,6 @@ const CONST = { DEFAULT_ROOMS: 'defaultRooms', VIOLATIONS: 'violations', DUPE_DETECTION: 'dupeDetection', - REPORT_FIELDS: 'reportFields', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', SPOTNANA_TRAVEL: 'spotnanaTravel', diff --git a/src/Expensify.tsx b/src/Expensify.tsx index f96c51961acc..6151f983e8d0 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -19,6 +19,7 @@ import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; import * as ActiveClientManager from './libs/ActiveClientManager'; import BootSplash from './libs/BootSplash'; +import FS from './libs/Fullstory'; import * as Growl from './libs/Growl'; import Log from './libs/Log'; import migrateOnyx from './libs/migrateOnyx'; @@ -147,6 +148,9 @@ function Expensify({ // Initialize this client as being an active client ActiveClientManager.init(); + // Initialize Fullstory lib + FS.init(); + // Used for the offline indicator appearing when someone is offline const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo(); diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 56006d599d6f..1e99e2132203 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -676,10 +676,15 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const, }, - WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { + WORKSPACE_ACCOUNTING_CARD_RECONCILIATION: { route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation', getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const, }, + WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: { + route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account', + getRoute: (policyID: string, connection: ValueOf) => + `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const, + }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, @@ -704,6 +709,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/edit', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const, }, + WORKSPACE_CATEGORY_PAYROLL_CODE: { + route: 'settings/workspaces/:policyID/categories/:categoryName/payroll-code', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/payroll-code` as const, + }, WORKSPACE_CATEGORY_GL_CODE: { route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const, @@ -842,6 +851,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card/issue-new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, }, + WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: { + route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4790ac3c6a32..8a71030dff44 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -330,6 +330,7 @@ const SCREENS = { SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account', SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced', SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account', + CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation', RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings', }, INITIAL: 'Workspace_Initial', @@ -341,6 +342,7 @@ const SCREENS = { RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New', + EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount', BILLS: 'Workspace_Bills', INVOICES: 'Workspace_Invoices', TRAVEL: 'Workspace_Travel', @@ -385,6 +387,7 @@ const SCREENS = { NAME: 'Workspace_Profile_Name', CATEGORY_CREATE: 'Category_Create', CATEGORY_EDIT: 'Category_Edit', + CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code', CATEGORY_GL_CODE: 'Category_GL_Code', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 0fd3cc0728ca..126c81961cee 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -1,6 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {ActivityIndicator, View} from 'react-native'; import Icon from '@components/Icon'; @@ -89,6 +89,9 @@ type ButtonProps = Partial & { /** Whether we should use the danger theme color */ danger?: boolean; + /** Whether we should display the button as a link */ + link?: boolean; + /** Should we remove the right border radius top + bottom? */ shouldRemoveRightBorderRadius?: boolean; @@ -205,6 +208,7 @@ function Button( id = '', accessibilityLabel = '', isSplitButton = false, + link = false, isContentCentered = false, ...rest }: ButtonProps, @@ -213,6 +217,7 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const [isHovered, setIsHovered] = useState(false); const renderContent = () => { if ('children' in rest) { @@ -233,6 +238,10 @@ function Button( danger && styles.buttonDangerText, !!icon && styles.textAlignLeft, textStyles, + link && styles.link, + link && isHovered && StyleUtils.getColorStyle(theme.linkHover), + link && styles.fontWeightNormal, + link && styles.fontSizeLabel, ]} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > @@ -343,6 +352,7 @@ function Button( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing text && shouldShowRightIcon ? styles.alignItemsStretch : undefined, innerStyles, + link && styles.bgTransparent, ]} hoverStyle={[ shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined, @@ -353,6 +363,8 @@ function Button( accessibilityLabel={accessibilityLabel} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} + onHoverIn={() => setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} > {renderContent()} {isLoading && ( diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a41f983434d8..3889c8597843 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -116,12 +116,11 @@ function Composer( }, [shouldClear, onClear]); useEffect(() => { - setSelection((prevSelection) => { - if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) { - return; - } - return selectionProp; - }); + if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) { + return; + } + setSelection(selectionProp); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [selectionProp]); /** diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx index d3e0459a11bb..b45a8418d9a3 100644 --- a/src/components/FlatList/index.tsx +++ b/src/components/FlatList/index.tsx @@ -150,10 +150,13 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false if (!isListRenderedRef.current) { return; } - requestAnimationFrame(() => { + const animationFrame = requestAnimationFrame(() => { prepareForMaintainVisibleContentPosition(); setupMutationObserver(); }); + return () => { + cancelAnimationFrame(animationFrame); + }; }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, ref); @@ -176,6 +179,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false const mutationObserver = mutationObserverRef.current; return () => { mutationObserver?.disconnect(); + mutationObserverRef.current = null; }; }, []); @@ -199,6 +203,10 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false ref={onRef} onLayout={(e) => { isListRenderedRef.current = true; + if (!mutationObserverRef.current) { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + } props.onLayout?.(e); }} /> diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index d3e5059d6f3f..702e6c384b58 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -91,18 +91,6 @@ type MoneyRequestAmountInputProps = { /** The width of inner content */ contentWidth?: number; - - /** - * Determines whether the amount should be reset. - */ - shouldResetAmount?: boolean; - - /** - * Callback function triggered when the amount is reset. - * - * @param resetValue - A boolean indicating whether the amount should be reset. - */ - onResetAmount?: (resetValue: boolean) => void; }; type Selection = { @@ -139,8 +127,6 @@ function MoneyRequestAmountInput( shouldKeepUserInput = false, autoGrow = true, contentWidth, - shouldResetAmount, - onResetAmount, ...props }: MoneyRequestAmountInputProps, forwardedRef: ForwardedRef, @@ -220,21 +206,10 @@ function MoneyRequestAmountInput( })); useEffect(() => { - const frontendAmount = onFormatAmount(amount, currency); - setCurrentAmount(frontendAmount); - if (shouldResetAmount) { - setSelection({ - start: frontendAmount.length, - end: frontendAmount.length, - }); - onResetAmount?.(false); - return; - } - if ((!currency || typeof amount !== 'number' || (formatAmountOnBlur && isTextInputFocused(textInput))) ?? shouldKeepUserInput) { return; } - + const frontendAmount = onFormatAmount(amount, currency); setCurrentAmount(frontendAmount); // Only update selection if the amount prop was changed from the outside and is not the same as the current amount we just computed @@ -245,7 +220,10 @@ function MoneyRequestAmountInput( end: frontendAmount.length, }); } - }, [amount, currency, formatAmountOnBlur, shouldKeepUserInput, onFormatAmount, shouldResetAmount, onResetAmount, currentAmount]); + + // we want to re-initialize the state only when the amount changes + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [amount, shouldKeepUserInput]); // Modifies the amount to match the decimals for changed currency. useEffect(() => { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index a9102eb77492..923d149dca10 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -297,7 +297,7 @@ function MoneyRequestConfirmationList({ const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant; const isCategoryRequired = !!policy?.requiresCategory; - const [shouldResetAmount, setShouldResetAmount] = useState(false); + useEffect(() => { if (shouldDisplayFieldError && didConfirmSplit) { setFormError('iou.error.genericSmartscanFailureMessage'); @@ -475,8 +475,6 @@ function MoneyRequestConfirmationList({ onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? -1, Number(value))} maxLength={formattedTotalAmount.length} contentWidth={formattedTotalAmount.length * 8} - shouldResetAmount={shouldResetAmount} - onResetAmount={(resetValue) => setShouldResetAmount(resetValue)} /> ), })); @@ -498,7 +496,6 @@ function MoneyRequestConfirmationList({ transaction?.comment?.splits, transaction?.splitShares, onSplitShareChange, - shouldResetAmount, ]); const isSplitModified = useMemo(() => { @@ -516,7 +513,6 @@ function MoneyRequestConfirmationList({ { IOU.resetSplitShares(transaction); - setShouldResetAmount(true); }} accessibilityLabel={CONST.ROLE.BUTTON} role={CONST.ROLE.BUTTON} diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4bd6d4103bee..d0f35990b507 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -62,7 +62,7 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) { {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ( <> - {ReportUtils.reportFieldsEnabled(report) && + {ReportUtils.isPaidGroupPolicyExpenseReport(report) && sortedPolicyReportFields.map((reportField) => { if (ReportUtils.isReportFieldOfTypeTitle(reportField)) { return null; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a0d73a1b2844..618503e19c33 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -216,15 +216,14 @@ function MoneyRequestView({ merchantTitle = translate('iou.receiptStatusTitle'); amountTitle = translate('iou.receiptStatusTitle'); } + const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal if (newBillable === TransactionUtils.getBillable(transaction)) { - Navigation.dismissModal(); return; } IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID, newBillable, policy, policyTagList, policyCategories); - Navigation.dismissModal(); }, [transaction, report, policy, policyTagList, policyCategories], ); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4a145d4e79e9..a435a5723670 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -128,6 +128,7 @@ function ReportPreview({ const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); + const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? ''); const {isSmallScreenWidth} = useWindowDimensions(); const [paymentType, setPaymentType] = useState(); @@ -203,6 +204,18 @@ function ReportPreview({ } }; + const getSettlementAmount = () => { + if (hasOnlyHeldExpenses) { + return ''; + } + + if (ReportUtils.hasHeldExpenses(iouReport?.reportID) && canAllowSettlement) { + return nonHeldAmount; + } + + return CurrencyUtils.convertToDisplayString(reimbursableSpend, iouReport?.currency); + }; + const getDisplayAmount = (): string => { if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); @@ -405,7 +418,7 @@ function ReportPreview({ {shouldShowSettlementButton && ( {isHoldMenuVisible && iouReport && requestType !== undefined && ( 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); - const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report); + const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy); const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); const additionalText = moneyRequestOptions .filter((item): item is Exclude => item !== CONST.IOU.TYPE.INVOICE) @@ -86,47 +87,47 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP {isPolicyExpenseChat && - (policy?.description ? ( + (welcomeMessage?.messageHtml ? ( { if (!canEditPolicyDescription) { return; } - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id)); + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1')); }} style={[styles.renderHTML, canEditPolicyDescription ? styles.cursorPointer : styles.cursorText]} accessibilityLabel={translate('reportDescriptionPage.roomDescription')} > - + ) : ( - {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')} + {welcomeMessage.phrase1} {ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)} - {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')} + {welcomeMessage.phrase2} {ReportUtils.getPolicyName(report)} - {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')} + {welcomeMessage.phrase3} ))} {isChatRoom && - (report?.description ? ( + (welcomeMessage?.messageHtml ? ( { if (ReportUtils.canEditReportDescription(report, policy)) { - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1')); return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1')); }} style={styles.renderHTML} accessibilityLabel={translate('reportDescriptionPage.roomDescription')} > - + ) : ( - {roomWelcomeMessage.phrase1} - {roomWelcomeMessage.showReportName && ( + {welcomeMessage.phrase1} + {welcomeMessage.showReportName && ( )} - {roomWelcomeMessage.phrase2 !== undefined && {roomWelcomeMessage.phrase2}} + {welcomeMessage.phrase2 !== undefined && {welcomeMessage.phrase2}} ))} {isSelfDM && ( - {translate('reportActionsView.beginningOfChatHistorySelfDM')} + {welcomeMessage.phrase1} )} {isSystemChat && ( - {translate('reportActionsView.beginningOfChatHistorySystemDM')} + {welcomeMessage.phrase1} )} {isDefault && ( - {translate('reportActionsView.beginningOfChatHistory')} + {welcomeMessage.phrase1} {displayNamesWithTooltips.map(({displayName, accountID}, index) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index ad77070c1b99..7888a8b26114 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -31,9 +31,19 @@ type ActionCellProps = { isLargeScreenWidth?: boolean; isSelected?: boolean; goToItem: () => void; + isChildListItem?: boolean; + parentAction?: string; }; -function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) { +function ActionCell({ + action = CONST.SEARCH.ACTION_TYPES.VIEW, + transactionID, + isLargeScreenWidth = true, + isSelected = false, + goToItem, + isChildListItem = false, + parentAction = '', +}: ActionCellProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -53,13 +63,11 @@ function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isL } }, [action, currentSearchHash, transactionID]); - if (!isLargeScreenWidth) { - return null; - } - const text = translate(actionTranslationsMap[action]); - if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) { + const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID); + + if ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE) { return ( + ) : null; + } + + if (action === CONST.SEARCH.ACTION_TYPES.REVIEW) { return (