diff --git a/.env.staging b/.env.staging index 17d82ac2d136..c789087ebded 100644 --- a/.env.staging +++ b/.env.staging @@ -6,4 +6,4 @@ EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 PUSHER_APP_KEY=268df511a204fbb60884 USE_WEB_PROXY=false ENVIRONMENT=staging -SEND_CRASH_REPORTS=true \ No newline at end of file +SEND_CRASH_REPORTS=true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2dfd9348d961..b8de634a489f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,9 +23,9 @@ PROPOSAL: diff --git a/.github/scripts/checkParser.sh b/.github/scripts/checkParser.sh new file mode 100755 index 000000000000..d63f4a01452f --- /dev/null +++ b/.github/scripts/checkParser.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +ROOT_DIR=$(dirname "$(dirname "$(dirname "${BASH_SOURCE[0]}")")") +cd "$ROOT_DIR" || exit 1 + +autocomplete_parser_backup="src/libs/SearchParser/autocompleteParser.js.bak" +search_parser_backup="src/libs/SearchParser/searchParser.js.bak" + +#Copying the current .js parser files +cp src/libs/SearchParser/autocompleteParser.js "$autocomplete_parser_backup" 2>/dev/null +cp src/libs/SearchParser/searchParser.js "$search_parser_backup" 2>/dev/null + +#Running the scripts that generate the .js parser files +npm run generate-search-parser +npm run generate-autocomplete-parser + +#Checking if the saved files differ from the newly generated +if ! diff -q "$autocomplete_parser_backup" src/libs/SearchParser/autocompleteParser.js >/dev/null || + ! diff -q "$search_parser_backup" src/libs/SearchParser/searchParser.js >/dev/null; then + echo "The files generated from the .peggy files using the commands: generate-search-parser and generate-autocomplete-parser are not identical to those currently on this branch." + echo "The parser .js files should never be edited manually. Make sure you’ve run locally: npm run generate-search-parser and npm run generate-autocomplete-parser, and committed the changes." + exit 1 +else + echo "The files generated from the .peggy files using the commands: generate-search-parser and generate-autocomplete-parser are identical to those currently on this branch." + exit 0 +fi diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml index 1772d5d309cc..5cb0a99730c9 100644 --- a/.github/workflows/cherryPick.yml +++ b/.github/workflows/cherryPick.yml @@ -39,6 +39,7 @@ jobs: with: ref: staging token: ${{ secrets.OS_BOTIFY_TOKEN }} + submodules: true - name: Set up git for OSBotify id: setupGitForOSBotify @@ -85,14 +86,13 @@ jobs: if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉" echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT" + git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})" else echo "😞 PR can't be automerged, there are merge conflicts in the following files:" git --no-pager diff --name-only --diff-filter=U - git add . - GIT_MERGE_AUTOEDIT=no git cherry-pick --continue + git cherry-pick --abort echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT" fi - git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})" - name: Push changes run: | @@ -109,7 +109,25 @@ jobs: run: | gh pr create \ --title "🍒 Cherry pick PR #${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒" \ - --body "🍒 Cherry pick https://github.com/Expensify/App/pull/${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒" \ + --body \ + "🍒 Cherry pick https://github.com/Expensify/App/pull/${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒 + This PR had conflicts when we tried to cherry-pick it to staging. You'll need to manually perform the cherry-pick, using the following steps: + + \`\`\`bash + git fetch + git checkout ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }}-${{ github.run_attempt }} + git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }} + \`\`\` + + Then manually resolve conflicts, and commit the change with \`git cherry-pick --continue\`. Lastly, please run: + + \`\`\`bash + git commit --amend -m \"$(git log -1 --pretty=%B)\" -m \"(CP triggered by ${{ github.actor }})\" + \`\`\` + + That will help us keep track of who triggered this CP. Once all that's done, push your changes with \`git push origin ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }}-${{ github.run_attempt }}\`, and then open this PR for review. + + Note that you **must** test this PR, and both the author and reviewer checklist should be completed, just as if you were merging the PR to main." \ --label "Engineering,Hourly" \ --base "staging" sleep 5 @@ -117,11 +135,12 @@ jobs: "This pull request has merge conflicts and can not be automatically merged. :disappointed: Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge. **Important:** There may be conflicts that GitHub is not able to detect, so please _carefully_ review this pull request before approving." - gh pr edit --add-assignee "${{ github.actor }},${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }}" + ORIGINAL_PR_AUTHOR="$(gh pr view ${{ github.event.inputs.PULL_REQUEST_NUMBER }} --json author --jq .author.login)" + gh pr edit --add-assignee "${{ github.actor }},${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }},$ORIGINAL_PR_AUTHOR" env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} - - name: Label PR with CP Staging + - name: Label original PR with CP Staging run: gh pr edit ${{ inputs.PULL_REQUEST_NUMBER }} --add-label 'CP Staging' env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index 29dddbcd3151..85c928707c6c 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -65,6 +65,7 @@ jobs: uses: actions/checkout@v4 with: ref: main + submodules: true # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify # This is a workaround to allow pushes to a protected branch token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} @@ -75,70 +76,24 @@ jobs: with: GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - name: Generate version + - name: Generate new E/App version id: bumpVersion uses: ./.github/actions/javascript/bumpVersion with: GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} SEMVER_LEVEL: ${{ inputs.SEMVER_LEVEL }} - - name: Commit new version - run: | - git add \ - ./package.json \ - ./package-lock.json \ - ./android/app/build.gradle \ - ./ios/NewExpensify/Info.plist \ - ./ios/NewExpensifyTests/Info.plist \ - ./ios/NotificationServiceExtension/Info.plist - git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" - - - name: Update main branch - run: git push origin main - - - name: Announce failed workflow in Slack - if: ${{ failure() }} - uses: ./.github/actions/composite/announceFailedWorkflowInSlack - with: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - createNewHybridVersion: - runs-on: macos-latest - needs: [validateActor, createNewVersion] - if: ${{ fromJSON(needs.validateActor.outputs.HAS_WRITE_ACCESS) }} - steps: - - name: Run turnstyle - uses: softprops/turnstyle@49108bdfa571e62371bd2c3094893c547ab3fc03 - with: - poll-interval-seconds: 10 - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Check out `App` repo - uses: actions/checkout@v4 - with: - ref: main - submodules: true - # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify - # This is a workaround to allow pushes to a protected branch - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Update submodule and checkout the main branch + - name: Update Mobile-Expensify submodule with the latest state of the Mobile-Expensify main branch run: | - git submodule update --init cd Mobile-Expensify + git fetch --depth=1 origin main git checkout main - git pull origin main - - - name: Setup git for OSBotify - uses: ./.github/actions/composite/setupGitForOSBotify - id: setupGitForOSBotify - with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + git reset --hard origin/main - name: Generate HybridApp version run: | cd Mobile-Expensify + # Generate all flavors of the version SHORT_APP_VERSION=$(echo "$NEW_VERSION" | awk -F'-' '{print $1}') BUILD_NUMBER=$(echo "$NEW_VERSION" | awk -F'-' '{print $2}') @@ -167,9 +122,9 @@ jobs: # Update JS HybridApp Version sed -i .bak -E "s/\"version\": \"([0-9\.]*)\"/\"version\": \"$FULL_APP_VERSION\"/" $JS_CONFIG_FILE env: - NEW_VERSION: ${{ needs.createNewVersion.outputs.NEW_VERSION }} + NEW_VERSION: ${{ steps.bumpVersion.outputs.NEW_VERSION }} - - name: Commit new version + - name: Commit new Mobile-Expensify version run: | cd Mobile-Expensify git add \ @@ -178,16 +133,25 @@ jobs: ./iOS/Expensify/Expensify-Info.plist\ ./iOS/SmartScanExtension/Info.plist \ ./iOS/NotificationServiceExtension/Info.plist - git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" + git push origin main - - name: Update main branch on Mobile-Expensify and App + - name: Commit new E/App version + run: | + git add \ + ./package.json \ + ./package-lock.json \ + ./android/app/build.gradle \ + ./ios/NewExpensify/Info.plist \ + ./ios/NewExpensifyTests/Info.plist \ + ./ios/NotificationServiceExtension/Info.plist + git commit -m "Update version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" + + - name: Update Mobile-Expensify submodule in E/App run: | - cd Mobile-Expensify - git push origin main - cd .. git add Mobile-Expensify - git commit -m "Update Mobile-Expensify to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" - git push origin main + git commit -m "Update Mobile-Expensify submodule version to ${{ steps.bumpVersion.outputs.NEW_VERSION }}" + git push origin main - name: Announce failed workflow in Slack if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fab1604f1ee4..6ca2f0f8a698 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -252,10 +252,6 @@ jobs: name: android-hybrid-apk-artifact path: Expensify.apk - - name: Upload Android build to Firebase distribution - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane android upload_firebase_distribution - - name: Upload Android build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 @@ -322,7 +318,7 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} + GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }} - name: Upload desktop sourcemaps artifact uses: actions/upload-artifact@v4 @@ -577,10 +573,6 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Upload iOS build to Firebase distribution - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios upload_firebase_distribution - - name: Upload iOS build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index d00781fa2a32..869db3d04be7 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -270,7 +270,7 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }} + GCP_GEOLOCATION_API_KEY: ${{ secrets.GCP_GEOLOCATION_API_KEY_STAGING }} web: name: Build and deploy Web diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index d958e0958083..6a8a0d5884bf 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -81,7 +81,7 @@ jobs: }); const body = pullRequest.data.body; - const regex = /MOBILE-EXPENSIFY:(?\d+)/; + const regex = /MOBILE-EXPENSIFY:\s*https:\/\/github.com\/Expensify\/Mobile-Expensify\/pull\/(?\d+)/; const found = body.match(regex)?.groups?.prNumber || ""; return found.trim(); diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index b3db2c37d4d7..c69487a1b4e4 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -1,7 +1,6 @@ name: Verify HybridApp build on: - workflow_call: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] @@ -24,9 +23,23 @@ concurrency: cancel-in-progress: true jobs: + comment_on_fork: + name: Comment on all PRs that are forks + # Only run on pull requests that *are* a fork + if: ${{ github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-latest + steps: + - name: Comment on forks + run: | + gh pr comment ${{github.event.pull_request.html_url }} --body \ + ":warning: This PR is possibly changing native code, it may cause problems with HybridApp. Please run an AdHoc build to verify that HybridApp will not break. :warning:" + env: + GITHUB_TOKEN: ${{ github.token }} verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl + # Only run on pull requests that are *not* on a fork + if: ${{ !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 @@ -34,13 +47,11 @@ jobs: submodules: true ref: ${{ github.event.pull_request.head.sha }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - # fetch-depth: 0 is required in order to fetch the correct submodule branch - fetch-depth: 0 - name: Update submodule to match main run: | - git submodule update --init --remote - git fetch + git submodule update --init --remote --depth 1 + cd Mobile-Expensify git checkout main - name: Configure MapBox SDK @@ -52,10 +63,14 @@ jobs: with: IS_HYBRID_BUILD: 'true' + - name: Setup Ruby + uses: ruby/setup-ruby@v1.204.0 + with: + bundler-cache: true + - name: Build Android Debug - working-directory: Mobile-Expensify/Android run: | - if ! ./gradlew assembleDebug + if ! npm run android-hybrid-build then echo "❌ Android HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." exit 1 @@ -64,6 +79,8 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge + # Only run on pull requests that are *not* on a fork + if: ${{ !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 @@ -71,13 +88,11 @@ jobs: submodules: true ref: ${{ github.event.pull_request.head.sha }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - # fetch-depth: 0 is required in order to fetch the correct submodule branch - fetch-depth: 0 - name: Update submodule to match main run: | - git submodule update --init --remote - git fetch + git submodule update --init --remote --depth 1 + cd Mobile-Expensify git checkout main - name: Configure MapBox SDK @@ -94,9 +109,6 @@ jobs: with: bundler-cache: true - - name: Install New Expensify Gems - run: bundle install - - name: Cache Pod dependencies uses: actions/cache@v4 id: pods-cache @@ -125,16 +137,7 @@ jobs: export RCT_NO_LAUNCH_PACKAGER=1 # Build iOS using xcodebuild - if ! xcodebuild \ - -workspace Mobile-Expensify/iOS/Expensify.xcworkspace \ - -scheme Expensify \ - -configuration Debug \ - -sdk iphonesimulator \ - -arch x86_64 \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO \ - build | xcpretty + if ! npm run ios-hybrid-build then echo "❌ iOS HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." exit 1 diff --git a/.github/workflows/verifyParserFiles.yml b/.github/workflows/verifyParserFiles.yml new file mode 100644 index 000000000000..66fec63f40f8 --- /dev/null +++ b/.github/workflows/verifyParserFiles.yml @@ -0,0 +1,22 @@ +name: Check consistency of search parser files + +on: + pull_request: + types: [opened, synchronize] + branches-ignore: [staging, production] + paths: + - "src/libs/SearchParser/**" + +jobs: + verify: + if: github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Verify parser files consistency + run: ./.github/scripts/checkParser.sh diff --git a/Mobile-Expensify b/Mobile-Expensify index 968526b0c05c..7d33c85d7554 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 968526b0c05cb7b22fc1993647574fa3a9d9c6b6 +Subproject commit 7d33c85d75549f3bc95621538ce0ca47b06074a9 diff --git a/README.md b/README.md index 3b55f54bead2..de5c746a964d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ * [Running The Tests](#running-the-tests) * [Debugging](#debugging) * [App Structure and Conventions](#app-structure-and-conventions) +* [HybridApp](#HybridApp) * [Philosophy](#Philosophy) * [Security](#Security) * [Internationalization](#Internationalization) diff --git a/android/app/build.gradle b/android/app/build.gradle index 4b21c4109dc7..98a560cbbed6 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 1009009201 - versionName "9.0.92-1" + versionCode 1009009401 + versionName "9.0.94-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/assets/images/customEmoji/global-create.svg b/assets/images/customEmoji/global-create.svg new file mode 100644 index 000000000000..60b46eb97aed --- /dev/null +++ b/assets/images/customEmoji/global-create.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md b/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md deleted file mode 100644 index 54bd12ce5c49..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Assign Company Cards -description: How to assign company cards to employees in Expensify once they have been connected or imported ---- - -After connecting or importing your company cards to Expensify, you can assign each card to its respective cardholder. - -# Assign new cards - -If you're assigning cards via CSV upload for the first time, - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain. -3. Click the card dropdown menu and select the desired feed from the list. -![Click the dropdown located right below the Imported Cards title near the top of the page. Then select a card from the list.](https://help.expensify.com/assets/images/csv-03.png){:width="100%"} - -{:start="4"} -4. Click **Assign New Cards**. - -![Under the Company Cards tab on the left, you'll use the dropdown menu to select a card and beneath that, you'll click Assign New Cards]({{site.url}}/assets/images/CompanyCards_Assign.png){:width="100%"} - -{:start="5"} -5. Enter the employee's email address and/or select it from the dropdown list. *Note: Employees must have an email address under this domain in order to assign a card to them.* -![Below the Assign a Card header, enter or select the employee's email address]({{site.url}}/assets/images/CompanyCards_EmailAssign.png){:width="100%"} - -{:start="6"} -6. Enter the last four digits of the card number and/or select it from the dropdown list. - - If no transactions have been posted on the card, the card number will not appear in the list and you'll need to enter the full card number into the field. Then press ENTER on your keyboard. The field may clear itself after you press ENTER, but you can disregard this and continue to the next step. -7. (Optional) Set the transaction start date. Any transactions that were posted before this date will not be imported into Expensify. If you do not make a selection, it will default to the earliest available transactions from the card. *Note: Expensify can only import data for the time period released by the bank. Most banks only provide a certain amount of historical data, averaging 30-90 days into the past. It's not possible to override the start date the bank has provided via this tool.* -8. Click **Assign**. - -Once assigned, you will see each cardholder associated with their card and the start date listed. The transactions will now be imported to the cardholder's account, where they can add receipts, code the expenses, and submit them for review and approval. - -![Expensify domain assigned cards](https://help.expensify.com/assets/images/ExpensifyHelp_AssignedCard.png){:width="100%"} - -# Upload new expenses for existing assigned cards - -To add new expenses to an existing uploaded and assigned card, - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain name. -3. Click **Manage/Import CSV**. -![Click Manage/Import CSV located in the top right between the Issue Virtual Card button and the Import Card button.](https://help.expensify.com/assets/images/csv-02.png){:width="100%"} - -{:start="4"} -4. Select the saved layout from the drop-down list. -5. Click **Upload CSV**. -6. Click **Update All Cards** to retrieve the new expenses for the assigned cards. - -# Unassign company cards - -{% include info.html %} -Unassigning a company card will delete any unsubmitted (Open or Unreported) expenses in the cardholder's account. -{% include end-info.html %} - -To unassign a specific card, click the Actions button to the right of the card and click **Unassign**. - -![Click the Actions button to the right of the card and select Unassign.]({{site.url}}/assets/images/CompanyCards_Unassign.png){:width="100%"} - -To completely remove the card connection, unassign every card from the list and then refresh the page. - -*Note: If expenses are Processing and then rejected, they will also be deleted when they're returned to an Open state, as the card they're linked to no longer exists.* - -{% include faq-begin.md %} - -**My Commercial Card Feed is set up. Why is a specific card not coming up when I try to assign it to an employee?** - -Cards will appear in the dropdown when they are activated and have at least one posted transaction. If the card is activated and has been used for a while and you're still not seeing it, reach out to your Account Manager or message concierge@expensify.com for further assistance. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md b/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md deleted file mode 100644 index 75580b94f1ad..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Configure Company Card Settings -description: How to customize your company card settings ---- - -Once you’ve imported your company cards via [commercial card feed](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds), [direct bank feed](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections), or [CSV import](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import), the next step is to configure the card settings. - -{% include info.html %} -You must be a Domain Admin to complete this process. -{% include end-info.html %} - -# Configure company card settings - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain. -3. Click the **Settings** tab located at the top of the Company Cards tab. -![Near the top right, click the Settings tab that is located between the Card List and Reconciliation tabs.](https://help.expensify.com/assets/images/compcard-01.png){:width="100%"} -5. Set the following preferences, then click **Save**. - -## Preferred Workspace - -Setting a preferred Workspace for a company card feed ensures that the imported transactions are added to a report for that Workspace. This is useful when members are on multiple Workspaces and need to ensure their company card expenses are reported to a particular Workspace. - -## Reimbursable preference - -You can control how your employees' company card expenses are flagged for reimbursement: - -- **Force Yes**: All expenses will be marked as reimbursable. Employees cannot change this setting. -- **Force No**: All expenses will be marked as non-reimbursable. Employees cannot change this setting. -- **Do Not Force**: Expenses will default to either reimbursable or non-reimbursable (your choice), but employees can adjust if necessary. - -## Liability type - -Choose the liability type that suits your needs: - -- **Corporate Liability**: Users cannot delete company card expenses. -- **Personal Liability**: Users are allowed to delete company card expenses. - -If you update the settings on an existing company card feed, the changes will apply to expenses imported after the date that the setting is saved. The update will not affect previously imported expenses. - -# Use Scheduled Submit with company cards - -With Scheduled Submit, employees no longer have to create their expenses, add them to a report, and submit them manually. All they need to do is SmartScan their receipts and Concierge will take care of the rest using a variety of schedules that you can set according to your preferences. - -{% include info.html %} -Concierge won't automatically submit expenses on reports that have expense violations. These expenses will be moved to a new report for the current reporting period. -{% include end-info.html %} - -To enable Scheduled Submit, - -1. Hover over **Settings** and click **Workspaces**. -2. Select the desired Workspace. -3. Click the **Reports** tab on the left. -4. Enable the Scheduled Submit toggle. -5. Select the report submission frequency. -6. Select the date that reports will be submitted. - -# Connect company cards to an accounting integration - -If you're using a connected accounting system such as NetSuite, Xero, Intacct, Quickbooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. First, connect the card itself, and once completed, follow the steps below: - -1. Hover over **Settings** and click **Domains** -2. Select the desired domain. -3. Click **Edit Exports** near the top right and select the general ledger (GL) account you want to export expenses to. -![Find the desired card in the table. In that same row, click Edit Exports.](https://help.expensify.com/assets/images/cardfeeds-02.png){:width="100%"} - -Once the account is set, exported expenses will be mapped to the selected account when exported by a Domain Admin. - -# Export company card expenses to a connected accounting integration - -## Pooled GL account - -To export credit card expenses to a pooled GL account, - -1. Hover over **Settings** and click **Workspaces**. -2. Select the desired Workspace. -3. Click the **Connections** tab on the left. -4. Under Accounting Integrations, click **Configure** next to the desired accounting integration. -5. For Non-reimbursable export, select **Credit Card / Charge Card / Bank Transaction**. -6. Review the Export Settings page for exporting Expense Reports to NetSuite. -7. Select the Vendor/liability account you want to export all non-reimbursable expenses to. - -## Individual GL account - -1. Hover over **Settings** and click **Domains**. -2. Select the desired Domain. -3. Click the **Edit Exports** to the right of the desired card. Then select the general ledger (GL) account you want to export expenses to. -![Find the desired card in the table. In that same row, click Edit Exports.](https://help.expensify.com/assets/images/cardfeeds-02.png){:width="100%"} - -Once the account is set, exported expenses will be mapped to the selected account. - -# Identify company card transactions - -When you link your credit cards to Expensify, the transactions will appear in each user's account on the Expenses page as soon as they're posted. Transactions from centrally managed cards have a locked card icon next to them to indicate that they’re company card expenses. - -# Import historical transactions - -Once a card is connected via direct connection or via Approved! banks, Expensify will import 30-90 days of historical transactions to your account (based on your bank's discretion). Any historical expenses beyond that date range can be imported using the [CSV import](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import). - -# Use eReceipts - -Expensify eReceipts are digital substitutes for paper receipts, eliminating the need to keep physical receipts or use SmartScan for receipts. For Expensify Card transactions, eReceipts are automatically generated for all amounts in these categories: Airlines, Commuter expenses, Gas, Groceries, Mail, Meals, Car rental, Taxis, and Utilities. For other card programs, eReceipts are generated for USD purchases of $75 or less. - -{% include info.html %} -To ensure seamless automatic importation, it is key that you maintain your transactions in US Dollars. eReceipts can also be directly imported from your bank account. CSV/OFX imported files of bank transactions do not support eReceipts. eReceipts are not generated for lodging expenses. Due to incomplete or inaccurate category information from certain banks, there may be instances of invalid eReceipts being generated for hotel purchases. If you choose to re-categorize expenses, a similar situation may arise. It's crucial to remember that our Expensify eReceipt Guarantee excludes coverage for hotel and motel expenses. -{% include end-info.html %} - -{% include faq-begin.md %} - -**What plan/subscription is required in order to manage corporate cards?** - -A Group Workspace is required. - -**When do my company card transactions import to Expensify?** - -Credit card transactions are imported to Expensify once they’re posted to the bank account. This usually takes 1-3 business days between the point of purchase and when the transactions populate in your account. - -**Scheduled Submit is disabled. Why are reports still being submitted automatically?** - -If Scheduled Submit is disabled at the Group Workspace level or set to a manual frequency but expense reports are still being automatically submitted, Scheduled Submit is most likely enabled on the user’s Individual Workspace settings. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards.md b/docs/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards.md index acfc0e615240..059cd8cf0cde 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards.md +++ b/docs/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards.md @@ -75,12 +75,19 @@ A CSV upload is a manual method for importing credit card transactions into Expe --- # CSV Upload: How do I import credit card transactions using a CSV? -1. Download your card transactions in a CSV, OFX, QFX, or XLS format from your bank. -2. Format the file to include the card number, date, merchant, amount, and currency. +1. Download your card transactions from your bank in CSV, OFX, QFX, or XLS format. +2. Format the CSV for upload using [this template](https://s3-us-west-1.amazonaws.com/concierge-responses-expensify-com/uploads%2F1594908368712-Best+Example+CSV+for+Domains.csv) as a guide -- be sure to include the card number, date, merchant, amount, and currency. + - You can also add mapping for Categories and Tags, but those parameters are optional. + +![Your CSV template should include, at a minimum, a column for the card number, posted date, merchant, posted amount, and posted currency.](https://help.expensify.com/assets/images/csv-01.png){:width="100%"} + 3. Go to **Settings > Domains > [Domain Name] > Company Cards > Manage/Import CSV**. 4. Upload the file and map the fields to Expensify’s requirements. 5. Review the Output Preview for errors and submit the file. + +![Click Manage/Import CSV located in the top right between the Issue Virtual Card button and the Import Card button.](https://help.expensify.com/assets/images/csv-02.png){:width="100%"} + --- # Assign Cards: How do I assign cards to employees? 1. Go to **Settings > Domains > [Domain Name] > Company Cards**. @@ -89,6 +96,10 @@ A CSV upload is a manual method for importing credit card transactions into Expe 4. (Optional) Set a transaction start date. 5. Click **Assign** to complete the process. +![Click the dropdown located right below the Imported Cards title near the top of the page. Then select a card from the list.](https://help.expensify.com/assets/images/csv-03.png){:width="100%"} + +![Under the Company Cards tab on the left, you'll use the dropdown menu to select a card and beneath that, you'll click Assign New Cards]({{site.url}}/assets/images/CompanyCards_Assign.png){:width="100%"} + --- # Unassign Cards: How do I unassign cards? 1. Go to **Settings > Domains > [Domain Name] > Company Cards**. @@ -97,18 +108,43 @@ A CSV upload is a manual method for importing credit card transactions into Expe _**Note: Unassigning a card deletes all open or unreported expenses linked to it.**_ +![Click the Actions button to the right of the card and select Unassign.]({{site.url}}/assets/images/CompanyCards_Unassign.png){:width="100%"} + --- # Configure Company Card Settings 1. Go to **Settings > Domains > [Domain Name] > Company Cards > Settings**. 2. Adjust preferences for: - **Preferred Workspace**: Ensures transactions are reported to a specific workspace. - **Reimbursable Preference**: Controls whether expenses are flagged as reimbursable or non-reimbursable. - - **Liability Type**: Sets corporate or personal liability for expenses. Corporate liability prevents users from deleting company card expenses, while personal liability allows users to manage and delete expenses directly. + - **Liability Type**: Set the corporate or personal liability settings for company card expenses. Corporate liability prevents users from deleting company card expenses, while personal liability allows users to manage and delete their company card expenses. 3. Save the settings to apply changes. -**Tip**: For businesses using accounting integrations like QuickBooks or NetSuite, connect the cards to export expenses to specific general ledger (GL) accounts via the "Edit Exports" option. +![Near the top right, click the Settings tab that is located between the Card List and Reconciliation tabs.](https://help.expensify.com/assets/images/compcard-01.png){:width="100%"} + +## Export Company Card Expenses to a Connected Accounting Integration: Centralized General Ledger Account + +For businesses using accounting integrations like QuickBooks or NetSuite, connect the cards to export expenses to specific general ledger (GL) accounts via the "Edit Exports" option. + +![Find the desired card in the table. In that same row, click Edit Exports.](https://help.expensify.com/assets/images/cardfeeds-02.png){:width="100%"} + +**To export credit card expenses to a pooled GL account:** +1. Navigate to **Settings > Workspaces**. +2. Select the appropriate **Workspace**. +3. Open the **Connections** tab. +4. Under **Accounting Integrations**, click **Configure** next to the relevant integration. +5. In the **Non-reimbursable Export** section, select **Credit Card / Charge Card / Bank Transaction**. +6. Review the **Export Settings** page to ensure proper expense report export to NetSuite. +7. Choose the **Vendor/Liability Account** for exporting all non-reimbursable expenses. + +## Exporting to Individual General Ledger Accounts + +1. Navigate to **Settings > Domains**. +2. Select the appropriate **Domain**. +3. Click **Edit Exports** next to the relevant card. +4. Select the **General Ledger (GL) Account** for expense exports. --- + # FAQ ## Missing Transactions: Why aren’t all transactions appearing? diff --git a/docs/articles/expensify-classic/connections/xero/Configure-Xero.md b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md index 170e8d0b6974..907223c72ca9 100644 --- a/docs/articles/expensify-classic/connections/xero/Configure-Xero.md +++ b/docs/articles/expensify-classic/connections/xero/Configure-Xero.md @@ -1,104 +1,107 @@ --- title: Configure Xero -description: Configure Xero +description: Learn how to configure Xero in Expensify, including best practices, export settings, coding configurations, and advanced options. --- -**Best Practices Using Xero** +## Best Practices Using Xero -A connection to Xero lets you combine the power of Expensify's expense management features with Xero's accounting capabilities. By following the recommended best practices below, your finances will be automatically categorized correctly and accounted for in the right place. +A connection to Xero lets you combine Expensify's expense management features with Xero's accounting capabilities. Follow these best practices to ensure your finances are correctly categorized and accounted for: -- Configure your setup immediately after making the connection, and review each settings tab thoroughly. -- Keep Auto Sync enabled. - - The daily auto sync will update Expensify with any changes to your chart of accounts, customers/projects, or bank accounts in Xero. - - Finalized reports will be exported to Xero automatically, saving your admin team time with every report. -- Set your preferred exporter to a user who is both a workspace and domain admin. -- Configure your coding settings and enforce them by requiring categories and tags on expenses. +- Configure your setup immediately after connecting and review each settings tab thoroughly. +- Keep **Auto Sync** enabled to: + - Update Expensify daily with changes to your chart of accounts, customers/projects, or bank accounts in Xero. + - Automatically export finalized reports to Xero, saving your admin team time. +- Set the **Preferred Exporter** to a user who is both a Workspace and Domain Admin. +- Configure **coding settings** and enforce them by requiring categories and tags on expenses. -# Accessing the Xero Configuration Settings -Xero is connected at the workspace level, and each workspace can have a unique configuration that dictates how the connection functions. To access the configuration: +## Accessing the Xero Configuration Settings + +Xero is connected at the workspace level, and each workspace has a unique configuration. To access the settings: 1. Click **Settings** near the bottom of the left-hand menu. -2. Navigate to Workspaces > Groups > [workspace Name] > Connections. -3. Scroll down to the Xero connection and click the **Configure** button to open the settings menu. - -# Step 1: Configure Export Settings -The following steps help you determine how data will be exported from Expensify to Xero. - -1. Click the **Configure** button under the Xero connection to open the settings menu. -2. Under the Export tab, review each of the following export settings: - - **Preferred Exporter**: Choose a Workspace Admin to set as the Preferred Exporter. - - Concierge exports reports automatically on behalf of the preferred exporter. - - Other Workspace Admins will still be able to export to Xero manually. - - If you set different export bank accounts for individual company cards under your Domain > Company Cards, then your Preferred Exporter must be a Domain Admin in addition to Workspace Admin. - - **Export reimbursable expenses and bills as**: Reimbursable expenses export as a Purchase Bill. This setting cannot be amended. - - **Purchase Bill Date**: Choose whether to use the date of the last expense on the report, export date, or submitted date. - - **Export invoices as**: All invoices exported to Xero will be as sales invoices. Sales invoices always display the date on which the invoice was sent. This setting cannot be amended. - - **Export non-reimbursable expenses as**: Each exported expense posts as a bank transaction to the Xero bank account you select below, and transaction dates will match the dates on your bank statement. - - **Xero Bank Account**: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported. - -## Step 1B: Optional configuration when company cards are connected -1. Click **Settings** near the bottom of the left-hand menu. -2. Navigate to Domains > [domain name] > Company Cards. -3. If you have more than one company card connection, select the connection first. -4. Locate the cardholder you want to configure in the list, -5. Click the **Edit Exports** button and assign the account the card expenses should export to in Xero. - -# Step 2: Configure Coding settings -The following steps help you determine how data will be imported from Xero to Expensify. - -1. Click the **Configure** button under the Xero connection to open the settings menu. -2. Under the Coding tab, review each of the following settings and configure the options to determine what information will be imported: - - **Chart of Accounts**: Your Xero Chart of Accounts is imported into Expensify as expense categories. _This is enabled by default and cannot be disabled._ - - **Tax Rates**: When Enabled, your tax rates in Xero will be imported into your workspace. After being imported, you can find them on the [Tax](https://expensify.com/policy?param=%7B%22policyID%22:%22B936DE4542E9E78B%22%7D#tax) page of your workspace settings. - - **Tracking Categories**: When Enabled, you can configure how Xero Cost Centres and Xero Regions import. - - Xero contact default (applies the Xero contact default during export to Xero) - - Tag (line-item level) - - Report Field (header level) - - **Billable Expenses**: When enabled, your Xero customer contacts will be imported as tags. Xero requires all billable expenses to have a customer tag to be able to be exported to Xero. - -# Step 3: Configure advanced settings -The following steps help you determine the advanced settings for your connection, like auto-sync. - -1. Click the **Configure** button under the Xero connection to open the settings menu. -2. Under the Advanced tab, review each of the following settings and configure the options you wish to use: - - **Auto Sync**: When enabled, the connection will sync daily to ensure that the data shared between the two systems is up-to-date. - - New report approvals/reimbursements will be synced during the next auto-sync period. -Reimbursable expenses will export after reimbursement occurs or the report is marked as reimbursed outside Expensify when using Direct or Indirect reimbursement. - - Non-reimbursable expenses will export automatically after the report is final approved. - - **Newly Imported Categories Should Be**: When a new account is created in the Xero chart of accounts, this setting controls whether the new category in Expensify is enabled or disabled by default. Disabled categories are not visible to employees when coding expenses. - - **Set purchase bill status** (optional): Reimbursable expenses are exported as purchase bills with the status selected. The options available are: - - Awaiting Payment (default) - - Draft - - Awaiting Approval - - **Sync Reimbursed Reports**: When enabled, you can configure the Bill Payment and Invoice Collections accounts to be used when reimbursing reports and paying invoices. - - Anytime a report is reimbursed, or an invoice is paid through Expensify, the corresponding purchase bill or sales invoice in Xero will be marked as paid. - - Similarly, if a purchase bill or sales invoice is marked as paid in Xero, the related Expensify report or invoice will be automatically marked as reimbursed/paid. - - **Xero Bill Payment Account**: Once the expense report is paid, your reimbursements will appear under this Xero Bill Payment account. - - **Xero Invoice Collections Account**: If you are exporting invoices from Expensify, select the invoice collection account under which you want invoices to appear once they are marked as paid. +2. Navigate to **Workspaces > Groups > [Workspace Name] > Connections**. +3. Scroll to the Xero connection and click **Configure**. -{% include faq-begin.md %} +--- + +## Step 1: Configure Export Settings -## I have multiple organizations in Xero. Can I connect them all to Expensify? +Define how data will be exported from Expensify to Xero: -Yes, you can connect each organization you have to Expensify. Here are some essential things to keep in mind: +1. Click **Configure** under the Xero connection. +2. Under the **Export** tab, review and set up: + - **Preferred Exporter**: Assign a Workspace Admin. + - Concierge exports reports automatically for the preferred exporter. + - Other Workspace Admins can export manually. + - If you set different export bank accounts for company cards, the Preferred Exporter must also be a Domain Admin. + - **Export reimbursable expenses and bills as**: Always exported as a **Purchase Bill**. + - **Purchase Bill Date**: Choose **last expense date, export date, or submitted date**. + - **Export invoices as**: Always exported as **Sales Invoices**. + - **Export non-reimbursable expenses as**: Posted as **bank transactions** to a Xero bank account. + - **Xero Bank Account**: Select the bank account for posting non-reimbursable expenses. -- Organization Selection in the Workspace > Connections > Xero Configuration > Export settings tab: This option is available only if multiple organizations are configured in Xero. -- One Workspace, One Organization: Each Workspace can connect to just one organization at a time. It’s a one-to-one connection. -- Adding New Organizations: If you create a new organization in Xero after your initial connection, you’ll need to disconnect and then reconnect it to Xero. Don’t forget to take a screenshot of your current settings by clicking Configure and checking the Export, Coding, and Advanced tabs. This way, you can easily set everything up again. +### Step 1B: Configure Company Card Exports (If Applicable) -## How can I view the purchase bills exported to Xero? +1. Click **Settings**. +2. Navigate to **Domains > [Domain Name] > Company Cards**. +3. If multiple company card connections exist, select the relevant one. +4. Locate the cardholder and click **Edit Exports**. +5. Assign the correct Xero account for the card expenses. + +--- + +## Step 2: Configure Coding Settings + +Define how data is imported from Xero to Expensify: + +1. Click **Configure** under the Xero connection. +2. Under the **Coding** tab, configure: + - **Chart of Accounts**: Imported as **expense categories** (enabled by default). + - **Tax Rates**: Enabled to import Xero tax rates, visible under [Tax Settings](https://expensify.com/policy?param=%7B%22policyID%22:%22B936DE4542E9E78B%22%7D#tax). + - **Tracking Categories**: Choose import method: + - **Xero contact default** (applies during export) + - **Tag** (line-item level) + - **Report Field** (header level) + - **Billable Expenses**: Enables importing Xero customer contacts as tags. All billable expenses require a customer tag for export. + +--- + +## Step 3: Configure Advanced Settings + +1. Click **Configure** under the Xero connection. +2. Under the **Advanced** tab, configure: + - **Auto Sync**: Ensures daily synchronization. + - Reimbursable expenses export after reimbursement. + - Non-reimbursable expenses export after final approval. + - **Newly Imported Categories Should Be**: Controls default visibility of new Xero accounts in Expensify. + - **Set Purchase Bill Status** (optional): + - **Awaiting Payment** (default) + - **Draft** + - **Awaiting Approval** + - **Sync Reimbursed Reports**: Ensures paid reports and invoices sync across Expensify and Xero. + - **Xero Bill Payment Account**: Specifies where reimbursements appear in Xero. + - **Xero Invoice Collections Account**: Defines the invoice collection account for exported invoices. + +--- + +{% include faq-begin.md %} + +## Can I connect multiple Xero organizations to Expensify? + +Yes, but each workspace can connect to only one Xero organization at a time. If you add a new organization in Xero, you'll need to disconnect and reconnect the integration. Take screenshots of your settings beforehand to reconfigure quickly. + +## How can I view purchase bills exported to Xero? -**To view the bills in Xero:** 1. Log into Xero. -2. Navigate to Business > Purchase Overview > Awaiting Payments. - - Bills will be payable to the individual who created and submitted the report in Expensify. +2. Navigate to **Business > Purchase Overview > Awaiting Payments**. + - Bills are payable to the user who created and submitted the report in Expensify. -## How can I view the bank transactions in Xero? +## How can I view bank transactions in Xero? -**To view the transactions in Xero:** 1. Log into Xero. -2. Head over to your Dashboard. -3. Select your company card. -4. Locate the specific expense you’re interested in. +2. Open your **Dashboard**. +3. Select your **Company Card**. +4. Locate the relevant expense. {% include faq-end.md %} + diff --git a/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md index 2fb71a780e41..fc79d1a595f6 100644 --- a/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md +++ b/docs/articles/expensify-classic/connections/xero/Connect-To-Xero.md @@ -1,28 +1,36 @@ --- title: Connect to Xero -description: Everything you need to know about Expensify's direct integration with Xero +description: Learn how to integrate Expensify with Xero for seamless expense management order: 1 --- -**Prerequisites** +## Prerequisites -You must be a Workspace Admin in Expensify using a Collect or Control Workspace to connect your Xero account to Expensify. +To connect Xero with Expensify, you must: +- Be a **Workspace Admin** in Expensify. +- Use a **Collect** or **Control** Workspace. ## Step 1: Connect Expensify to Xero -1. Click **Settings** near the bottom of the left-hand menu. -2. Navigate to Workspaces > Groups > [workspace Name] > Connections. -3. Click on **Connect to Xero**. -4. Click the **Create a New Xero Connection** button. + +Follow these steps to set up the Xero integration: + +1. Click **Settings** in the bottom left menu. +2. Navigate to **Workspaces** > **Groups** > *[Workspace Name]* > **Connections**. +3. Click **Connect to Xero**. +4. Click **Create a New Xero Connection**. 5. Enter your Xero login credentials. -6. Review the access information and click Allow Access. -7. You will be redirected back to Expensify and the connection will import some initial settings from Xero to Expensify. -8. Once the sync is complete, the configuration window for Xero will open automatically so you can configure your export, import, and advanced settings. -9. Click the **Save** button when you’re done configuring to finalize the connection. +6. Review the access permissions and click **Allow Access**. +7. You will be redirected back to Expensify, where the connection will begin syncing initial settings from Xero. +8. Once the sync is complete, the **Xero Configuration** window will open automatically. +9. Configure your **export, import, and advanced settings** as needed. +10. Click **Save** to finalize the connection. {% include faq-begin.md %} -## I use a Cashbook or Ledger Xero account, can I still connect in Expensify? +## FAQ + +### Can I connect a Cashbook or Ledger Xero account to Expensify? -Starting in September 2021, there’s a chance for Cashbook and Ledger-type organizations in Xero. Apps like Expensify won’t be able to create invoices and bills for these accounts using the Xero API. So, if you’re using a Cashbook or Ledger Xero account, please be aware that this might affect your Expensify integration. +Starting in September 2021, **Cashbook and Ledger** Xero accounts may experience limitations. Apps like Expensify cannot create invoices or bills for these account types using the Xero API. If you are using a **Cashbook or Ledger** Xero account, please note that this may affect your Expensify integration. {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md index f23ec515cde4..6d34d87d4f73 100644 --- a/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/xero/Xero-Troubleshooting.md @@ -1,155 +1,142 @@ --- title: Xero Troubleshooting -description: Xero Troubleshooting +description: Troubleshooting common Xero integration errors in Expensify, including sync and export issues. --- # Overview of Xero Troubleshooting -Synchronizing and exporting data between Expensify and Xero can streamline your financial processes, but occasionally, users may encounter errors that prevent a smooth integration. These errors often arise from discrepancies in settings, missing data, or configuration issues within Xero or Expensify. +Synchronizing and exporting data between Expensify and Xero can streamline your financial processes, but occasionally, errors may occur due to discrepancies in settings, missing data, or configuration issues. -This troubleshooting guide aims to help you identify and resolve common sync and export errors, ensuring a seamless connection between your financial management systems. By following the step-by-step solutions provided for each specific error, you can quickly address issues and maintain accurate and efficient expense reporting and data management. +This guide provides step-by-step solutions to common Xero-related errors to ensure a seamless connection and accurate expense reporting. -# ExpensiError XRO014 Billable Expenses Require A Customer - -**Why does this happen?** - -This happens because Xero requires all billable expenses exported from Expensify to have a customer associated with it. This error occurs when one or more expenses on a report have been marked "billable," and do not have a customer associated with them. - -## How to fix it - -1. Navigate to your Settings > Workspaces > [click workspace] > Connections > Configure button > Coding tab. -2. Click the **toggle** to enable Billable Expenses. -3. Click the **Save** button to save the change and sync the connection. -4. Open the report in question and apply a _Customer_ tag to each billable expense. - - _Note: A Contact in Xero is not imported as a Customer until they have had some kind of bill raised against them. If you can't see your Customer imported as a tag, you may need to raise a dummy invoice at the Xero end and then delete/void it. Don’t forget to sync the connection again after taking this step._ -5. Try to export the report again by clicking the **Export to** button and select the **Xero** option. - -# ExpensiError XRO027: Expense on this report is categorized with a category no longer in Xero - -**Why does this happen?** - -When exporting expense data, Xero will not accept a category on an expense that no longer exists in the chart of accounts. This error occurs when one or more expenses on the report are categorized with a category that no longer exists in Xero. - -## How to fix it - -1. Log into Xero. -2. Navigate to Settings > Chart of Accounts. -3. Confirm that each category used on an expense in the report in Expensify is still an active account in Xero. -4. If the account doesn’t exist, add it again and sync the connection in Expensify. -5. If the account still exists, open the details, check “Show in Expense Claims,” and then sync the connection in Expensify. -6. After syncing, open the report and re-categorize any expenses showing a red workspace violation for out-of-workspace categories. -7. After recategorizing, click the **Export to** button and select the **Xero** option. - -# ExpensiError XRO031: Payment has already been allocated to reimbursable expenses - -**Why does this happen?** - -Xero does not allow for paid expenses to be modified. When you attempt to export the reimbursable expenses again, Xero considers that a modification and rejects the export. This error occurs when the report contains reimbursable expenses that have already been exported to Xero where a payment was issued on the purchase bill. - -## How to fix it +--- -1. Log into Xero. -2. Click on Business > Bills to Pay > and then the Paid tab. -3. Locate the report from the error and click on it to open it. -4. Click on the blue text that says Payment. -5. Click the Options dropdown and then Remove and Redo to delete the payment. - - _Note: Do not void the bill in Xero._ -6. Head back to Expensify and open the report again. -7. Click the **Export to** button and select the **Xero** option. +## ExpensiError XRO014: Billable Expenses Require a Customer -The new export will override the current report in Xero and retain the same report ID. +### Why does this happen? +Xero requires all billable expenses exported from Expensify to have a customer assigned. This error occurs when one or more expenses are marked "billable" but lack an associated customer. -# ExpensiError XRO087 No Bank Account or Incorrect Bank Account +### How to fix it +1. Navigate to **Settings > Workspaces > [workspace] > Connections > Configure > Coding tab**. +2. Enable **Billable Expenses** by toggling the setting. +3. Click **Save** to sync the connection. +4. Open the report and apply a **Customer** tag to each billable expense. + - *Note: A Xero Contact becomes a Customer only after an invoice has been raised against them. If the Customer is missing, create a dummy invoice in Xero, then delete/void it and sync again.* +5. Retry the export by clicking **Export to > Xero**. -**Why does this happen?** +--- -Xero requires all bank transactions created from non-reimbursable expenses in Expensify to be posted to an active bank account. This error occurs when the destination account in Xero doesn’t exist, isn’t set, or is not the right type. +## ExpensiError XRO027: Category No Longer Exists in Xero -## How to fix it +### Why does this happen? +Xero does not accept expenses categorized under accounts that no longer exist in the Chart of Accounts. -1. Navigate to Settings > Workspaces > [workspace name] > Connections > click the **Configure** button. -2. Select a Xero Bank Account from the dropdown that will apply to all non-reimbursable expenses exported to Xero. -3. Click the **Save** button to sync the connection. -4. Open the report again and click the **Export to** button and then the **Xero** option. +### How to fix it +1. Log into Xero and navigate to **Settings > Chart of Accounts**. +2. Ensure all expense categories in Expensify are active in Xero. +3. If a category is missing, add it back in Xero and sync Expensify. +4. If the category exists, ensure **Show in Expense Claims** is enabled. +5. Sync Expensify, open the report, and recategorize expenses flagged with a red violation. +6. Click **Export to > Xero**. -# ExpensiError XRO052: Expenses Are Not Categorized With A Xero Account +--- -**Why does this happen?** +## ExpensiError XRO031: Payment Already Allocated to Reimbursable Expenses -Xero requires all expenses exported from Expensify to use a category matching an account in your chart of accounts. If a category from another source is used, Xero will reject the expense. This error occurs when an expense on the report has a category applied that is not valid. +### Why does this happen? +Xero does not allow modifications to paid expenses. If a reimbursable expense is re-exported, Xero rejects it as a modification. -## How to fix it +### How to fix it +1. In Xero, go to **Business > Bills to Pay > Paid tab**. +2. Locate and open the report with the error. +3. Click on the blue **Payment** link. +4. Click **Options > Remove and Redo** (*Do not void the bill*). +5. In Expensify, open the report and click **Export to > Xero**. + - The new export will override the previous report while retaining the same ID. -1. Sync your Xero connection in Expensify from Settings > Workspaces > [click workspace] > Connections, and click the **Sync Now** button. -2. Review the expenses on the report. If any appear with a red _Category no longer valid_ violation, recategorize the expense until all expenses are violation-free. -3. Click the **Export to** button and then the **Xero** option. -4. If you receive the same error, continue. - - _Note the categories used on the expenses and check the Settings > Workspaces > [click workspace] > Categories page to confirm the exact categories used on the report are enabled and connected to Xero (you'll see a blue icon next to all connected categories)._ -5. Confirm that the categories used for expenses in the report match exactly the accounts in your Xero chart of accounts. -6. If you make any changes in Xero or in Expensify, always sync the connection and then try to export again. +--- -# ExpensiError XRO068: Organization is not subscribed to currency x +## ExpensiError XRO087: No or Incorrect Bank Account -**Why does this happen?** +### Why does this happen? +Xero requires bank transactions from Expensify to post to an active bank account. This error occurs when the destination account is missing or incorrect. -Xero requires the currencies you’re using in Expensify to be added to your account before you can export expenses in that currency. For example, if your workspace is set to Canadian currency, all expenses submitted on that workspace will be converted to CAD. You must also have the Canadian currency added to your Xero account to export successfully. This error occurs when your Xero account does not have the currency mentioned in the error added. +### How to fix it +1. In Expensify, go to **Settings > Workspaces > [workspace] > Connections > Configure**. +2. Select a **Xero Bank Account** for non-reimbursable expenses. +3. Click **Save** to sync the connection. +4. Open the report and retry the export. -## How to fix it -_Note: Not all versions of Xero allow adding currencies. To add currencies, please upgrade your Xero account to the Established [plan](https://www.xero.com/us/pricing-plans/)._ +--- -1. Log into Xero. -2. Navigate to Settings > General Settings. -3. Under the heading Features, select Currencies. -4. Click **Add Currency** to add the currency listed in the error message. -5. Sync your Xero connection in Settings > Workspaces > [click workspace] > Connections. -6. Open the report and click the **Export to** button and then the **Xero** option. +## ExpensiError XRO052: Expenses Not Categorized with a Xero Account -# ExpensiError XRO076: This report has already been exported once to Xero, but has been voided +### Why does this happen? +Xero requires all expenses to be categorized under valid accounts in the Chart of Accounts. -**Why does this happen?** +### How to fix it +1. Sync Expensify with Xero (**Settings > Workspaces > [workspace] > Connections > Sync Now**). +2. Review expenses for red category violations and recategorize them. +3. Click **Export to > Xero**. +4. If errors persist: + - Verify category settings under **Settings > Workspaces > Categories**. + - Ensure categories match exactly with Xero’s Chart of Accounts. +5. Sync again and retry the export. -Xero does not allow Expensify to modify a purchase bill created from a previous export if the bill has been voided. This error occurs when the report has already been exported to Xero, and the purchase bill has been voided. +--- -## How to fix it -_Note: Xero does not support “unvoiding” a bill, it is an irreversible action._ +## ExpensiError XRO068: Currency Not Subscribed in Xero -1. From the Reports page in Expensify, locate the report associated with the voided bill. -2. Check the box to the left of the report and click **Copy**. -3. Open the new report and submit it through the approval workflow, then confirm it exports to Xero successfully. +### Why does this happen? +Xero requires all currencies used in Expensify to be added before exporting expenses in that currency. -# ExpensiError XRO099: You have reached the limit of invoices you can approve with your Xero account. +### How to fix it +1. In Xero, go to **Settings > General Settings > Features > Currencies**. +2. Click **Add Currency** and select the required currency. +3. Sync Expensify with Xero (**Settings > Workspaces > Connections**). +4. Open the report and click **Export to > Xero**. + - *Note: Adding currencies requires the Established Xero plan. [Upgrade if necessary](https://www.xero.com/us/pricing-plans/).* -**Why does this happen?** +--- -The Early plan only allows you to enter 5 bills per month. This error occurs when you are on a trial account of Xero and have run out of your allowable exports. +## ExpensiError XRO076: Report Previously Exported and Voided -## How to fix it -Please upgrade your Xero account to a Growing or Established [plan](https://www.xero.com/us/pricing-plans/) so you can continue to use the integration and export reports without error. +### Why does this happen? +Xero does not allow modifications to voided purchase bills. -# Why are company card expenses exported to the wrong account? +### How to fix it +1. In Expensify, locate the report on the **Reports** page. +2. Select the report and click **Copy**. +3. Submit the copied report for approval and export it to Xero. -Multiple factors could be causing your company card transactions to export to the wrong place in your accounting system, but the best place to start is always the same. +--- -- First, confirm that the company cards have been mapped to the correct accounts in Settings > Domains > Company Cards > click the **Edit Export** button for the card to view the account. -- Next, confirm the expenses in question have been imported from the company card? - - Only expenses that have the Card+Lock icon next to them will export according to the mapping settings that you configure in the domain settings. +## ExpensiError XRO099: Xero Invoice Approval Limit Reached -It’s important to note that expenses imported from a card linked at the individual account level, expenses created from a SmartScanned receipt, and manually created cash expenses will export to the default bank account selected in your connection's configuration settings. +### Why does this happen? +The Early plan in Xero allows only 5 bills per month. This error occurs when the limit is reached. -**Is the report exporter a domain admin?** +### How to fix it +Upgrade your Xero account to a **Growing or Established plan**. [See Xero pricing](https://www.xero.com/us/pricing-plans/). -The user exporting the report must be a domain admin. You can check the history and comment section at the bottom of the report to see who exported the report. +--- -If your reports are being exported automatically by Concierge, the user listed as the Preferred Exporter under Settings > Workspaces > [workspaces name] > Connections > click **Configure** must be a domain admin as well. +## Why Are Company Card Expenses Exported to the Wrong Account? -If the report exporter is not a domain admin, all company card expenses will export to the bank account set in Settings > Workspaces > [workspace name] > Connections > click **Configure** for non-reimbursable expenses. +1. Confirm that **company cards** are mapped correctly: + - **Settings > Domains > Company Cards > Edit Export**. +2. Verify that expenses have the **Card+Lock icon** (indicating they were imported from a company card). +3. Ensure the exporter is a **Domain Admin**: + - Check the **Preferred Exporter** setting under **Settings > Workspaces > Connections > Configure**. +4. Verify company card mapping under the correct workspace. -**Has the company card been mapped under the correct workspace?** +--- -If you have multiple workspaces connected to Xero, each connected workspace will have a separate list of accounts to assign the card to. Unless you choose an account listed under the same workspace as the report you are exporting, expenses will export to the default bank account. +## Why Do Non-Reimbursable Expenses Show 'Credit Card Misc' Instead of the Merchant? -# Why do non-reimbursable expenses say 'Credit Card Misc,' instead of the merchant? +If a merchant in Expensify **matches** a contact in Xero, expenses will reflect the vendor name. Otherwise, they default to \"Expensify Credit Card Misc\" to prevent duplicates. -Where the merchant in Expensify is an exact match to a contact you have set up in Xero then exported credit card expenses will show the vendor name. If not we use the the default name Expensify Credit Card Misc. This is done to prevent multiple variations of the same contact (e.g. Starbucks and Starbucks #1234 as is often seen in credit card statements) being created in Xero. +### How to fix it +Use **Expense Rules** in Expensify to standardize merchant names. Learn more [here](https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules). -To change merchant names to match your vendor list in Xero, we recommend using our Expense Rules feature. More information on this can be found [here](https://community.expensify.com/discussion/5654/deep-dive-using-expense-rules-to-vendor-match-when-exporting-to-an-accounting-package/p1?new=1). +--- diff --git a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md index 671d0c41e772..f084e3b01cd9 100644 --- a/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md +++ b/docs/articles/expensify-classic/expensify-billing/Consolidated-Domain-Billing.md @@ -1,28 +1,53 @@ --- title: Consolidated Domain Billing -description: Consolidated Domain Billing allows organizations to have different billing owners with only one person being billed for all paid workspaces. +description: Learn how to enable and manage Consolidated Domain Billing, allowing one billing owner to cover all paid workspaces under a verified domain. --- - + # Overview -If your organization requires that different workspaces have different billing owners, but only one person should pay the Expensify bill each month, you can enable Consolidated Domain Billing. -# How to enable Consolidated Domain Billing -Consolidated Domain Billing is a domain-level feature, so to access this setting, you’ll first need to claim and verify your domain. You can do this by heading to **Settings > Domains > Domain Name** > clicking on a setting such as **Groups** > and then clicking **Verify**. +Consolidated Domain Billing allows organizations to have different billing owners for workspaces while ensuring that only one person is billed for all paid workspaces within a verified domain. + +--- + +# Enabling Consolidated Domain Billing +To enable this feature, you must first claim and verify your domain: + +1. Go to **Settings > Domains**. +2. Select your domain. +3. Click on a setting such as **Groups**. +4. Click **Verify**. + +Once your domain is verified, enable Consolidated Domain Billing: + +1. Navigate to **Settings > Domains > [Your Domain]**. +2. Select **Domain Admins > Primary Contact and Billing**. +3. Toggle **Consolidated Domain Billing** on. -Once the domain is verified, you can enable Consolidated Domain Billing under **Settings > Domains > Domain Name > Domain Admins > Primary Contact and Billing**. -# How to use Consolidated Domain Billing -When a Domain Admin enables Consolidated Domain Billing, all Group workspaces owned by any user with an email address matching the domain will get billed to the Consolidated Domain Billing owner’s account. -# Deep Dive -## Consolidated Domain Billing best practices -If you don’t have multiple billing owners across your organization, or if you want to keep billing separate for any reason, then this feature isn’t necessary. +--- + +# How It Works +When a **Domain Admin** enables Consolidated Domain Billing: -If you have an Annual Subscription and enable Consolidated Domain Billing, the Consolidated Domain Billing feature will gather the amounts due for each Group workspace Billing Owner (listed under **Settings > Workspaces > Group**). To make full use of the Annual Subscription for all workspaces in your domain, you should also be the billing owner for all Group workspaces. +- All **Group Workspaces** owned by users with an email address matching the domain will be billed to the **Primary Contact** listed under **Domain Admins**. +- Individual workspace billing owners will no longer receive separate charges. + +--- + +# Best Practices +- **When to Use It**: If multiple billing owners exist in your organization but you want a single, consolidated bill. +- **When to Avoid It**: If you need to keep workspace billing separate for accounting or financial tracking purposes. +- **Annual Subscription Considerations**: + - If you have an **Annual Subscription**, Consolidated Domain Billing will combine the amounts due for each Group workspace billing owner. + - To maximize savings, the **Primary Contact** should also be the billing owner for all Group Workspaces. + +--- -{% include faq-begin.md %} +# FAQ -## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? -You’ll have to toggle off Consolidated Domain Billing, take over ownership of the workspace, and then toggle it back on. +## How do I take over the billing of a workspace with Consolidated Domain Billing enabled? +1. Toggle off **Consolidated Domain Billing**. +2. Take ownership of the workspace. +3. Toggle **Consolidated Domain Billing** back on. -## Can I use Consolidated Domain Billing to cover the bill for some workspaces, but not others? -No, this feature means that you’ll be paying the bill for all domain members who choose a subscription. +## Can I cover the bill for some workspaces but not others? +No, enabling this feature means you will be billed for **all** domain members who choose a subscription. -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md b/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md index abbd51f3efb7..129da9603110 100644 --- a/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md +++ b/docs/articles/expensify-classic/settings/Enable-two-factor-authentication.md @@ -1,62 +1,77 @@ --- -title: Enable Two-factor authentication -description: Use 2FA for extra login security +title: Enable Two-Factor Authentication +description: Use 2FA for extra login security. --- -
-Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication (2FA). This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. +Add an extra layer of security to protect your financial data by enabling two-factor authentication (2FA). This requires you to enter a code generated by your preferred authenticator app (such as Google Authenticator or Microsoft Authenticator) when you log in. -Expensify's Two-Factor Authentication (2FA) is implemented via a Time-based One-Time Password (TOTP) algorithm. This requires you to use an Authenticator app to generate a unique code each time you log in, adding a second “factor” to your login. +Expensify's 2FA is implemented via a Time-based One-Time Password (TOTP) algorithm. This means that each time you log in, you must use an authenticator app to generate a unique 6-digit code, adding a second “factor” to your login. + +## Recommended Authenticator Apps + +You can use any authenticator app, but here are a few we recommend: -You can choose to use whichever authenticator you prefer, but here are a few we recommend: - [1Password](https://support.1password.com/one-time-passwords/) - [Authy](https://authy.com/) - [Google Authenticator](https://support.google.com/accounts/answer/1066447) - [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app) -You will need to select an authenticator app to use before proceeding. +Ensure you have an authenticator app installed before proceeding. -## Enable and Set Up Two-factor authentication +# Enable and Set Up Two-Factor Authentication -1. Hover over Settings, then click **Account**. -2. Under the Account Details tab, scroll down to the Two Factor Authentication section and enable the toggle. -3. Save a copy of your backup codes. - - Click **Download** to save a copy of your backup codes to your computer. - - Click **Copy** to paste the codes into a document or other secure location. +1. Hover over **Settings**, then click **Account**. +2. Under the **Account Details** tab, scroll to the **Two-Factor Authentication** section and enable the toggle. +3. Save a copy of your backup codes: + - Click **Download** to save a copy to your computer. + - Click **Copy** to store the codes in a secure location. {% include info.html %} -This step is critical—You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes. +This step is critical—If you lose access to your authenticator app and do not have your recovery codes, you will lose access to your account. {% include end-info.html %} -4. Click **Continue**. -5. Download or open your authenticator app and either: - - Scan the QR code shown on your computer screen. +4. Click **Continue**. +5. Open your authenticator app and either: + - Scan the QR code displayed on your screen. - Enter the 6-digit code from your authenticator app into Expensify and click **Verify**. -When you log in to Expensify in the future, you’ll be emailed a magic code that you’ll use to log in with. Then you’ll be prompted to open your authenticator app to get the 6-digit code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. +Once set up, when logging into Expensify, you will: +- Receive a Magic Code email to initiate login. +- Be prompted to enter a 6-digit code from your authenticator app. + +New codes regenerate every few seconds. If the code expires, generate a new one. + +# Lost Recovery Codes or Authenticator App + +If you lose your mobile device and recovery codes, a **Domain Admin** can reset your 2FA **only if**: -## Lost recovery codes and authenticator app +- You use a company email or a domain you own. +- The Domain Admin also has 2FA enabled. -If you have lost your mobile device and can’t find your recovery codes, your Domain Admin can complete the steps below to reset your 2FA **only if (1) you use a company email address or email address on a domain that you own and (2) the Domain Admin also has 2FA enabled**: +## Reset 2FA as a Domain Admin -If your domain has 2FA enabled, a domain admin can follow Settings > Domains > Domain Members and click **Edit Settings** for your email address. -They can then click **Reset** to reset two-factor authentication (2FA) on your account. This will allow you to gain access to your account on the web or mobile app and configure 2FA again. +1. Navigate to **Settings > Domains > Domain Members**. +2. Click **Edit Settings** for the affected email address. +3. Click **Reset** to disable 2FA. +4. The user can now log in and reconfigure 2FA. -If your domain does not have 2FA enabled, a domain admin can follow Settings > Domains > Domain Members and enable Two Factor Authentication. Then they can follow the previously mentioned steps to reset 2FA for your account. +If your domain does not have 2FA enabled: +1. Go to **Settings > Domains > Domain Members**. +2. Enable **Two-Factor Authentication**. +3. Follow the previous steps to reset 2FA for the user. {% include info.html %} -If you use a public email address such as gmail, hotmail, or yahoo, we unfortunately can’t help you disable your 2FA setting. If you are unable to find your recovery codes, you may need to create a new Expensify account with a different email address. +If you use a public email (e.g., Gmail, Yahoo, Hotmail), Expensify cannot disable 2FA. If recovery codes are lost, you may need to create a new account with a different email. {% include end-info.html %} -If you don’t have a Domain Admin, follow the steps in this [guide](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain) to verify the domain. +If no Domain Admin is available, follow [this guide](https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain) to verify your domain. -## General troubleshooting +# General Troubleshooting -Make sure your phone’s time is set to automatically update (a manual time that’s fractionally different can cause issues). -Try disabling 2FA using a device that you are still logged into. For example, if you’re having trouble logging in with your computer, try to see if your mobile device is still logged in. If so, -Hover over Settings, then click Account. -Under the Account Details tab, scroll down to the Two Factor Authentication section and disable the toggle. -Try logging in with your other device. -Once you’ve logged in again, you can re-enable 2FA. +- Ensure your phone’s time is set to **automatic update**. A manual time difference can cause issues. +- If you are still logged in on another device: + 1. Hover over **Settings**, then click **Account**. + 2. Under the **Account Details** tab, scroll to **Two-Factor Authentication** and disable the toggle. + 3. Try logging in again, then re-enable 2FA. -
+Following these steps ensures your account remains secure while preventing access issues. diff --git a/docs/articles/expensify-classic/settings/Merge-accounts.md b/docs/articles/expensify-classic/settings/Merge-accounts.md index b2e3b0cdf55b..e9e90accc459 100644 --- a/docs/articles/expensify-classic/settings/Merge-accounts.md +++ b/docs/articles/expensify-classic/settings/Merge-accounts.md @@ -1,30 +1,43 @@ --- -title: Merge accounts -description: Merge two Expensify accounts into one +title: Merge Accounts +description: Learn how to merge two Expensify accounts into one. --- -
-If you have two Expensify accounts (for example, a personal account and a separate account for your company), you can combine the two accounts by merging them. Once merged, all receipts, expenses, expense reports, invoices, bills, imported cards, secondary logins, co-pilots, and group workspace settings from both accounts will be combined into one account. +If you have two Expensify accounts (e.g., a personal account and a separate company account), you can combine them by merging. This process consolidates all receipts, expenses, reports, invoices, bills, imported cards, secondary logins, co-pilots, and group workspace settings into one account. {% include info.html %} -Merging two accounts is a permanent action that cannot be reversed. To merge a company and personal account, you must sign in to your company account and merge your personal account with it. You cannot merge a company account into a personal account, nor can you merge two different company accounts together if they are private domains. +Merging accounts is **permanent** and **cannot be undone**. To merge a company and personal account, log in to your **company account** and merge your **personal account** with it. + +- You **cannot** merge a company account into a personal account. +- You **cannot** merge two company accounts if they belong to private domains. {% include end-info.html %} -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* +*Note: This process must be completed from the Expensify website and is not available in the mobile app.* + +--- + +## How to Merge Accounts -1. Log in to Expensify using the account you want to keep as the primary. -2. Hover over Settings and click **Account**. -3. Under Account Details, scroll down to the Merge Accounts section. -4. Enter the email address or phone number associated with the account that you want to merge into this account. -5. Select the “Yes, I understand this is not reversible” checkbox to agree. +1. Log in to Expensify using the **account you want to keep** as the primary. +2. Hover over **Settings** and click **Account**. +3. Scroll down to the **Merge Accounts** section under Account Details. +4. Enter the **email address or phone number** associated with the account you want to merge. +5. Select the **“Yes, I understand this is not reversible”** checkbox. 6. Click **Merge Accounts**. -7. Check your email for the magic code sent from Expensify and copy the code. -8. Paste the code into the field and click **Merge**. +7. Check your email for the **magic code** sent from Expensify. +8. Copy and paste the code into the field, then click **Merge**. -# FAQ +--- -**What information merges into my new account?** +# FAQ -All receipts, expenses, expense reports, invoices, bills, imported cards, secondary logins, co-pilots, and group workspace settings will be merged into your new account. +**What happens to my data when I merge accounts?** +All of the following will be transferred into your new account: +- Receipts and expenses +- Expense reports +- Invoices and bills +- Imported cards +- Secondary logins +- Co-pilots +- Group workspace settings -
diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md index bc39e33bab4a..ee181706d70d 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md @@ -1,48 +1,46 @@ --- -title: Expensify plan types and pricing +title: Expensify Plan Types and Pricing +description: An overview of Expensify's plan types and pricing description: An overview of plan types and pricing --- -
-Expensify offers plans and flexible pricing to cater to different business sizes and needs, whether you’re self-employed, part of a large organization, or anything in between. +Expensify offers flexible pricing plans designed to suit different business sizes and needs, whether you’re self-employed, part of a large organization, or somewhere in between. # Choosing the Right Plan -Expensify offers two pricing plans: +Expensify provides two main pricing plans: +| Feature | **Collect Plan** | **Control Plan** | +|----------------------|--------------------------------------------------|--------------------------------------------------| +| **Ideal for** | Small teams or businesses with 1-10 employees | Larger companies with 10-1000 employees | +| **Pricing*** | $5 USD per user/month | $9 USD per user/month | +| **SmartScans** | ✔ Unlimited | ✔ Unlimited | +| **Expensify Card** | ✔ Smart Limits & 1-2% cash back | ✔ Smart Limits & 1-2% cash back | +| **Expense Approvals** | ✔ Yes | ✔ Multiple approvers | +| **ACH Reimbursement** | ✔ Unlimited | ✔ Unlimited | +| **Bank Feed Support** | ❌ Not available | ✔ Third-party card feeds & reconciliation | +| **Accounting Sync** | ✔ QuickBooks Online & Xero | ✔ NetSuite, Sage Intacct, QuickBooks Desktop | +| **HR & Payroll Sync** | ❌ Not available | ✔ Gusto, Zenefits, Certinia, Workday | +| **Security & Control**| ❌ Not available | ✔ SAML/SSO & admin-enforced controls | -| | Collect Plan | Control Plan | -|--------------------|---------------------------|---------------------------------------------------------| -| **Ideal for:** | Sole proprietors and small teams or businesses with 1-10 employees | Larger companies with 10-1000 employees and more complex expense management needs | -| **Pricing starts at:** | $5 USD per user/month on an annual subscription (Non-USD prices available in FAQ) | $9 USD per user/month on an annual subscription (Non-USD prices available in FAQ) | -| | ✔ Unlimited SmartScans and distance tracking | ✔ All Collect Plan features | -| | ✔ Expensify Cards with Smart Limits and cash back | ✔ Third-party card feeds and reconciliation | -| | ✔ Expense approvals | ✔ Integration with NetSuite, Sage Intacct, and QuickBooks Desktop | -| | ✔ Unlimited ACH reimbursement | ✔ Gusto, Zenefits, Certinia, and Workday sync | -| | ✔ Integration with QuickBooks Online and Xero | ✔ Multiple expense approvers | -| | | ✔ SAML/SSO for added security | -| | | ✔ Admin-enforced controls | +***Note**: This price is available if you have an **Annual Subscription** and your team adopts the **Expensify Card**. Expensify Card usage on both plans generates 1% cash back with every swipe on US purchases—no minimums necessary—and 2% back if you spend $250k+/month across cards. -Expensify Card usage on both plans generates 1% cash back with every swipe on US purchases --- no minimums necessary --- and 2% back if you spend $250k+/month across cards. +--- # FAQ ## How much does Expensify cost? - -The cost depends on your plan and subscription type. Expensify offers a 50% discount for annual subscriptions and up to another 50% discount for using Expensify Cards. Try out our [savings calculator](https://use.expensify.com/savings-calculator) for an easy estimate based on your numbers. +The cost depends on your plan and subscription type. Expensify offers a 50% discount for annual subscriptions and up to another 50% discount for using Expensify Cards. Try out our [savings calculator](https://use.expensify.com/savings-calculator) to estimate your cost. ## Does Expensify bill in non-USD currencies? +Yes! Customers can pay in AUD, GBP, or NZD in addition to USD. -Yes! Customers can pay for Expensify in AUD, GBP, or NZD in addition to USD. -- The Collect plan begins at A$14, £8, or NZ$16 per user/month on an annual subscription -- The Control plan begins at A$30, £14, or NZ$32 per user/month on an annual subscription +- **Collect Plan:** A$14, £8, or NZ$16 per user/month (Annual subscription + Expensify Cards) +- **Control Plan:** A$30, £14, or NZ$32 per user/month (Annual subscription + Expensify Cards) ## Is Expensify free for individuals? - Yes! Individuals can use Expensify for free to track expenses. ## How do I get more info about pricing? +For customized information or help choosing the right plan, reach out to Expensify Concierge or email **concierge@expensify.com**. -For customized information or help choosing the right plan, reach out to Expensify Concierge or email concierge@expensify.com. - -
diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index 2157e05aa377..23c1bc58e5fc 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -56,6 +56,10 @@ When an expense is submitted to a workspace, your approver will receive an email ![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png){:width="100%"} ![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png){:width="100%"} +{% include info.html %} +SmartScan can only detect and process text written in the Latin alphabet. +{% include end-info.html %} + {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} diff --git a/docs/articles/new-expensify/settings/Add-personal-information.md b/docs/articles/new-expensify/settings/Add-personal-information.md deleted file mode 100644 index 492d349357ec..000000000000 --- a/docs/articles/new-expensify/settings/Add-personal-information.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Add personal information -description: Add your legal name, DOB, and/or address for travel and payments ---- -
- -You can add private details to your Expensify account that are only visible to you, such as your legal name, date of birth, and/or address. This information is useful for booking travel and for payment purposes. - -To add or update your private account details, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Scroll down to the Private details section and click the Legal Name, Date of Birth, and/or Address fields to update them. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu. -3. Scroll down to the Private details section and tap the Legal Name, Date of Birth, and/or Address fields to update them. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Add-profile-photo.md b/docs/articles/new-expensify/settings/Add-profile-photo.md deleted file mode 100644 index 60e56deaafbc..000000000000 --- a/docs/articles/new-expensify/settings/Add-profile-photo.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Add profile photo -description: Add an image to your profile ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences.md b/docs/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences.md new file mode 100644 index 000000000000..6161b853cc26 --- /dev/null +++ b/docs/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences.md @@ -0,0 +1,134 @@ +--- +title: Manage Profile and Account Preferences +description: Learn how to update your profile settings, preferences, and notifications in Expensify. +--- + +Expensify allows users to customize their profile and preferences to enhance their experience. This guide covers how to update your profile photo, set your timezone, change your language, manage notification settings, update your pronouns, switch themes, share your status, and update your personal information. + +## Main Uses +- Customize your profile settings to match your preferences. +- Manage notifications and language settings for better communication. +- Adjust appearance settings for improved user experience. +- Provide personal details for travel and payment purposes. + +## Core Users +- Expensify members managing their profile. +- Admins assisting employees with profile settings. +- Users looking to personalize their account. + +## Key Advantages +- Easy-to-use profile customization features. +- Syncs settings across all Expensify platforms. +- Enhances communication, accessibility, and travel and payment processes. + +--- +# Profile Customization: What profile settings can I update? +Expensify allows you to personalize your account with the following options: + +- **Profile Photo** – Add or update your profile image. +- **Status** – Display a custom status message for your team. +- **Pronouns** – Choose pronouns to be displayed on your account. +- **Language** – Change your account language to Spanish or another supported language. +- **Timezone** – Set your correct timezone for accurate timestamps. +- **Theme** – Switch between light mode and dark mode, or match your device settings. +- **Notifications** – Manage email and in-app notification preferences. +- **Personal Information** – Add or update your legal name, date of birth, and address. + +--- +# How to Access and Update + +## Profile Photo: How do I add or update my profile photo? +To upload a new profile picture: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select the **Edit** (pencil) icon next to your profile image. +3. Choose **Upload Image** and select a new image from your files. + +## Timezone: How do I set my timezone? +To update your timezone settings: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Profile** from the left menu. +3. Click **Timezone** and select your preferred timezone. + +## Language: How do I change my account language? +To switch your account language to Spanish: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Preferences**. +3. Click the **Language** option and choose **Spanish**. + +## Notifications: How do I manage my notification settings? +To customize the notifications you receive: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Preferences**. +3. Adjust the toggles under **Notifications**: + - **Receive relevant feature updates and Expensify news**: Enable to receive emails and in-app notifications about new features. + - **Mute all sounds from Expensify**: Enable to silence all in-app notification sounds. + +## Status: How do I update my profile status? +To share your status with your team: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Profile**. +3. Click **Status** and enter a custom status message. +4. (Optional) Click the **emoji icon** to add an emoji. +5. Set an expiration time under **Clear After** (e.g., 30 minutes, 1 hour, etc.). +6. Click **Save**. + +## Pronouns: How do I update my pronouns? +To set your preferred pronouns: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Profile**. +3. Click **Pronouns** and choose from the available options. + +## Theme: How do I switch between light and dark mode? +To change Expensify’s appearance: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Preferences**. +3. Click **Theme** and choose: + - **Dark mode**: A dark background theme. + - **Light mode**: A light background theme. + - **Use Device Settings**: Matches your device’s default theme. + +## Personal Information: How do I add or update my personal information? +To update your legal name, date of birth, or address: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Profile** from the left menu. +3. Scroll to the **Private details** section and update the **Legal Name**, **Date of Birth**, and/or **Address** fields. + +## Name: How do I update my display or legal name? +To change your display or legal name: + +1. Press your **profile image or icon** in the bottom left menu. +2. Select **Profile** from the left menu. +3. Edit your name: + - **Display name**: Click **Display Name**, enter your preferred name, and click **Save**. + - **Legal name**: Scroll to the **Private Details** section, click **Legal Name**, update the fields, and click **Save**. + +--- +# FAQ + +## Profile: Why should I update my profile photo? +Updating your profile photo helps colleagues recognize you in chats and improves account personalization. + +## Language: Will changing my language setting affect my reports? +No, changing your language setting only updates the app’s interface and does not alter any report contents. + +## Notifications: Can I disable all Expensify notifications? +Yes, you can mute all sounds and opt out of feature updates by adjusting the notification preferences under **Preferences**. + +## Theme: Will my theme preference sync across devices? +Yes, your theme preference applies across all Expensify apps, including mobile, web, and desktop. + +## Status: Can I set a permanent status message? +Yes, after updating your status, choose **Never** under _**When should we clear your status?**_ + +## Personal Information: Why should I add my legal name and address? +Providing your legal name, date of birth, and address is useful for booking travel and securely receiving payments. + +--- diff --git a/docs/articles/new-expensify/settings/Preferences.md b/docs/articles/new-expensify/settings/Preferences.md deleted file mode 100644 index b94c9d35c1a1..000000000000 --- a/docs/articles/new-expensify/settings/Preferences.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Preferences -description: How to manage your Expensify Preferences ---- -# Overview -Your Preferences in Expensify allow you to customize how you use New Expensify. - -- Set your theme preference - -# How to set your theme preference in New Expensify - -To set or update your theme preference in New Expensify: -1. Go to **Settings > Preferences** -2. Tap on **Theme** -3. You can choose between the _Dark_ theme, the _Light_ theme, or _Use Device Settings_ - -_Use Device Settings_ is the default setting. - -Selecting _Use Device Settings_ will use your device's theme settings. For example, if your device is set to adjust the appearance from light to dark during the day, we'll match that. - -Your theme preference will sync across all your New Expensify apps (mobile, web, or OSX desktop apps). diff --git a/docs/articles/new-expensify/settings/Set-timezone.md b/docs/articles/new-expensify/settings/Set-timezone.md deleted file mode 100644 index 11ce1340c7bb..000000000000 --- a/docs/articles/new-expensify/settings/Set-timezone.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Set timezone -description: Set your timezone ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Timezone** to select your timezone. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu -3. Tap **Timezone** to select your timezone. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md deleted file mode 100644 index a431d34fbc0f..000000000000 --- a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Switch account language to Spanish -description: Change your account language ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Click the Language option and select **Spanish**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Tap the Language option and select **Spanish**. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md deleted file mode 100644 index 34f96f9f5f7d..000000000000 --- a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Switch to light or dark mode -description: Change the appearance of Expensify ---- -
- -Expensify has three theme options that determine how the app looks: -- **Dark mode**: The app appears with a dark background -- **Light mode**: The app appears with a light background -- **Use Device settings**: Expensify will automatically use your device’s default theme - -To change your Expensify theme, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Click the **Theme** option and select the desired theme. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Tap the **Theme** option and select the desired theme. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md deleted file mode 100644 index e4111b3d02d3..000000000000 --- a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Update notification preferences -description: Determine how you want to receive Expensify notifications ---- -
- -To customize the email and in-app notifications you receive from Expensify, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Enable or disable the toggles under Notifications: - - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. - - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Enable or disable the toggles under Notifications: - - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. - - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Update-your-name.md b/docs/articles/new-expensify/settings/Update-your-name.md deleted file mode 100644 index d6b65def12ac..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-name.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Update your name -description: Update your display or legal name ---- -
- -Your Expensify account includes two names: -- Your display name that everyone can see (which can include a nickname) -- Your legal name that only you can see (for booking travel and for payment purposes) - -To update your display or legal name, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Edit your name. - - **Display name**: Click **Display Name** and enter your first name (or nickname) and last name into the fields and click **Save**. This name will be visible to anyone in your company workspace. - - **Legal name**: Scroll down to the Private Details section and click **Legal name**. Then enter your legal first and last name and click **Save**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap Profile in the left menu. -3. Edit your name. - - **Display name**: Tap **Display Name** and enter your first name (or nickname) and last name into the fields and tap **Save**. This name will be visible to anyone in your company workspace. - - **Legal name**: Scroll down to the Private Details section and tap **Legal name**. Then enter your legal first and last name and tap **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/settings/Update-your-profile-status.md b/docs/articles/new-expensify/settings/Update-your-profile-status.md deleted file mode 100644 index 5e5130f69cd5..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-profile-status.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Update your profile status -description: Share your status with your team ---- -
- -You can update your status in Expensify to let your coworkers know if you are out of the office, in a meeting, or even list your work hours or a different message. This message will appear when someone clicks on your profile or in a chat conversation. - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Status**. -4. (Optional) Click the emoji icon to add an emoji. -5. Click the message field and enter a status. For example, out of office, in a meeting, at lunch, etc. -6. Click **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. -7. Click **Save**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu. -3. Tap **Status**. -4. (Optional) Tap the emoji icon to add an emoji. -5. Tap the message field and enter a status. For example, out of office, in a meeting, at lunch, Office Hours: M-F 8-5 PT, etc. -6. Tap **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. -7. Tap **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
- diff --git a/docs/articles/new-expensify/settings/Update-your-pronouns.md b/docs/articles/new-expensify/settings/Update-your-pronouns.md deleted file mode 100644 index bf0e902092ff..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-pronouns.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Update your pronouns -description: Display your pronouns on your account ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu. -3. Tap **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. -{% include end-option.html %} - -{% include end-selector.html %} - -
diff --git a/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md b/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md index c0a467bd220e..eab24598b162 100644 --- a/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md +++ b/docs/articles/new-expensify/travel/Manage-Travel-Member-Roles.md @@ -12,16 +12,14 @@ To assign a role to a travel member, 2. Click **Book or manage travel**. 3. Click the **Program** tab at the top and select **Users**. 4. Click the name of the member whose role you wish to update. -5. Click the **Roles** tab and select a role. - - **Traveler**: Can only book travel for themselves. - - **Travel Arranger**: Can book travel for themselves and for other workspace members. Arrangers can be set to arrange travel for everyone in the workspace or for specific individuals only. - - **Company Admin**: Can book travel for themselves as well as any other workspace members. They can also access administrative features to: - - Define travel policies - - Add Users - - Remove Users - - Add and configure corporate cards as payment methods - - View analytics and metrics - - Use the Safety feature +5. Click the **Roles** tab and select a role. + - **Traveler**: Can only book travel for themselves. + - **Travel Arranger**: Can book travel for themselves and for other workspace members. Arrangers can be set to arrange travel for everyone in the workspace or for specific individuals only. + - **Company Admin**: Can book travel for themselves as well as any other workspace members. They can also access administrative features to: + - Define travel policies + - Add and configure corporate cards as payment methods + - View analytics and metrics + - Use the Safety feature 6. Click **Save**. diff --git a/docs/assets/images/commfeed/commfeed-01-updated.png b/docs/assets/images/commfeed/commfeed-01-updated.png new file mode 100644 index 000000000000..347847089abe Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-01-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-02-updated.png b/docs/assets/images/commfeed/commfeed-02-updated.png new file mode 100644 index 000000000000..00e30982d92d Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-02-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-03-updated.png b/docs/assets/images/commfeed/commfeed-03-updated.png new file mode 100644 index 000000000000..172ae62da80c Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-03-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-04-updated.png b/docs/assets/images/commfeed/commfeed-04-updated.png new file mode 100644 index 000000000000..6eb5daeed399 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-04-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-05-updated.png b/docs/assets/images/commfeed/commfeed-05-updated.png new file mode 100644 index 000000000000..a1831587d559 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-05-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-06-updated.png b/docs/assets/images/commfeed/commfeed-06-updated.png new file mode 100644 index 000000000000..918d6301eb6d Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-06-updated.png differ diff --git a/docs/assets/images/commfeed/commfeed-07-updated.png b/docs/assets/images/commfeed/commfeed-07-updated.png new file mode 100644 index 000000000000..63aaae0b1620 Binary files /dev/null and b/docs/assets/images/commfeed/commfeed-07-updated.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 9ccef010ec96..2ddd13d5fc8b 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -624,3 +624,15 @@ https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address,https://help.expensify.com/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO,https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards +https://help.expensify.com/articles/new-expensify/settings/Add-profile-photo,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Set-timezone,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Switch-account-language-to-Spanish,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-Notification-Preferences,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-profile-status,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-pronouns,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Preferences,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Switch-to-light-or-dark-mode,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Add-personal-information,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-name,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards +https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7efd6d5ebe1b..be90ce55ffaa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -120,8 +120,7 @@ platform :android do gradle( project_dir: 'Mobile-Expensify/Android', task: 'assemble', - flavor: 'Production', - build_type: 'Release', + build_type: 'Debug', ) setGradleOutputsInEnv() end @@ -197,17 +196,6 @@ platform :android do ) end - desc "Upload app to Firebase distribution" - lane :upload_firebase_distribution do - firebase_app_distribution( - app: "1:1008697809946:android:2e48f9ffe8d0b6a2", - service_credentials_file: "./firebase.json", - groups: "applause", - android_artifact_path: ENV[KEY_GRADLE_AAB_PATH], - android_artifact_type: "AAB" - ) - end - desc "Upload HybridApp to Google Play for internal testing" lane :upload_google_play_internal_hybrid do # Google is very unreliable, so we retry a few times @@ -458,7 +446,12 @@ platform :ios do ENV["ENVFILE"]=".env.production" build_app( workspace: "./ios/NewExpensify.xcworkspace", - scheme: "New Expensify" + scheme: "New Expensify", + configuration: "Debug", + sdk: "iphonesimulator", + skip_codesigning: true, + skip_archive: true, + export_method: "development" ) setIOSBuildOutputsInEnv() end @@ -468,7 +461,12 @@ platform :ios do ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp.ios" build_app( workspace: "./Mobile-Expensify/iOS/Expensify.xcworkspace", - scheme: "Expensify" + scheme: "Expensify", + configuration: "Debug", + sdk: "iphonesimulator", + skip_codesigning: true, + skip_archive: true, + export_method: "development" ) setIOSBuildOutputsInEnv() end @@ -518,16 +516,6 @@ platform :ios do sh("echo '{\"ipa_path\": \"#{lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../ios_paths.json") end - desc "Upload app to Firebase distribution" - lane :upload_firebase_distribution do - firebase_app_distribution( - app: "1:1008697809946:ios:3ffad71f664f2886", - service_credentials_file: "./firebase.json", - groups: "applause", - ipa_path: ENV[KEY_IPA_PATH], - ) - end - desc "Upload app to TestFlight" lane :upload_testflight do upload_to_testflight( diff --git a/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg b/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg new file mode 100644 index 000000000000..c9b3eb213f79 Binary files /dev/null and b/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index b282e883628d..25deb5ad9f83 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.92 + 9.0.94 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.92.1 + 9.0.94.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2c9396128bb8..acfce5c2e675 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.92 + 9.0.94 CFBundleSignature ???? CFBundleVersion - 9.0.92.1 + 9.0.94.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index e43588eea8dd..2e661cccef7f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.92 + 9.0.94 CFBundleVersion - 9.0.92.1 + 9.0.94.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b0e84d3d033f..395e6b1c618a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -166,6 +166,7 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (9.1.0) + - ForkInputMask (7.3.3) - FullStory (1.52.0) - fullstory_react-native (1.7.2): - DoubleConversion @@ -1604,6 +1605,28 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-advanced-input-mask (1.2.1): + - DoubleConversion + - ForkInputMask (~> 7.3.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-airship (19.2.1): - AirshipFrameworkProxy (= 7.1.2) - DoubleConversion @@ -2491,7 +2514,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.221): + - RNLiveMarkdown (0.1.223): - DoubleConversion - glog - hermes-engine @@ -2511,10 +2534,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.221) + - RNLiveMarkdown/newarch (= 0.1.223) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.221): + - RNLiveMarkdown/newarch (0.1.223): - DoubleConversion - glog - hermes-engine @@ -2880,6 +2903,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-advanced-input-mask (from `../node_modules/react-native-advanced-input-mask`) - "react-native-airship (from `../node_modules/@ua/react-native-airship`)" - react-native-app-logs (from `../node_modules/react-native-app-logs`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) @@ -2968,6 +2992,7 @@ SPEC REPOS: - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig + - ForkInputMask - GoogleAppMeasurement - GoogleDataTransport - GoogleSignIn @@ -3093,6 +3118,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-advanced-input-mask: + :path: "../node_modules/react-native-advanced-input-mask" react-native-airship: :path: "../node_modules/@ua/react-native-airship" react-native-app-logs: @@ -3270,6 +3297,7 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be + ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d fullstory_react-native: 63a803cca04b0447a71daa73e4df3f7b56e1919d glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a @@ -3323,6 +3351,7 @@ SPEC CHECKSUMS: React-logger: 26155dc23db5c9038794db915f80bd2044512c2e React-Mapbuffer: ad1ba0205205a16dbff11b8ade6d1b3959451658 React-microtasksnativemodule: e771eb9eb6ace5884ee40a293a0e14a9d7a4343c + react-native-advanced-input-mask: 22e3bd2a0f38fada50b475c98bf39d39053097a3 react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc react-native-app-logs: ee32b6e80bf8d1b883dfc5ac96efa7c1bd9a06a5 react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44 @@ -3383,7 +3412,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 364e6862a112045bb5c5d35601f0bdb0304af979 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 9940212ca19bf54101b585178e691ee040b82c35 + RNLiveMarkdown: 5c76c659b125006ff525a095b65184ecb72392f3 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: d184c8d3213acf4c97ec71fbbb6f9d4954552d80 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/package-lock.json b/package-lock.json index 4222166d7e32..ef23310e7f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "new.expensify", - "version": "9.0.92-1", + "version": "9.0.94-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.92-1", + "version": "9.0.94-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-background-task": "file:./modules/background-task", - "@expensify/react-native-live-markdown": "0.1.221", + "@expensify/react-native-live-markdown": "0.1.223", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -79,6 +79,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", + "react-native-advanced-input-mask": "1.2.1", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", @@ -3641,9 +3642,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.221", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.221.tgz", - "integrity": "sha512-2CeBE1LsNvslaqYmPlf1hsl5gqG3eMsn+7jUSAZ4YmQqz1iLKJn+ryQVE4Rl0eLeeikWDlKvqX9isQHgKofLgw==", + "version": "0.1.223", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.223.tgz", + "integrity": "sha512-rE5cQ9lBDP2tqtR4Tta3PNx2i5K83sdht1meYMvmLPqFVy7C9A743wzZe6oudVnhSDem8MbU4NMJStadp9xn6Q==", "license": "MIT", "workspaces": [ "./example", @@ -32027,6 +32028,23 @@ } } }, + "node_modules/react-native-advanced-input-mask": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.2.1.tgz", + "integrity": "sha512-qXK6l8f5zOLrWxhrtA2od4R2UsV8OEcvFlZlX5VTp3sB/JlHW/iJd15m8Rgn/mcJFfvnKlHmVVHJefDrUOJFvA==", + "license": "MIT", + "workspaces": [ + "example", + "WebExample" + ], + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-android-location-enabler": { "version": "2.0.1", "license": "MIT", diff --git a/package.json b/package.json index 6be584d00bf9..ae6c407494fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.92-1", + "version": "9.0.94-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.", @@ -39,7 +39,9 @@ "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts", "desktop-build-adhoc": "./scripts/build-desktop.sh adhoc", "ios-build": "bundle exec fastlane ios build_unsigned", + "ios-hybrid-build": "bundle exec fastlane ios build_unsigned_hybrid", "android-build": "bundle exec fastlane android build_local", + "android-hybrid-build": "bundle exec fastlane android build_local_hybrid", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", @@ -71,13 +73,13 @@ "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", "react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt", "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy", - "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy", + "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy && ./scripts/parser-workletization.sh src/libs/SearchParser/autocompleteParser.js", "web:prod": "http-server ./dist --cors" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.221", "@expensify/react-native-background-task": "file:./modules/background-task", + "@expensify/react-native-live-markdown": "0.1.223", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -144,6 +146,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", + "react-native-advanced-input-mask": "1.2.1", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", diff --git a/scripts/parser-workletization.sh b/scripts/parser-workletization.sh new file mode 100755 index 000000000000..ab048e407de3 --- /dev/null +++ b/scripts/parser-workletization.sh @@ -0,0 +1,22 @@ +#!/bin/bash +### +# This script modifies the autocompleteParser.js file to be compatible with worklets. +# autocompleteParser.js is generated by PeggyJS and uses some parts of syntax not supported by worklets. +# This script runs each time the parser is generated by the `generate-autocomplete-parser` command. +### + +filePath=$1 + +if [ ! -f "$filePath" ]; then + echo "$filePath does not exist." + exit 1 +fi +# shellcheck disable=SC2016 +if awk 'BEGIN { print "\47worklet\47\n\nclass peg\$SyntaxError{}" } 1' "$filePath" | sed 's/function peg\$SyntaxError/function temporary/g' | sed 's/peg$subclass(peg$SyntaxError, Error);//g' > tmp.txt; then + mv tmp.txt "$filePath" + echo "Successfully updated $filePath" +else + echo "An error occurred while modifying the file." + rm -f tmp.txt + exit 1 +fi diff --git a/scripts/run-build.sh b/scripts/run-build.sh index 70e0dcf7c586..0abbd4530adf 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -38,6 +38,9 @@ NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" SCHEME="Expensify Dev" APP_ID="org.me.mobiexpensifyg.dev" + # Build Yapl JS + cd Mobile-Expensify && npm run grunt:build:shared && cd .. + echo -e "\n${GREEN}Starting a HybridApp build!${NC}" PROJECT_ROOT_PATH="Mobile-Expensify/" export CUSTOM_APK_NAME="Expensify-debug.apk" diff --git a/scripts/shellCheck.sh b/scripts/shellCheck.sh index d148958900d4..4d8155eeca88 100755 --- a/scripts/shellCheck.sh +++ b/scripts/shellCheck.sh @@ -9,9 +9,11 @@ source scripts/shellUtils.sh declare -r DIRECTORIES_TO_IGNORE=( './node_modules' + './desktop/node_modules' './vendor' './ios/Pods' './.husky' + './docs/vendor' ) # This lists all shell scripts in this repo except those in directories we want to ignore diff --git a/src/App.tsx b/src/App.tsx index f9403e258af1..3513cb23953b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,8 +43,9 @@ import type {Route} from './ROUTES'; import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; +/** Values passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ type AppProps = { - /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ + /** URL containing all necessary data to run React Native app (e.g. login data) */ url?: Route; }; diff --git a/src/CONST.ts b/src/CONST.ts index b4c70adba0c2..1250092cb910 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -75,6 +75,7 @@ const selectableOnboardingChoices = { const backendOnboardingChoices = { ADMIN: 'newDotAdmin', SUBMIT: 'newDotSubmit', + TRACK_WORKSPACE: 'newDotTrackWorkspace', } as const; const onboardingChoices = { @@ -101,6 +102,50 @@ const selfGuidedTourTask: OnboardingTask = { description: ({navatticURL}) => `[Take a self-guided product tour](${navatticURL}) and learn about everything Expensify has to offer.`, }; +const createWorkspaceTask: OnboardingTask = { + type: 'createWorkspace', + autoCompleted: true, + title: 'Create a workspace', + description: ({workspaceSettingsLink}) => + '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + + '\n' + + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click *Settings*.\n' + + '2. Click *Workspaces* > *New workspace*.\n' + + '\n' + + `*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`, +}; + +const meetGuideTask: OnboardingTask = { + type: 'meetGuide', + autoCompleted: false, + title: 'Meet your setup specialist', + 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}).`, +}; + +const setupCategoriesTask: OnboardingTask = { + type: 'setupCategories', + autoCompleted: false, + title: 'Set up categories', + description: ({workspaceCategoriesLink}) => + '*Set up categories* so your team can code expenses for easy reporting.\n' + + '\n' + + 'Here’s how to set up categories:\n' + + '\n' + + '1. Click *Settings*.\n' + + '2. Go to *Workspaces*.\n' + + '3. Select your workspace.\n' + + '4. Click *Categories*.\n' + + "5. Disable any categories you don't need.\n" + + '6. Add your own categories in the top right.\n' + + '\n' + + `[Take me to workspace category settings](${workspaceCategoriesLink}).`, +}; + const onboardingEmployerOrSubmitMessage: OnboardingMessage = { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', tasks: [ @@ -114,7 +159,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -137,7 +182,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -161,7 +206,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -184,7 +229,7 @@ const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -5007,30 +5052,9 @@ const CONST = { [onboardingChoices.MANAGE_TEAM]: { message: 'Here are some important tasks to help get your team’s expenses under control.', tasks: [ - { - type: 'createWorkspace', - autoCompleted: true, - title: 'Create a workspace', - description: ({workspaceSettingsLink}) => - '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + - '\n' + - 'Here’s how to create a workspace:\n' + - '\n' + - '1. Click *Settings*.\n' + - '2. Click *Workspaces* > *New workspace*.\n' + - '\n' + - `*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`, - }, + createWorkspaceTask, selfGuidedTourTask, - { - type: 'meetGuide', - autoCompleted: false, - title: 'Meet your setup specialist', - 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}).`, - }, + meetGuideTask, { type: 'setupCategoriesAndTags', autoCompleted: false, @@ -5040,24 +5064,7 @@ const CONST = { '\n' + `Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceSettingsLink}).`, }, - { - type: 'setupCategories', - autoCompleted: false, - title: 'Set up categories', - description: ({workspaceCategoriesLink}) => - '*Set up categories* so your team can code expenses for easy reporting.\n' + - '\n' + - 'Here’s how to set up categories:\n' + - '\n' + - '1. Click *Settings*.\n' + - '2. Go to *Workspaces*.\n' + - '3. Select your workspace.\n' + - '4. Click *Categories*.\n' + - "5. Disable any categories you don't need.\n" + - '6. Add your own categories in the top right.\n' + - '\n' + - `[Take me to workspace category settings](${workspaceCategoriesLink}).`, - }, + setupCategoriesTask, { type: 'setupTags', autoCompleted: false, @@ -5135,6 +5142,42 @@ const CONST = { }, ], }, + [onboardingChoices.TRACK_WORKSPACE]: { + message: 'Here are some important tasks to help get your workspace set up.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team-v2.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + createWorkspaceTask, + meetGuideTask, + setupCategoriesTask, + { + type: 'inviteAccountant', + autoCompleted: false, + title: 'Invite your accountant', + description: ({workspaceMembersLink}) => + '*Invite your accountant* to Expensify and share your expenses with them to make tax time easier.\n' + + '\n' + + 'Here’s how to invite your accountant:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Go to *Workspaces*.\n' + + '3. Select your workspace.\n' + + '4. Click *Members* > Invite member.\n' + + '5. Enter their email or phone number.\n' + + '6. Add an invite message if you’d like.\n' + + '7. You’ll be set as the expense approver. You can change this to any admin once you invite your team.\n' + + '\n' + + 'That’s it, happy expensing! 😄\n' + + '\n' + + `[View your workspace members](${workspaceMembersLink}).`, + }, + ], + }, [onboardingChoices.PERSONAL_SPEND]: onboardingPersonalSpendMessage, [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', @@ -5149,7 +5192,7 @@ const CONST = { '\n' + 'Here’s how to start a chat:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Start chat*.\n' + '3. Enter emails or phone numbers.\n' + '\n' + @@ -5166,7 +5209,7 @@ const CONST = { '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button\n' + '2. Choose *Start chat*.\n' + '3. Enter any email, SMS, or name of who you want to split with.\n' + '4. From within the chat, click the *+* button on the message bar, and click *Split expense*.\n' + @@ -5179,15 +5222,7 @@ const CONST = { [onboardingChoices.ADMIN]: { message: "As an admin, learn how to manage your team's workspace and submit expenses yourself.", tasks: [ - { - type: 'meetSetupSpecialist', - autoCompleted: false, - title: 'Meet your setup specialist', - description: - '*Meet your setup specialist* who can answer any questions as you get started with Expensify. Yes, a real human!' + - '\n' + - 'Chat with them in your #admins room or schedule a call today.', - }, + meetGuideTask, { type: 'reviewWorkspaceSettings', autoCompleted: false, @@ -5209,7 +5244,7 @@ const CONST = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -5976,6 +6011,13 @@ const CONST = { TRAIN: 'train', }, + CANCELLATION_POLICY: { + UNKNOWN: 'UNKNOWN', + NON_REFUNDABLE: 'NON_REFUNDABLE', + FREE_CANCELLATION_UNTIL: 'FREE_CANCELLATION_UNTIL', + PARTIALLY_REFUNDABLE: 'PARTIALLY_REFUNDABLE', + }, + DOT_SEPARATOR: '•', DEFAULT_TAX: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0e977c0ddb1e..54b7da704cd1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -324,7 +324,7 @@ const ONYXKEYS = { // The theme setting set by the user in preferences. // This can be either "light", "dark" or "system" - PREFERRED_THEME: 'preferredTheme', + PREFERRED_THEME: 'nvp_preferredTheme', // Information about the onyx updates IDs that were received from the server ONYX_UPDATES_FROM_SERVER: 'onyxUpdatesFromServer', @@ -548,6 +548,9 @@ const ONYXKEYS = { /** Expensify cards settings */ PRIVATE_EXPENSIFY_CARD_SETTINGS: 'private_expensifyCardSettings_', + /** Expensify cards manual billing setting */ + PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING: 'private_expensifyCardManualBilling_', + /** Stores which connection is set up to use Continuous Reconciliation */ EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'expensifyCard_continuousReconciliationConnection_', @@ -898,6 +901,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod; [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CardFeeds; [ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings; + [ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING]: boolean; [ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d08669c2693d..393085ab4384 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -68,7 +68,10 @@ const ROUTES = { SEARCH_ADVANCED_FILTERS_POSTED: 'search/filters/posted', SEARCH_REPORT: { route: 'search/view/:reportID/:reportActionID?', - getRoute: ({reportID, reportActionID, backTo}: {reportID: string; reportActionID?: string; backTo?: string}) => { + getRoute: ({reportID, reportActionID, backTo}: {reportID: string | undefined; reportActionID?: string; backTo?: string}) => { + if (!reportID) { + Log.warn('Invalid reportID is used to build the SEARCH_REPORT route'); + } const baseRoute = reportActionID ? (`search/view/${reportID}/${reportActionID}` as const) : (`search/view/${reportID}` as const); return getUrlWithBackToParam(baseRoute, backTo); }, @@ -341,7 +344,12 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', - getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo), + getRoute: (reportID: string | undefined, backTo?: string) => { + if (!reportID) { + Log.warn('Invalid reportID is used to build the REPORT_WITH_ID_DETAILS_SHARE_CODE route'); + } + return getUrlWithBackToParam(`r/${reportID}/details/shareCode` as const, backTo); + }, }, ATTACHMENTS: { route: 'attachment', @@ -518,7 +526,7 @@ const ROUTES = { }, MONEY_REQUEST_STEP_CATEGORY: { route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, MONEY_REQUEST_ATTENDEE: { diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 0feabf9b6092..d4c56c43835f 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -162,7 +162,13 @@ function AddressSearch( // Make sure that the order of keys remains such that the country is always set above the state. // Refer to https://github.com/Expensify/App/issues/15633 for more information. - const {country: countryFallbackLongName = '', state: stateAutoCompleteFallback = '', city: cityAutocompleteFallback = ''} = getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); + const { + country: countryFallbackLongName = '', + state: stateAutoCompleteFallback = '', + city: cityAutocompleteFallback = '', + street: streetAutocompleteFallback = '', + streetNumber: streetNumberAutocompleteFallback = '', + } = getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName); @@ -170,7 +176,7 @@ function AddressSearch( const country = countryPrimary || countryFallback || ''; const values = { - street: `${streetNumber} ${streetName}`.trim(), + street: `${streetNumber || streetNumberAutocompleteFallback} ${streetName || streetAutocompleteFallback}`.trim(), name: details.name ?? '', // Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise. street2: subpremise, diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx new file mode 100644 index 000000000000..4d54258dbef0 --- /dev/null +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type {ForwardedRef} from 'react'; +import CONST from '@src/CONST'; +import TextInput from './TextInput'; +import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; + +type AmountFormProps = { + /** Amount supplied by the FormProvider */ + value?: string; + + /** Callback to update the amount in the FormProvider */ + onInputChange?: (value: string) => void; + + /** Should we allow negative number as valid input */ + shouldAllowNegative?: boolean; +} & Partial; + +function AmountWithoutCurrencyInput( + {value: amount, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + ref: ForwardedRef, +) { + return ( + + ); +} + +AmountWithoutCurrencyInput.displayName = 'AmountWithoutCurrencyForm'; + +export default React.forwardRef(AmountWithoutCurrencyInput); diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 078cbed25631..ed5ecf41078a 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -1,13 +1,12 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; import TextLink from './TextLink'; @@ -15,7 +14,6 @@ import TextLink from './TextLink'; type BrokenConnectionDescriptionProps = { /** Transaction id of the corresponding report */ transactionID: string | undefined; - /** Current report */ report: OnyxEntry; @@ -26,7 +24,7 @@ type BrokenConnectionDescriptionProps = { function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const transactionViolations = getTransactionViolations(transactionID); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); @@ -46,7 +44,12 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn {`${translate('violations.adminBrokenConnectionError')}`} Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id))} + onPress={() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id)); + }} >{`${translate('workspace.common.companyCards')}`} . diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 0b84f0034035..bf3746b61776 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -326,7 +326,8 @@ function FormProvider( value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. - defaultValue: undefined, + // If it's uncontrolled, then we set the `defaultValue` prop to actual value + defaultValue: inputProps.uncontrolled ? inputProps.defaultValue : undefined, onTouched: (event) => { if (!inputProps.shouldSetTouchedOnBlurOnly) { setTimeout(() => { diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d6bcc28e09bf..02cc4e899b32 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -118,6 +118,7 @@ type InputComponentBaseProps = Input autoGrowHeight?: boolean; blurOnSubmit?: boolean; shouldSubmitForm?: boolean; + uncontrolled?: boolean; }; type FormOnyxValues = Omit; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 12b515194928..332255e53995 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -80,6 +80,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim 'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}), 'mention-report': HTMLElementModel.fromCustomModel({tagName: 'mention-report', contentModel: HTMLContentModel.textual}), 'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}), + 'custom-emoji': HTMLElementModel.fromCustomModel({tagName: 'custom-emoji', contentModel: HTMLContentModel.textual}), 'next-step': HTMLElementModel.fromCustomModel({ tagName: 'next-step', mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16}, diff --git a/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx new file mode 100644 index 000000000000..8cd33eab6c90 --- /dev/null +++ b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx @@ -0,0 +1,21 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; + +type CustomEmojiWithDefaultPressableActionProps = { + /* Key name identifying the emoji */ + emojiKey: string; + + /* Emoji content to render */ + children: ReactNode; +}; + +function CustomEmojiWithDefaultPressableAction({emojiKey, children}: CustomEmojiWithDefaultPressableActionProps) { + if (emojiKey === 'actionMenuIcon') { + return {children}; + } + + return children; +} + +export default CustomEmojiWithDefaultPressableAction; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx new file mode 100644 index 000000000000..dab8c89013dd --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {FC} from 'react'; +import {View} from 'react-native'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import type {SvgProps} from 'react-native-svg'; +import GlobalCreateIcon from '@assets/images/customEmoji/global-create.svg'; +import CustomEmojiWithDefaultPressableAction from '@components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction'; +import ImageSVG from '@components/ImageSVG'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; + +const emojiMap: Record> = { + actionMenuIcon: GlobalCreateIcon, +}; + +function CustomEmojiRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const emojiKey = tnode.attributes.emoji; + + if (emojiMap[emojiKey]) { + const image = ( + + + + ); + + if ('pressablewithdefaultaction' in tnode.attributes) { + return {image}; + } + + return image; + } + + return null; +} + +export default CustomEmojiRenderer; +export {emojiMap}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 91ed66f8b931..bcf3d4dfaf94 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -1,6 +1,7 @@ import type {CustomTagRendererRecord} from 'react-native-render-html'; import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; +import CustomEmojiRenderer from './CustomEmojiRenderer'; import DeletedActionRenderer from './DeletedActionRenderer'; import EditedRenderer from './EditedRenderer'; import EmojiRenderer from './EmojiRenderer'; @@ -29,6 +30,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'mention-user': MentionUserRenderer, 'mention-report': MentionReportRenderer, 'mention-here': MentionHereRenderer, + 'custom-emoji': CustomEmojiRenderer, emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, 'deleted-action': DeletedActionRenderer, diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 894f5ddc2477..e48646204f34 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import TextBlock from '@components/TextBlock'; +import useLHNEstimatedListSize from '@hooks/useLHNEstimatedListSize'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -48,6 +49,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const styles = useThemeStyles(); const {translate, preferredLocale} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const estimatedListSize = useLHNEstimatedListSize(); const shouldShowEmptyLHN = shouldUseNarrowLayout && data.length === 0; // When the first item renders we want to call the onFirstItemRendered callback. @@ -284,6 +286,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio showsVerticalScrollIndicator={false} onLayout={onLayout} onScroll={onScroll} + estimatedListSize={estimatedListSize} /> )} diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 604e2b3065fd..1e7a9f796641 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -30,7 +30,7 @@ import Performance from '@libs/Performance'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import { isAdminRoom, - isChatUsedForOnboarding, + isChatUsedForOnboarding as isChatUsedForOnboardingReportUtils, isConciergeChatReport, isGroupChat, isOneOnOneChat, @@ -62,7 +62,8 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const session = useSession(); const shouldShowWokspaceChatTooltip = isPolicyExpenseChat(report) && activePolicyID === report?.policyID && session?.accountID === report?.ownerAccountID; const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); - const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) : isConciergeChatReport(report); + const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, introSelected?.choice); + const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) && isChatUsedForOnboarding : isConciergeChatReport(report); const isActiveRouteHome = useIsCurrentRouteHome(); const {tooltipToRender, shouldShowTooltip} = useMemo(() => { @@ -76,9 +77,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); - // During the onboarding flow, the introSelected NVP is not yet available. - const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); - const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -283,7 +281,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti isSystemChat(report) } /> - {isChatUsedForOnboarding(report, onboardingPurposeSelected) && } + {isChatUsedForOnboarding && } {isStatusVisible && ( { const state = navigationContainerRef.getRootState(); const targetRouteName = state?.routes?.[state?.index ?? 0]?.name; - if (!isSideModalNavigator(targetRouteName)) { + if (!isSideModalNavigator(targetRouteName) || isMobile()) { setHasNavigatedAway(true); } }); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 8492f18d2512..40ec431ca893 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -15,7 +15,7 @@ import getButtonState from '@libs/getButtonState'; import Parser from '@libs/Parser'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; -import {checkIfActionIsAllowed} from '@userActions/Session'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type {TooltipAnchorAlignment} from '@src/types/utils/AnchorAlignment'; @@ -611,7 +611,7 @@ function MenuItem( {(isHovered) => ( shouldBlockSelection && shouldUseNarrowLayout && canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 268ab770059e..0d0e862f36f5 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -8,6 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {markAsCash as markAsCashUtil} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -25,7 +26,6 @@ import { shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, } from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -61,13 +61,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID : CONST.DEFAULT_NUMBER_ID }`, ); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); @@ -88,7 +87,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { - markAsCashAction(transaction?.transactionID, reportID); + markAsCashUtil(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction); @@ -122,7 +121,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre ), }; } - if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID, transactionViolations))) { + if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { diff --git a/src/components/PinButton.tsx b/src/components/PinButton.tsx index 2ae74853d571..d5be4e32c1c6 100644 --- a/src/components/PinButton.tsx +++ b/src/components/PinButton.tsx @@ -2,8 +2,8 @@ import React from 'react'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportActions from '@userActions/Report'; -import * as Session from '@userActions/Session'; +import {togglePinnedState} from '@userActions/Report'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import type {Report} from '@src/types/onyx'; import Icon from './Icon'; @@ -24,7 +24,7 @@ function PinButton({report}: PinButtonProps) { return ( ReportActions.togglePinnedState(report.reportID, report.isPinned ?? false))} + onPress={callFunctionIfActionIsAllowed(() => togglePinnedState(report.reportID, report.isPinned ?? false))} style={styles.touchableButtonImage} accessibilityLabel={report.isPinned ? translate('common.unPin') : translate('common.pin')} role={CONST.ROLE.BUTTON} diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx index e6ce3080ee0a..79a213dc15b0 100644 --- a/src/components/PromotedActionsBar.tsx +++ b/src/components/PromotedActionsBar.tsx @@ -3,14 +3,14 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as HeaderUtils from '@libs/HeaderUtils'; -import * as Localize from '@libs/Localize'; +import {getPinMenuItem, getShareMenuItem} from '@libs/HeaderUtils'; +import {translateLocal} from '@libs/Localize'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {RootStackParamList, State} from '@libs/Navigation/types'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as ReportActions from '@userActions/Report'; -import * as Session from '@userActions/Session'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {joinRoom, navigateToAndOpenReport, navigateToAndOpenReportWithAccountIDs} from '@userActions/Report'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -44,25 +44,25 @@ type PromotedActionsType = Record P const PromotedActions = { pin: (report) => ({ key: CONST.PROMOTED_ACTIONS.PIN, - ...HeaderUtils.getPinMenuItem(report), + ...getPinMenuItem(report), }), share: (report, backTo) => ({ key: CONST.PROMOTED_ACTIONS.SHARE, - ...HeaderUtils.getShareMenuItem(report, backTo), + ...getShareMenuItem(report, backTo), }), join: (report) => ({ key: CONST.PROMOTED_ACTIONS.JOIN, icon: Expensicons.ChatBubbles, - text: Localize.translateLocal('common.join'), - onSelected: Session.checkIfActionIsAllowed(() => { + text: translateLocal('common.join'), + onSelected: callFunctionIfActionIsAllowed(() => { Navigation.dismissModal(); - ReportActions.joinRoom(report); + joinRoom(report); }), }), message: ({reportID, accountID, login}) => ({ key: CONST.PROMOTED_ACTIONS.MESSAGE, icon: Expensicons.CommentBubbles, - text: Localize.translateLocal('common.message'), + text: translateLocal('common.message'), onSelected: () => { if (reportID) { Navigation.dismissModal(reportID); @@ -71,18 +71,18 @@ const PromotedActions = { // The accountID might be optimistic, so we should use the login if we have it if (login) { - ReportActions.navigateToAndOpenReport([login]); + navigateToAndOpenReport([login]); return; } if (accountID) { - ReportActions.navigateToAndOpenReportWithAccountIDs([accountID]); + navigateToAndOpenReportWithAccountIDs([accountID]); } }, }), hold: ({isTextHold, reportAction, reportID, isDelegateAccessRestricted, setIsNoDelegateAccessMenuVisible, currentSearchHash}) => ({ key: CONST.PROMOTED_ACTIONS.HOLD, icon: Expensicons.Stopwatch, - text: Localize.translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`), + text: translateLocal(`iou.${isTextHold ? 'hold' : 'unhold'}`), onSelected: () => { if (isDelegateAccessRestricted) { setIsNoDelegateAccessMenuVisible(true); // Show the menu @@ -92,15 +92,15 @@ const PromotedActions = { if (!isTextHold) { Navigation.goBack(); } - const targetedReportID = reportID ?? reportAction?.childReportID ?? ''; + const targetedReportID = reportID ?? reportAction?.childReportID; const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State); if (topmostCentralPaneRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE && isTextHold) { - ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.REPORT_WITH_ID.getRoute(targetedReportID)); + changeMoneyRequestHoldStatus(reportAction, ROUTES.REPORT_WITH_ID.getRoute(targetedReportID)); return; } - ReportUtils.changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute({reportID: targetedReportID}), currentSearchHash); + changeMoneyRequestHoldStatus(reportAction, ROUTES.SEARCH_REPORT.getRoute({reportID: targetedReportID}), currentSearchHash); }, }), } satisfies PromotedActionsType; diff --git a/src/components/RNMaskedTextInput.tsx b/src/components/RNMaskedTextInput.tsx new file mode 100644 index 000000000000..22a69d2c7fbd --- /dev/null +++ b/src/components/RNMaskedTextInput.tsx @@ -0,0 +1,39 @@ +import type {ForwardedRef} from 'react'; +import React from 'react'; +import type {TextInput} from 'react-native'; +import type {MaskedTextInputProps} from 'react-native-advanced-input-mask'; +import {MaskedTextInput} from 'react-native-advanced-input-mask'; +import Animated from 'react-native-reanimated'; +import useTheme from '@hooks/useTheme'; + +// Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet +const AnimatedTextInput = Animated.createAnimatedComponent(MaskedTextInput); + +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput & HTMLInputElement; + +function RNMaskedTextInputWithRef(props: MaskedTextInputProps, ref: ForwardedRef) { + const theme = useTheme(); + + return ( + { + if (typeof ref !== 'function') { + return; + } + ref(refHandle as AnimatedTextInputRef); + }} + // eslint-disable-next-line + {...props} + /> + ); +} + +RNMaskedTextInputWithRef.displayName = 'RNMaskedTextInputWithRef'; + +export default React.forwardRef(RNMaskedTextInputWithRef); +export type {AnimatedTextInputRef}; diff --git a/src/components/Reactions/AddReactionBubble.tsx b/src/components/Reactions/AddReactionBubble.tsx index 8364a6658270..f755f2a6a4af 100644 --- a/src/components/Reactions/AddReactionBubble.tsx +++ b/src/components/Reactions/AddReactionBubble.tsx @@ -11,9 +11,9 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import variables from '@styles/variables'; -import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import {emojiPickerRef, resetEmojiPopoverAnchor, showEmojiPicker} from '@userActions/EmojiPickerAction'; import type {AnchorOrigin} from '@userActions/EmojiPickerAction'; -import * as Session from '@userActions/Session'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import type {CloseContextMenuCallback, OpenPickerCallback, PickerRefElement} from './QuickEmojiReactions/types'; @@ -54,11 +54,11 @@ function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWi const ref = useRef(null); const {translate} = useLocalize(); - useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); + useEffect(() => resetEmojiPopoverAnchor, []); const onPress = () => { const openPicker = (refParam?: PickerRefElement, anchorOrigin?: AnchorOrigin) => { - EmojiPickerAction.showEmojiPicker( + showEmojiPicker( () => { setIsEmojiPickerActive?.(false); }, @@ -72,7 +72,7 @@ function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWi ); }; - if (!EmojiPickerAction.emojiPickerRef.current?.isEmojiPickerVisible) { + if (!emojiPickerRef.current?.isEmojiPickerVisible) { setIsEmojiPickerActive?.(true); if (onPressOpenPicker) { onPressOpenPicker(openPicker); @@ -81,7 +81,7 @@ function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWi } } else { setIsEmojiPickerActive?.(false); - EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); + emojiPickerRef.current.hideEmojiPicker(); } }; @@ -90,7 +90,7 @@ function AddReactionBubble({onSelectEmoji, reportAction, onPressOpenPicker, onWi [styles.emojiReactionBubble, styles.userSelectNone, StyleUtils.getEmojiReactionBubbleStyle(hovered || pressed, false, isContextMenu)]} - onPress={Session.checkIfActionIsAllowed(onPress)} + onPress={callFunctionIfActionIsAllowed(onPress)} onMouseDown={(event) => { // Allow text input blur when Add reaction is right clicked if (!event || event.button === 2) { diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx index 815736d8af76..7c8b02412df9 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.tsx +++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx @@ -1,6 +1,6 @@ import React, {useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem'; import Icon from '@components/Icon'; @@ -9,14 +9,14 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as EmojiUtils from '@libs/EmojiUtils'; +import {getLocalizedEmojiName, getPreferredEmojiCode} from '@libs/EmojiUtils'; import getButtonState from '@libs/getButtonState'; import variables from '@styles/variables'; -import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; -import * as Session from '@userActions/Session'; +import {emojiPickerRef, showEmojiPicker} from '@userActions/EmojiPickerAction'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BaseQuickEmojiReactionsOnyxProps, BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types'; +import type {BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types'; type MiniQuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & { /** @@ -32,23 +32,18 @@ type MiniQuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & { * context menu which we just show on web, when hovering * a message. */ -function MiniQuickEmojiReactions({ - reportAction, - onEmojiSelected, - preferredLocale = CONST.LOCALES.DEFAULT, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - emojiReactions = {}, - onPressOpenPicker = () => {}, - onEmojiPickerClosed = () => {}, -}: MiniQuickEmojiReactionsProps) { +function MiniQuickEmojiReactions({reportAction, reportActionID, onEmojiSelected, onPressOpenPicker = () => {}, onEmojiPickerClosed = () => {}}: MiniQuickEmojiReactionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const ref = useRef(null); const {translate} = useLocalize(); + const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {initialValue: CONST.LOCALES.DEFAULT}); + const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE}); + const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, {initialValue: {}}); const openEmojiPicker = () => { onPressOpenPicker(); - EmojiPickerAction.showEmojiPicker( + showEmojiPicker( onEmojiPickerClosed, (emojiCode, emojiObject) => { onEmojiSelected(emojiObject, emojiReactions); @@ -66,24 +61,24 @@ function MiniQuickEmojiReactions({ onEmojiSelected(emoji, emojiReactions))} + tooltipText={`:${getLocalizedEmojiName(emoji.name, preferredLocale)}:`} + onPress={callFunctionIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions))} > - {EmojiUtils.getPreferredEmojiCode(emoji, preferredSkinTone)} + {getPreferredEmojiCode(emoji, preferredSkinTone)} ))} { - if (!EmojiPickerAction.emojiPickerRef.current?.isEmojiPickerVisible) { + onPress={callFunctionIfActionIsAllowed(() => { + if (!emojiPickerRef.current?.isEmojiPickerVisible) { openEmojiPicker(); } else { - EmojiPickerAction.emojiPickerRef.current?.hideEmojiPicker(); + emojiPickerRef.current?.hideEmojiPicker(); } })} isDelayButtonStateComplete={false} @@ -104,14 +99,4 @@ function MiniQuickEmojiReactions({ MiniQuickEmojiReactions.displayName = 'MiniQuickEmojiReactions'; -export default withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - }, - emojiReactions: { - key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, -})(MiniQuickEmojiReactions); +export default MiniQuickEmojiReactions; diff --git a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx index 87968fa38261..246c9a5dd0d5 100644 --- a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx +++ b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx @@ -1,41 +1,42 @@ import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import AddReactionBubble from '@components/Reactions/AddReactionBubble'; import EmojiReactionBubble from '@components/Reactions/EmojiReactionBubble'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as EmojiUtils from '@libs/EmojiUtils'; -import * as Session from '@userActions/Session'; +import {getLocalizedEmojiName, getPreferredEmojiCode} from '@libs/EmojiUtils'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BaseQuickEmojiReactionsOnyxProps, BaseQuickEmojiReactionsProps} from './types'; +import type {BaseQuickEmojiReactionsProps} from './types'; function BaseQuickEmojiReactions({ reportAction, + reportActionID, onEmojiSelected, - preferredLocale = CONST.LOCALES.DEFAULT, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, - emojiReactions = {}, onPressOpenPicker = () => {}, onWillShowPicker = () => {}, setIsEmojiPickerActive, }: BaseQuickEmojiReactionsProps) { const styles = useThemeStyles(); + const [preferredLocale] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE, {initialValue: CONST.LOCALES.DEFAULT}); + const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE}); + const [emojiReactions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, {initialValue: {}}); return ( {CONST.QUICK_REACTIONS.map((emoji: Emoji) => ( onEmojiSelected(emoji, emojiReactions))} + onPress={callFunctionIfActionIsAllowed(() => onEmojiSelected(emoji, emojiReactions))} /> @@ -54,14 +55,4 @@ function BaseQuickEmojiReactions({ BaseQuickEmojiReactions.displayName = 'BaseQuickEmojiReactions'; -export default withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - }, - emojiReactions: { - key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, -})(BaseQuickEmojiReactions); +export default BaseQuickEmojiReactions; diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts index 0021f33ce2c0..447c13c70818 100644 --- a/src/components/Reactions/QuickEmojiReactions/types.ts +++ b/src/components/Reactions/QuickEmojiReactions/types.ts @@ -3,7 +3,7 @@ import type {TextInput, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import type {AnchorOrigin} from '@userActions/EmojiPickerAction'; -import type {Locale, ReportAction, ReportActionReactions} from '@src/types/onyx'; +import type {ReportAction, ReportActionReactions} from '@src/types/onyx'; type PickerRefElement = RefObject; @@ -37,18 +37,7 @@ type BaseReactionsProps = { setIsEmojiPickerActive?: (state: boolean) => void; }; -type BaseQuickEmojiReactionsOnyxProps = { - /** All the emoji reactions for the report action. */ - emojiReactions: OnyxEntry; - - /** The user's preferred locale. */ - preferredLocale: OnyxEntry; - - /** The user's preferred skin tone. */ - preferredSkinTone: OnyxEntry; -}; - -type BaseQuickEmojiReactionsProps = BaseReactionsProps & BaseQuickEmojiReactionsOnyxProps; +type BaseQuickEmojiReactionsProps = BaseReactionsProps; type QuickEmojiReactionsProps = BaseReactionsProps & { /** @@ -60,4 +49,4 @@ type QuickEmojiReactionsProps = BaseReactionsProps & { setIsEmojiPickerActive?: (state: boolean) => void; }; -export type {BaseQuickEmojiReactionsProps, BaseQuickEmojiReactionsOnyxProps, QuickEmojiReactionsProps, OpenPickerCallback, CloseContextMenuCallback, PickerRefElement}; +export type {BaseQuickEmojiReactionsProps, QuickEmojiReactionsProps, OpenPickerCallback, CloseContextMenuCallback, PickerRefElement}; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e20fe09058e1..c70bda657e67 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -112,7 +112,8 @@ function MoneyRequestPreviewContent({ const transactionID = isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const transactionViolations = getTransactionViolations(transaction?.transactionID); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; @@ -145,9 +146,9 @@ function MoneyRequestPreviewContent({ const isOnHold = isOnHoldTransactionUtils(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = hasViolationTransactionUtils(transaction?.transactionID, transactionViolations, true); - const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, transactionViolations, true) && isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, transactionViolations, true); + const hasViolations = hasViolationTransactionUtils(transaction?.transactionID, allViolations, true); + const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true); const hasFieldErrors = hasMissingSmartscanFields(transaction); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction); @@ -163,11 +164,8 @@ function MoneyRequestPreviewContent({ // Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates const allDuplicates = useMemo( - () => - transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]?.find( - (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - )?.data?.duplicates ?? [], - [transaction?.transactionID, transactionViolations], + () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], + [transactionViolations], ); // Remove settled transactions from duplicates @@ -240,14 +238,13 @@ function MoneyRequestPreviewContent({ } if (shouldShowRBR && transaction) { - const violations = getTransactionViolations(transaction.transactionID, transactionViolations); if (shouldShowHoldMessage) { return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; } - const firstViolation = violations?.at(0); + const firstViolation = transactionViolations?.at(0); if (firstViolation) { const violationMessage = ViolationsUtils.getViolationTranslation(firstViolation, translate); - const violationsCount = violations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; + const violationsCount = transactionViolations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; const isTooLong = violationsCount > 1 || violationMessage.length > 15; const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; @@ -287,7 +284,7 @@ function MoneyRequestPreviewContent({ if (shouldShowBrokenConnectionViolation(transaction ? [transaction.transactionID] : [], iouReport, policy)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; } - if (hasPendingUI(transaction, getTransactionViolations(transaction?.transactionID, transactionViolations))) { + if (hasPendingUI(transaction, transactionViolations)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 10c72dbd8841..54f3a8b6907f 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -49,6 +49,7 @@ import { getDistanceInMeters, getTagForDisplay, getTaxName, + getTransactionViolations, hasMissingSmartscanFields, hasReceipt as hasReceiptTransactionUtils, hasReservationList, @@ -133,7 +134,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const transactionViolations = getTransactionViolations(linkedTransactionID); const { created: transactionDate, @@ -698,7 +699,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals {shouldShowTax && ( ({...getThumbnailAndImageURIs(transaction), transaction})); const transactionIDList = transactions?.map((reportTransaction) => reportTransaction.transactionID) ?? []; - const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations)); + const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID)); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null; const formattedDescription = numberOfRequests === 1 ? getDescription(lastTransaction) : null; @@ -250,7 +250,7 @@ function ReportPreview({ const isArchived = isArchivedReportWithID(iouReport?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const filteredTransactions = transactions?.filter((transaction) => transaction) ?? []; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, transactionViolations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 3eca7561eb04..495b3dbd51fd 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -20,7 +20,7 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {checkIfActionIsAllowed} from '@libs/actions/Session'; +import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; import {canActionTask, completeTask, getTaskAssigneeAccountID, reopenTask} from '@libs/actions/Task'; import ControlSelection from '@libs/ControlSelection'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; @@ -108,7 +108,7 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che style={[styles.mr2]} isChecked={isTaskCompleted} disabled={!canActionTask(taskReport, currentUserPersonalDetails.accountID, taskOwnerAccountID, taskAssigneeAccountID)} - onPress={checkIfActionIsAllowed(() => { + onPress={callFunctionIfActionIsAllowed(() => { if (isTaskCompleted) { reopenTask(taskReport, taskReportID); } else { diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 3e077c2bda4a..efd6c1061720 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -21,7 +21,7 @@ import Navigation from '@libs/Navigation/Navigation'; import {getAvatarsForAccountIDs, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getDisplayNameForParticipant, getDisplayNamesWithTooltips, isCompletedTaskReport, isOpenTaskReport} from '@libs/ReportUtils'; import {isActiveTaskEditRoute} from '@libs/TaskUtils'; -import {checkIfActionIsAllowed} from '@userActions/Session'; +import {callFunctionIfActionIsAllowed} from '@userActions/Session'; import {canActionTask as canActionTaskUtil, canModifyTask as canModifyTaskUtil, clearTaskErrors, completeTask, reopenTask, setTaskReport} from '@userActions/Task'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -37,9 +37,11 @@ function TaskView({report}: TaskViewProps) { const StyleUtils = useStyleUtils(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); + useEffect(() => { setTaskReport(report); }, [report]); + const taskTitle = convertToLTR(report?.reportName ?? ''); const assigneeTooltipDetails = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(report?.managerID ? [report?.managerID] : [], personalDetails), false); const isOpen = isOpenTaskReport(report); @@ -61,7 +63,7 @@ function TaskView({report}: TaskViewProps) { {(hovered) => ( { + onPress={callFunctionIfActionIsAllowed((e) => { if (isDisableInteractive) { return; } @@ -85,7 +87,7 @@ function TaskView({report}: TaskViewProps) { {translate('task.title')} { + onPress={callFunctionIfActionIsAllowed(() => { // If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page. if (isActiveTaskEditRoute(report?.reportID)) { return; diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 72d71b39b9d7..3e4253d848c4 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -1,3 +1,4 @@ +import {Str} from 'expensify-common'; import React, {useMemo} from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -104,7 +105,7 @@ function ReservationView({reservation, transactionID, tripRoomReportID, reservat numberOfLines={1} style={[styles.textStrong, styles.lh20]} > - {reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName} + {reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : Str.recapitalize(reservation.start.longName ?? '')} {!!bottomDescription && {bottomDescription}} diff --git a/src/components/ReportActionItem/TripRoomPreview.tsx b/src/components/ReportActionItem/TripRoomPreview.tsx index d85c19d21ee0..de8a559c602a 100644 --- a/src/components/ReportActionItem/TripRoomPreview.tsx +++ b/src/components/ReportActionItem/TripRoomPreview.tsx @@ -1,3 +1,4 @@ +import {Str} from 'expensify-common'; import React, {useMemo} from 'react'; import type {ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native'; import {FlatList, View} from 'react-native'; @@ -53,16 +54,17 @@ type TripRoomPreviewProps = { type ReservationViewProps = { reservation: Reservation; + onPress?: () => void; }; -function ReservationView({reservation}: ReservationViewProps) { +function ReservationView({reservation, onPress}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const reservationIcon = getTripReservationIcon(reservation.type); - const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName; + const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : Str.recapitalize(reservation.start.longName ?? ''); let titleComponent = ( ) => ; - function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -136,6 +137,14 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch ); }, [currency, totalDisplaySpend, tripTransactions]); + const navigateToTrip = () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID)); + const renderItem = ({item}: ListRenderItemInfo) => ( + + ); + return ( canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)} shouldUseHapticsOnLongPress - style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox, styles.cursorDefault]} + style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('iou.viewDetails')} > @@ -184,7 +194,7 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch