diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml index f0ca77bdbf00..40dfc05e5448 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml +++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml @@ -14,7 +14,6 @@ inputs: GITHUB_TOKEN: description: "Github token for authentication" required: true - default: "${{ github.token }}" ANDROID: description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" required: true @@ -27,6 +26,12 @@ inputs: WEB: description: "Web job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + DATE: + description: "The date of deployment" + required: false + NOTE: + description: "Additional note from the deployer" + required: false runs: using: "node20" main: "./index.js" diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 9f97e4a72d20..b38b04141395 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -12713,9 +12713,15 @@ async function run() { const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true })); const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true })); const webResult = getDeployTableMessage(core.getInput('WEB', { required: true })); + const date = core.getInput('DATE'); + const note = core.getInput('NOTE'); function getDeployMessage(deployer, deployVerb, prTitle) { let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; - message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; + message += ` by https://github.com/${deployer} in version: ${version} `; + if (date) { + message += `on ${date}`; + } + message += `πŸš€`; message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { @@ -12723,6 +12729,9 @@ async function run() { message += '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; } + if (note) { + message += `\n\n_Note:_ ${note}`; + } return message; } if (isProd) { diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 71a5c7d5c6ee..e6424c89833a 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -55,9 +55,16 @@ async function run() { const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult); const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult); + const date = core.getInput('DATE'); + const note = core.getInput('NOTE'); + function getDeployMessage(deployer: string, deployVerb: string, prTitle?: string): string { let message = `πŸš€ [${deployVerb}](${workflowURL}) to ${isProd ? 'production' : 'staging'}`; - message += ` by https://github.com/${deployer} in version: ${version} πŸš€`; + message += ` by https://github.com/${deployer} in version: ${version} `; + if (date) { + message += `on ${date}`; + } + message += `πŸš€`; message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; @@ -67,6 +74,10 @@ async function run() { '\n\n@Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.'; } + if (note) { + message += `\n\n_Note:_ ${note}`; + } + return message; } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2ab19d13183a..53afe03720f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -649,34 +649,14 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - postGithubComment: - name: Post a GitHub comments on all deployed PRs when platforms are done building and deploying - runs-on: ubuntu-latest + postGithubComments: + uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} needs: [prep, android, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Get Release Pull Request List - id: getReleasePRList - uses: ./.github/actions/javascript/getDeployPullRequestList - with: - TAG: ${{ needs.prep.outputs.APP_VERSION }} - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - - - name: Comment on issues - uses: ./.github/actions/javascript/markPullRequestsAsDeployed - with: - PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - IS_PRODUCTION_DEPLOY: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - DEPLOY_VERSION: ${{ needs.prep.outputs.APP_VERSION }} - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - ANDROID: ${{ needs.android.result }} - DESKTOP: ${{ needs.desktop.result }} - IOS: ${{ needs.iOS.result }} - WEB: ${{ needs.web.result }} + with: + version: ${{ needs.prep.outputs.APP_VERSION }} + env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} + android: ${{ needs.android.result }} + ios: ${{ needs.iOS.result }} + web: ${{ needs.web.result }} + desktop: ${{ needs.desktop.result }} diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml new file mode 100644 index 000000000000..3893d3cf3f7c --- /dev/null +++ b/.github/workflows/postDeployComments.yml @@ -0,0 +1,118 @@ +name: Post Deploy Comments + +on: + workflow_call: + inputs: + version: + description: The version that was deployed + required: true + type: string + env: + description: The environment that was deployed (staging or prod) + required: true + type: string + android: + description: Android deploy status + required: true + type: string + ios: + description: iOS deploy status + required: true + type: string + web: + description: Web deploy status + required: true + type: string + desktop: + description: Desktop deploy status + required: true + type: string + workflow_dispatch: + inputs: + version: + description: The version that was deployed + required: true + type: string + env: + description: The environment that was deployed (staging or prod) + required: true + type: choice + options: + - staging + - production + android: + description: Android deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped + ios: + description: iOS deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped + web: + description: Web deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped + desktop: + description: Desktop deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped + date: + description: The date when this deploy occurred + required: false + type: string + note: + description: Any additional note you want to include with the deploy comment + required: false + type: string + +jobs: + postDeployComments: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Get pull request list + id: getPullRequestList + uses: ./.github/actions/javascript/getDeployPullRequestList + with: + TAG: ${{ inputs.version }} + GITHUB_TOKEN: ${{ github.token }} + IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }} + + - name: Comment on issues + uses: ./.github/actions/javascript/markPullRequestsAsDeployed + with: + PR_LIST: ${{ steps.getPullRequestList.outputs.PR_LIST }} + IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }} + DEPLOY_VERSION: ${{ inputs.version }} + GITHUB_TOKEN: ${{ github.token }} + ANDROID: ${{ inputs.android }} + DESKTOP: ${{ inputs.desktop }} + IOS: ${{ inputs.ios }} + WEB: ${{ inputs.web }} + DATE: ${{ inputs.date }} + NOTE: ${{ inputs.note }} diff --git a/__mocks__/react-native-haptic-feedback.ts b/__mocks__/react-native-haptic-feedback.ts new file mode 100644 index 000000000000..6d20b410d835 --- /dev/null +++ b/__mocks__/react-native-haptic-feedback.ts @@ -0,0 +1,5 @@ +import type HapticFeedback from 'react-native-haptic-feedback'; + +const RNHapticFeedback: typeof HapticFeedback = {trigger: jest.fn()}; + +export default RNHapticFeedback; diff --git a/android/app/build.gradle b/android/app/build.gradle index 0594d6afc211..833f8290e5e6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009003904 - versionName "9.0.39-4" + versionCode 1009004001 + versionName "9.0.40-1" // 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/android/gradle.properties b/android/gradle.properties index 87333d20f743..46cd98554d29 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -55,3 +55,5 @@ MYAPP_UPLOAD_KEY_ALIAS=ReactNativeChat-Key-Alias disableFrameProcessors=true android.nonTransitiveRClass=false + +org.gradle.parallel=true diff --git a/docs/articles/new-expensify/workspaces/Add-approvals.md b/docs/articles/new-expensify/workspaces/Add-approvals.md new file mode 100644 index 000000000000..5d8c1f733287 --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Add-approvals.md @@ -0,0 +1,73 @@ +--- +title: Add approvals +description: Add approvals to your workspace to require additional approval before authorizing payments. +--- +
+ +# Add approvals + +Each Expensify workspace can be configured to require additional approvals before payments are authorized. Once approvals are enabled on a workspace, admins will be able to set a default approval workflow to apply to all members of the workspace, as well as set custom approval workflows for specific members. + +When workspace members submit expenses, the expenses will require approval from each approver in their approval workflow before payment is authorized. + +## Add approvals on a workspace + +**To enable Add approvals on a workspace you are an admin on:** + +1. Click your profile image or icon in the bottom left menu +2. Click **Workspaces** in the left menu +3. Select the workspace where you want to add approvals +4. Click **Workflows** in the left menu +5. Click the toggle next to **Add approvals** + +Toggling on **Add approvals** will reveal an option to set a default approval workflow. + +## Configure approval workflows + +**To configure the default approval workflow for the workspace:** + +1. Click your profile image or icon in the bottom left menu +2. Click **Workspaces** in the left menu +3. Select the workspace where you want to set the approval workflow +4. Click **Workflows** in the left menu +5. Under **Expenses from Everyone**, click on **First approver** +6. Select the workspace member who should be the first approver in the approval workflow +7. Under **Additional approver**, continue selecting workspace members until all the desired approvers are listed +8. Click **Save** + +Note: When Add approvals is enabled, the workspace must have a default approval workflow. + +**To set an approval workflow that applies only to specific workspace members:** + +1. Click your profile image or icon in the bottom left menu +2. Click **Workspaces** in the left menu +3. Select the workspace where you want to add approvals +4. Click **Workflows** in the left menu +5. Under **Add approvals**, click on **Add approval workflow** +6. Choose the workspace member whose expenses should go through the custom approval workfow +7. Click **Next** +8. Choose the workspace member who should be the first approver on submitted expenses in the approval workflow +9. Click **Next** +10. Click **Additional approver** to continue selecting workspace members until all the desired approvers are listed +11. Click **Add workflow** to save it + +## Edit or delete approval workflows + +**To edit an approval workflow:** + +1. On the **Workflows** page, click the approval workflow that should be edited +2. Click on the Approver field for the approval level where the edit should be made +3. Choose the workspace member who should be set as the approver for that level, or deselect them to remove the approval level from the workflow +4. Click **Save** + +**To delete an approval workflow:** + +1. On the **Workflows** page, click the approval workflow that shoudld be deleted +2. Click **Delete** +3. In the window that appears,click **Delete** again + +# FAQ + +## Can an employee have more than one approval workflow? +No, each employee can have only one approval workflow + diff --git a/docs/articles/new-expensify/workspaces/Set-distance-rates.md b/docs/articles/new-expensify/workspaces/Set-distance-rates.md new file mode 100644 index 000000000000..c434f34d2cef --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Set-distance-rates.md @@ -0,0 +1,50 @@ +--- +title: Set Distance Rates +description: Set distance rates on your Expensify workspace +--- +
+ +# Set Distance eates + +Each Expensify workspace can be configured with one or more distance rates. Once distance rates are enabled on your workspace, employees will be able to choose between the available rates to create distance expenses. + +## Enable distance rates on a workspace + +**To enable distance rates on a workspace you are an admin on:** + +1. Click your profile image or icon in the bottom left menu +2. Click **Workspaces** in the left menu +3. Select the workspace where you want to enable distance rates +4. Click **More features** in the left menu +5. Click the toggle next to **Distance rates** + +After toggling on distance rates, you will see a new **Distance rates** option in the left menu. + +## Add, delete, or edit distance rates + +**To add a distance rate:** + +1. Click your profile image or icon in the bottom left menu +2. Click **Workspaces** in the left menu +3. Select the workspace where you want to add distance rates +4. Click **Distance rates** in the left menu +5. Click **Add rate** in the top right +6. Enter a value, then click **Save** + +**To enable, disable, edit or delete a single distance rate:** + +1. Click the distance rate on the **Distance rates** settings page +2. To enable or disable the distance rate, click the toggle next to **Enable rate**, then click **Save** +3. To edit the rate amount, click on the amount field, enter the new value, then click **Save** +4. To permanently delete the distance rate, click **Delete** + +Note: When Distance rates is enabled, the workspace must have at least one enabled distance rate. + +**To enable, disable, edit or delete distance rates in bulk:** + +1. On the **Distance rates** settings page, click the checkboxes next to the distance rates that should me modified +2. Click β€œx selected” at the top right +3. To enable or disable all the selected distance rates, click **Enable rates** or **Disable rates** +4. To permanently delete the distance rates, click **Delete rates** + +Note: When Distance rates are enabled, the workspace must have at least one enabled distance rate. diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 78fb5d53d9e9..d2f181f6b7f4 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index ac2c76a118d5..c0afa40ecb29 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5f6051c85745..4eeb658f3347 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.39 + 9.0.40 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.39.4 + 9.0.40.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c29a15f26438..7dc1b1416139 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.39 + 9.0.40 CFBundleSignature ???? CFBundleVersion - 9.0.39.4 + 9.0.40.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2b43cf12b38a..83fa9ece1deb 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.39 + 9.0.40 CFBundleVersion - 9.0.39.4 + 9.0.40.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a801a7c4de1c..beac64acd083 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2451,7 +2451,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReactNativeHapticFeedback (2.3.1): + - RNReactNativeHapticFeedback (2.3.3): - DoubleConversion - glog - hermes-engine @@ -3233,7 +3233,7 @@ SPEC CHECKSUMS: RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 - RNReactNativeHapticFeedback: 31833c3ef341d716dbbd9d64e940f0c230db46f6 + RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 RNReanimated: 76901886830e1032f16bbf820153f7dc3f02d51d RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 diff --git a/package-lock.json b/package-lock.json index c1896b581645..6af9c1981bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.39-4", + "version": "9.0.40-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.39-4", + "version": "9.0.40-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -85,7 +85,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "2.18.0", "react-native-google-places-autocomplete": "2.5.6", - "react-native-haptic-feedback": "^2.3.1", + "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", @@ -34543,9 +34543,9 @@ } }, "node_modules/react-native-haptic-feedback": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.1.tgz", - "integrity": "sha512-dPfjV4iVHfhVyfG+nRd88ygjahbdup7KFZDM5L2aNIAzqbNtKxHZn5O1pHegwSj1t15VJliu0GyTX7XpBDeXUw==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz", + "integrity": "sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ==", "workspaces": [ "example" ], diff --git a/package.json b/package.json index 577eac08c6d5..deff4054ce01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.39-4", + "version": "9.0.40-1", "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.", @@ -142,7 +142,7 @@ "react-native-fs": "^2.20.0", "react-native-gesture-handler": "2.18.0", "react-native-google-places-autocomplete": "2.5.6", - "react-native-haptic-feedback": "^2.3.1", + "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", diff --git a/patches/react-native-haptic-feedback+2.3.1.patch b/patches/react-native-haptic-feedback+2.3.1.patch deleted file mode 100644 index 799bdaf7e53e..000000000000 --- a/patches/react-native-haptic-feedback+2.3.1.patch +++ /dev/null @@ -1,56 +0,0 @@ -diff --git a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h -index c1498b9..250df1f 100644 ---- a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h -+++ b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedback.h -@@ -1,5 +1,5 @@ - #ifdef RCT_NEW_ARCH_ENABLED --#import "RNHapticFeedbackSpec.h" -+#import - - @interface RNHapticFeedback : NSObject - #else -diff --git a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h b/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h -deleted file mode 100644 -index 6f0f81d..0000000 ---- a/node_modules/react-native-haptic-feedback/ios/RNHapticFeedback/RNHapticFeedbackSpec.h -+++ /dev/null -@@ -1,15 +0,0 @@ --// --// RNHapticFeedbackSpec.h --// RNHapticFeedback --// --// Created by Michael Kuczera on 05.08.24. --// Copyright Β© 2024 Facebook. All rights reserved. --// --#import -- --@protocol NativeHapticFeedbackSpec -- --// Indicates whether the device supports haptic feedback --- (Boolean)supportsHaptic; -- --@end -diff --git a/node_modules/react-native-haptic-feedback/package.json b/node_modules/react-native-haptic-feedback/package.json -index 86dfaa4..9cec8e4 100644 ---- a/node_modules/react-native-haptic-feedback/package.json -+++ b/node_modules/react-native-haptic-feedback/package.json -@@ -6,18 +6,7 @@ - "source": "src/index.ts", - "main": "./lib/commonjs/index.js", - "module": "./lib/module/index.js", -- "exports": { -- ".": { -- "import": { -- "types": "./lib/typescript/module/src/index.d.ts", -- "default": "./lib/module/index.js" -- }, -- "require": { -- "types": "./lib/typescript/commonjs/src/index.d.ts", -- "default": "./lib/commonjs/index.js" -- } -- } -- }, -+ "types": "./lib/typescript/module/src/index.d.ts", - "scripts": { - "typecheck": "tsc --noEmit --project tsconfig.test.json", - "test": "jest", diff --git a/src/CONST.ts b/src/CONST.ts index 9ee9ec4d9147..7d0cd3c4cf54 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -171,7 +171,7 @@ const CONST = { }, // Note: Group and Self-DM excluded as these are not tied to a Workspace - WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], + WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, @@ -2097,6 +2097,9 @@ const CONST = { ACCESS_VARIANTS: { CREATE: 'create', }, + PAGE_INDEX: { + CONFIRM: 'confirm', + }, PAYMENT_SELECTED: { BBA: 'BBA', PBA: 'PBA', @@ -4556,7 +4559,7 @@ const CONST = { { type: 'setupTags', autoCompleted: false, - title: 'Set up tags (optional)', + title: 'Set up tags', 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' + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c0ec944b71e1..2b959ba4ddba 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -74,7 +74,7 @@ const ROUTES = { SUBMIT_EXPENSE: 'submit-expense', FLAG_COMMENT: { route: 'flag/:reportID/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const, + getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo), }, CHAT_FINDER: 'chat-finder', PROFILE: { @@ -287,11 +287,12 @@ const ROUTES = { }, EDIT_REPORT_FIELD_REQUEST: { route: 'r/:reportID/edit/policyField/:policyID/:fieldID', - getRoute: (reportID: string, policyID: string, fieldID: string) => `r/${reportID}/edit/policyField/${policyID}/${fieldID}` as const, + getRoute: (reportID: string, policyID: string, fieldID: string, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/edit/policyField/${policyID}/${encodeURIComponent(fieldID)}` as const, backTo), }, REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', - getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo), }, ATTACHMENTS: { route: 'attachment', @@ -300,19 +301,19 @@ const ROUTES = { }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', - getRoute: (reportID: string) => `r/${reportID}/participants` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants` as const, backTo), }, REPORT_PARTICIPANTS_INVITE: { route: 'r/:reportID/participants/invite', - getRoute: (reportID: string) => `r/${reportID}/participants/invite` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/invite` as const, backTo), }, REPORT_PARTICIPANTS_DETAILS: { route: 'r/:reportID/participants/:accountID', - getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}` as const, + getRoute: (reportID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/${accountID}` as const, backTo), }, REPORT_PARTICIPANTS_ROLE_SELECTION: { route: 'r/:reportID/participants/:accountID/role', - getRoute: (reportID: string, accountID: number) => `r/${reportID}/participants/${accountID}/role` as const, + getRoute: (reportID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/participants/${accountID}/role` as const, backTo), }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', @@ -320,65 +321,65 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS_EXPORT: { route: 'r/:reportID/details/export/:connectionName', - getRoute: (reportID: string, connectionName: ConnectionName) => `r/${reportID}/details/export/${connectionName}` as const, + getRoute: (reportID: string, connectionName: ConnectionName, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/export/${connectionName}` as const, backTo), }, REPORT_SETTINGS: { route: 'r/:reportID/settings', - getRoute: (reportID: string) => `r/${reportID}/settings` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings` as const, backTo), }, REPORT_SETTINGS_NAME: { route: 'r/:reportID/settings/name', - getRoute: (reportID: string) => `r/${reportID}/settings/name` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/name` as const, backTo), }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', - getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/notification-preferences` as const, backTo), }, REPORT_SETTINGS_WRITE_CAPABILITY: { route: 'r/:reportID/settings/who-can-post', - getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/who-can-post` as const, backTo), }, REPORT_SETTINGS_VISIBILITY: { route: 'r/:reportID/settings/visibility', - getRoute: (reportID: string) => `r/${reportID}/settings/visibility` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/visibility` as const, backTo), }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, + getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/split/${reportActionID}` as const, backTo), }, TASK_TITLE: { route: 'r/:reportID/title', - getRoute: (reportID: string) => `r/${reportID}/title` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/title` as const, backTo), }, REPORT_DESCRIPTION: { route: 'r/:reportID/description', - getRoute: (reportID: string) => `r/${reportID}/description` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/description` as const, backTo), }, TASK_ASSIGNEE: { route: 'r/:reportID/assignee', - getRoute: (reportID: string) => `r/${reportID}/assignee` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/assignee` as const, backTo), }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', - getRoute: (reportID: string) => `r/${reportID}/notes` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/notes` as const, backTo), }, PRIVATE_NOTES_EDIT: { route: 'r/:reportID/notes/:accountID/edit', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit` as const, + getRoute: (reportID: string, accountID: string | number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/notes/${accountID}/edit` as const, backTo), }, ROOM_MEMBERS: { route: 'r/:reportID/members', - getRoute: (reportID: string) => `r/${reportID}/members` as const, + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/members` as const, backTo), }, ROOM_MEMBER_DETAILS: { route: 'r/:reportID/members/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/members/${accountID}` as const, + getRoute: (reportID: string, accountID: string | number, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/members/${accountID}` as const, backTo), }, ROOM_INVITE: { route: 'r/:reportID/invite/:role?', - getRoute: (reportID: string, role?: string) => { + getRoute: (reportID: string, role?: string, backTo?: string) => { const route = role ? (`r/${reportID}/invite/${role}` as const) : (`r/${reportID}/invite` as const); - return route; + return getUrlWithBackToParam(route, backTo); }, }, MONEY_REQUEST_HOLD_REASON: { @@ -406,9 +407,9 @@ const ROUTES = { `${action as string}/${iouType as string}/confirmation/${transactionID}/${reportID}${participantsAutoAssigned ? '?participantsAutoAssigned=true' : ''}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { - route: ':action/:iouType/amount/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/amount/:transactionID/:reportID/:pageIndex?', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex: string, backTo = '') => + getUrlWithBackToParam(`${action as string}/${iouType as string}/amount/${transactionID}/${reportID}/${pageIndex}`, backTo), }, MONEY_REQUEST_STEP_TAX_RATE: { route: ':action/:iouType/taxRate/:transactionID/:reportID?', @@ -495,6 +496,10 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, + SETTINGS_TAGS_ROOT: { + route: 'settings/:policyID/tags', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo), + }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID?: string, pageIndex = '', backTo = '') => @@ -536,12 +541,27 @@ const ROUTES = { IOU_SEND_ADD_DEBIT_CARD: 'pay/new/add-debit-card', IOU_SEND_ENABLE_PAYMENTS: 'pay/new/enable-payments', - NEW_TASK: 'new/task', - NEW_TASK_ASSIGNEE: 'new/task/assignee', + NEW_TASK: { + route: 'new/task', + getRoute: (backTo?: string) => getUrlWithBackToParam('new/task', backTo), + }, + NEW_TASK_ASSIGNEE: { + route: 'new/task/assignee', + getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/assignee', backTo), + }, NEW_TASK_SHARE_DESTINATION: 'new/task/share-destination', - NEW_TASK_DETAILS: 'new/task/details', - NEW_TASK_TITLE: 'new/task/title', - NEW_TASK_DESCRIPTION: 'new/task/description', + NEW_TASK_DETAILS: { + route: 'new/task/details', + getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/details', backTo), + }, + NEW_TASK_TITLE: { + route: 'new/task/title', + getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/title', backTo), + }, + NEW_TASK_DESCRIPTION: { + route: 'new/task/description', + getRoute: (backTo?: string) => getUrlWithBackToParam('new/task/description', backTo), + }, TEACHERS_UNITE: 'settings/teachersunite', I_KNOW_A_TEACHER: 'settings/teachersunite/i-know-a-teacher', @@ -1097,7 +1117,10 @@ const ROUTES = { route: 'referral/:contentType', getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, - PROCESS_MONEY_REQUEST_HOLD: 'hold-expense-educational', + PROCESS_MONEY_REQUEST_HOLD: { + route: 'hold-expense-educational', + getRoute: (backTo?: string) => getUrlWithBackToParam('hold-expense-educational', backTo), + }, TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', @@ -1126,39 +1149,39 @@ const ROUTES = { }, TRANSACTION_DUPLICATE_REVIEW_PAGE: { route: 'r/:threadReportID/duplicates/review', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE: { route: 'r/:threadReportID/duplicates/review/merchant', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/merchant` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/merchant` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE: { route: 'r/:threadReportID/duplicates/review/category', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/category` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/category` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE: { route: 'r/:threadReportID/duplicates/review/tag', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tag` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/tag` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE: { route: 'r/:threadReportID/duplicates/review/tax-code', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/tax-code` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/tax-code` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE: { route: 'r/:threadReportID/duplicates/review/description', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/description` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/description` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE: { route: 'r/:threadReportID/duplicates/review/reimbursable', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/reimbursable` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/reimbursable` as const, backTo), }, TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE: { route: 'r/:threadReportID/duplicates/review/billable', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/review/billable` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/review/billable` as const, backTo), }, TRANSACTION_DUPLICATE_CONFIRMATION_PAGE: { route: 'r/:threadReportID/duplicates/confirm', - getRoute: (threadReportID: string) => `r/${threadReportID}/duplicates/confirm` as const, + getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/confirm` as const, backTo), }, POLICY_ACCOUNTING_XERO_IMPORT: { route: 'settings/workspaces/:policyID/accounting/xero/import', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 920bd48dd42e..395f1c4d5fb1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -244,6 +244,8 @@ const SCREENS = { SETTINGS_CATEGORIES_ROOT: 'Settings_Categories', }, + SETTINGS_TAGS_ROOT: 'Settings_Tags', + REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', NAME: 'Report_Settings_Name', diff --git a/src/components/AccountSwitcherSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx index 3faf7e563f3c..379a4094e032 100644 --- a/src/components/AccountSwitcherSkeletonView/index.tsx +++ b/src/components/AccountSwitcherSkeletonView/index.tsx @@ -22,7 +22,7 @@ function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.A const StyleUtils = useStyleUtils(); const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize); const avatarPlaceholderRadius = avatarPlaceholderSize / 2; - const startPositionX = 30; + const startPositionX = avatarPlaceholderRadius; return ( diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 1cd1bfb36d83..07845eca37ba 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -79,10 +79,15 @@ function AvatarWithDisplayName({ actorAccountID.current = parentReportAction?.actorAccountID ?? -1; }, [parentReportActions, report]); + const goToDetailsPage = useCallback(() => { + ReportUtils.navigateToDetailsPage(report, Navigation.getReportRHPActiveRoute()); + }, [report]); + const showActorDetails = useCallback(() => { // We should navigate to the details page if the report is a IOU/expense report if (shouldEnableDetailPageNavigation) { - return ReportUtils.navigateToDetailsPage(report); + goToDetailsPage(); + return; } if (ReportUtils.isExpenseReport(report) && report?.ownerAccountID) { @@ -107,7 +112,7 @@ function AvatarWithDisplayName({ // Report detail route is added as fallback but based on the current implementation this route won't be executed Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); } - }, [report, shouldEnableDetailPageNavigation]); + }, [report, shouldEnableDetailPageNavigation, goToDetailsPage]); const headerView = ( @@ -172,7 +177,7 @@ function AvatarWithDisplayName({ return ( ReportUtils.navigateToDetailsPage(report)} + onPress={goToDetailsPage} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} accessibilityLabel={title} role={CONST.ROLE.BUTTON} diff --git a/src/components/Button/validateSubmitShortcut/index.ts b/src/components/Button/validateSubmitShortcut/index.ts index f8cea44f73d6..29ba071c25f2 100644 --- a/src/components/Button/validateSubmitShortcut/index.ts +++ b/src/components/Button/validateSubmitShortcut/index.ts @@ -11,7 +11,7 @@ import type ValidateSubmitShortcut from './types'; const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading, event) => { const eventTarget = event?.target as HTMLElement; - if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') { + if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA' || (eventTarget?.contentEditable === 'true' && eventTarget.ariaMultiLine)) { return false; } diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 876f1a745403..8eb991b17b63 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -22,6 +22,7 @@ function EmptyStateComponent({ buttonAction, containerStyles, title, + titleStyles, subtitle, headerStyles, headerContentStyles, @@ -30,7 +31,7 @@ function EmptyStateComponent({ }: EmptyStateComponentProps) { const styles = useThemeStyles(); const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO); - const {isSmallScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => { if (!event) { @@ -82,7 +83,10 @@ function EmptyStateComponent({ }, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo, lottieWebViewStyles]); return ( - + {HeaderComponent} - - {title} - {subtitle} + + {title} + {typeof subtitle === 'string' ? {subtitle} : subtitle} {!!buttonText && !!buttonAction && (