diff --git a/.env.example b/.env.example index f398a72aa0af..c4adc4f98b65 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ USE_WEB_PROXY=false USE_WDYR=false CAPTURE_METRICS=false ONYX_METRICS=false +USE_THIRD_PARTY_SCRIPTS=false EXPENSIFY_ACCOUNT_ID_ACCOUNTING=-1 EXPENSIFY_ACCOUNT_ID_ADMIN=-1 diff --git a/.env.production b/.env.production index bb925eb70d39..cca9adf26f52 100644 --- a/.env.production +++ b/.env.production @@ -8,6 +8,6 @@ USE_WEB_PROXY=false ENVIRONMENT=production SEND_CRASH_REPORTS=true -FB_API_KEY=AIzaSyDxzigVLZl4G8MP7jACQ0qpmADMzmrrON0 -FB_APP_ID=1:921154746561:web:1583e882584cf151027c40 -FB_PROJECT_ID=expensify-chat +FB_API_KEY=AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow +FB_APP_ID=1:1008697809946:web:08de4ecb7656b7235445a3 +FB_PROJECT_ID=expensify-mobile-app diff --git a/.eslintrc.js b/.eslintrc.js index cfbfdcc8fe91..fefad92ce29d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -294,6 +294,7 @@ module.exports = { files: ['*.ts', '*.tsx'], rules: { 'rulesdir/prefer-at': 'error', + 'rulesdir/boolean-conditional-rendering': 'error', }, }, ], diff --git a/.github/ISSUE_TEMPLATE/Internal.md b/.github/ISSUE_TEMPLATE/Internal.md new file mode 100644 index 000000000000..c4fde407df13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Internal.md @@ -0,0 +1,25 @@ +--- +name: Open an internal issue for a backend fix +about: Use this template to report a backend issue that an internal Expensify employee needs to fix +labels: Hot Pick, Daily, Internal, AutoAssignerNewDotQuality +--- + + +**Original GH:** + +## Action Performed: +Break down in numbered steps + +## Expected Result: +Describe what you think the backend _SHOULD_ have done + +## Actual Result: +Describe what the backend _ACTUALLY_ did + +## Screenshots/Videos + +
+ Add any screenshot/video evidence + + +
diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md index fa50d48b341b..7ae439777e78 100644 --- a/.github/ISSUE_TEMPLATE/Standard.md +++ b/.github/ISSUE_TEMPLATE/Standard.md @@ -16,7 +16,7 @@ ___ **Logs:** https://stackoverflow.com/c/expensify/questions/4856 **Expensify/Expensify Issue URL:** **Issue reported by:** -**Slack conversation:** +**Slack conversation** (hyperlinked to channel name): ## Action Performed: Break down in numbered steps diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 36b921570e7f..459a780ca8b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ -### Details - +### Explanation of Change + ### Fixed Issues +// TODO: These must be filled out, or the issue title must include "[No QA]." - [ ] Verify that no errors appear in the JS console @@ -79,7 +82,6 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed) - [ ] I followed proper code patterns (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`) - - [ ] I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. `myBool && `. - [ ] I verified that comments were added to code that is not self explanatory - [ ] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing. - [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60) diff --git a/.github/actions/composite/setupGitForOSBotify/action.yml b/.github/actions/composite/setupGitForOSBotify/action.yml index c61fa7e934fd..456cef93676a 100644 --- a/.github/actions/composite/setupGitForOSBotify/action.yml +++ b/.github/actions/composite/setupGitForOSBotify/action.yml @@ -20,10 +20,10 @@ runs: - name: Set up git for OSBotify shell: bash run: | - git config user.signingkey AEE1036472A782AB - git config commit.gpgsign true - git config user.name OSBotify - git config user.email infra+osbotify@expensify.com + git config --global user.signingkey AEE1036472A782AB + git config --global commit.gpgsign true + git config --global user.name OSBotify + git config --global user.email infra+osbotify@expensify.com - name: Enable debug logs for git shell: bash diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml index 40dfc05e5448..aadf433fba2f 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml +++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml @@ -17,12 +17,18 @@ inputs: ANDROID: description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + ANDROID_HYBRID: + description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true DESKTOP: description: "Desktop job result ('success', 'failure', 'cancelled', or 'skipped')" required: true IOS: description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + IOS_HYBRID: + description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true WEB: description: "Web job result ('success', 'failure', 'cancelled', or 'skipped')" required: true diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 62d326c9af3a..c67f38ca1f3e 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -12710,8 +12710,10 @@ async function run() { const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); const version = core.getInput('DEPLOY_VERSION', { required: true }); const androidResult = getDeployTableMessage(core.getInput('ANDROID', { required: true })); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', { required: true })); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true })); const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true })); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', { required: true })); const webResult = getDeployTableMessage(core.getInput('WEB', { required: true })); const date = core.getInput('DATE'); const note = core.getInput('NOTE'); @@ -12724,6 +12726,7 @@ async function run() { message += `🚀`; message += `\n\nplatform | result\n---|---\n🤖 android 🤖|${androidResult}\n🖥 desktop 🖥|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\n🕸 web 🕸|${webResult}`; + message += `\n🤖🔄 android HybridApp 🤖🔄|${androidHybridResult}\n🍎🔄 iOS HybridApp 🍎🔄|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len message += diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 9c2defebd01d..ba61c31a6bb2 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -51,8 +51,10 @@ async function run() { const version = core.getInput('DEPLOY_VERSION', {required: true}); const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true}) as PlatformResult); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', {required: true}) as PlatformResult); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true}) as PlatformResult); const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', {required: true}) as PlatformResult); const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult); const date = core.getInput('DATE'); @@ -67,6 +69,7 @@ async function run() { message += `🚀`; message += `\n\nplatform | result\n---|---\n🤖 android 🤖|${androidResult}\n🖥 desktop 🖥|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\n🕸 web 🕸|${webResult}`; + message += `\n🤖🔄 android HybridApp 🤖🔄|${androidHybridResult}\n🍎🔄 iOS HybridApp 🍎🔄|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len diff --git a/.github/scripts/enforceRedirect.sh b/.github/scripts/enforceRedirect.sh index 4d7d169b01c5..b7dc7abc80ed 100755 --- a/.github/scripts/enforceRedirect.sh +++ b/.github/scripts/enforceRedirect.sh @@ -13,13 +13,13 @@ declare -r REDIRECTS_FILE="docs/redirects.csv" hasRenamedOrDeletedArticle=false hasModifiedRedirect=false -if git log origin/main..HEAD --name-status --pretty=format: $ARTICLES_DIRECTORY | grep -q -E "^(R|D)" +if git diff origin/main..HEAD --name-status --pretty=format: $ARTICLES_DIRECTORY | grep -q -E "^(R|D)" then echo "Articles have been renamed/moved/deleted" hasRenamedOrDeletedArticle=true fi -if git log origin/main..HEAD --name-status --pretty=format: $REDIRECTS_FILE | grep -q -E "^(M)" +if git diff origin/main..HEAD --name-status --pretty=format: $REDIRECTS_FILE | grep -q -E "^(M)" then echo "Redirects.csv has been modified" hasModifiedRedirect=true diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index c88542068b92..49f3d23ec02c 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -163,7 +163,7 @@ jobs: echo "APK_PATH=$apkPath" echo "APK_FILE_NAME=$(basename "$apkPath")" echo "SHOULD_UPLOAD_SOURCEMAPS=$SHOULD_UPLOAD_SOURCEMAPS" - echo "APK_ARTIFACT_NAME=${{ inputs.artifact-prefix }}android-artifact-apk" >> "$GITHUB_OUTPUT" + echo "APK_ARTIFACT_NAME=${{ inputs.artifact-prefix }}android-apk-artifact" >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT" - name: Upload Android AAB artifact @@ -171,7 +171,7 @@ jobs: continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-prefix }}android-artifact-aab + name: ${{ inputs.artifact-prefix }}android-aab-artifact path: ${{ steps.build.outputs.AAB_PATH }} - name: Upload Android APK artifact @@ -187,5 +187,5 @@ jobs: continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-prefix }}android-artifact-sourcemaps + name: ${{ inputs.artifact-prefix }}android-sourcemaps-artifact path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index ca9a128e848d..93fe07be9298 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -101,3 +101,92 @@ jobs: 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) }} + defaults: + run: + working-directory: Mobile-Expensify + 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 + # 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: Check out `Mobile-Expensify` repo + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} + + - name: Update submodule + run: | + cd react-native + git submodule update --init + + - name: Setup git for OSBotify + uses: ./.github/actions/composite/setupGitForOSBotify + id: setupGitForOSBotify + with: + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Generate HybridApp version + run: | + # 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}') + FULL_APP_VERSION="$SHORT_APP_VERSION.$BUILD_NUMBER" + ANDROID_VERSION_CODE=$(echo "$FULL_APP_VERSION" | ruby -e "puts '05%02d%02d%02d%02d' % STDIN.read.split('.')") + + # File paths to update + ANDROID_MANIFEST_FILE="Android/AndroidManifest.xml" + IOS_INFO_PLIST_FILE="iOS/Expensify/Expensify-Info.plist" + IOS_SHARE_EXTENSION_PLIST_FILE="iOS/SmartScanExtension/Info.plist" + JS_CONFIG_FILE="app/config/config.json" + + # Update Android HybridApp Version + sed -i .bak -E "s/versionName=\"([0-9\.]*)\"/versionName=\"$FULL_APP_VERSION\"/" $ANDROID_MANIFEST_FILE + sed -i .bak -E "s/versionCode=\"([0-9]*)\"/versionCode=\"$ANDROID_VERSION_CODE\"/" $ANDROID_MANIFEST_FILE + + # Update iOS HybridApp Version + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_INFO_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_INFO_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE + + # 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 }} + + - name: Commit new version + run: | + git add \ + ./Android/AndroidManifest.xml \ + ./app/config/config.json \ + ./iOS/Expensify/Expensify-Info.plist\ + ./iOS/SmartScanExtension/Info.plist + git commit -m "Update version to ${{ needs.createNewVersion.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 }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4ff1a2004d8f..d578621930a7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -96,7 +96,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/artifacts - pattern: android-artifact-* + pattern: android-*-artifact merge-multiple: true - name: Log downloaded artifact paths @@ -163,6 +163,148 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + android_hybrid: + name: Build and deploy Android HybridApp + needs: prep + runs-on: ubuntu-latest-xl + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + defaults: + run: + working-directory: Mobile-Expensify/react-native + env: + RUBYOPT: '-rostruct' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch + fetch-depth: 0 + + - name: Update submodule + run: | + git submodule update --init + # Update submodule to latest on staging + git fetch + git checkout staging + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - uses: actions/setup-node@v4 + with: + node-version-file: 'Mobile-Expensify/react-native/.nvmrc' + cache: npm + cache-dependency-path: 'Mobile-Expensify/react-native' + + - name: Install node modules + run: | + npm install + cd .. && npm install + + # Fixes https://github.com/Expensify/App/issues/51682 + npm run grunt:build:shared + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: '17' + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + working-directory: 'Mobile-Expensify/react-native' + + - name: Install New Expensify Gems + run: bundle install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./upload-key.keystore upload-key.keystore + op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json + # Copy the keystore to the Android directory for Fullstory + cp ./upload-key.keystore ../Android + + - name: Load Android upload keystore credentials from 1Password + id: load-credentials + uses: 1password/load-secrets-action@v2 + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD + ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS + ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD + + - name: Get Android native version + id: getAndroidVersion + run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" + + - name: Build Android app + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane android build_hybrid + env: + ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + + - name: Upload Android app to Google Play + run: bundle exec fastlane android upload_google_play_internal_hybrid + env: + VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + + - name: Upload Android build to Browser Stack + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.aabPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload Android build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-hybrid-build-artifact + path: ${{ env.aabPath }} + + - name: Upload Android sourcemap artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-hybrid-sourcemap-artifact + path: /home/runner/work/App/App/Mobile-Expensify/Android/build/generated/sourcemaps/react/release/index.android.bundle.map + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Warn deployers if Android production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 Android HybridApp production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + desktop: name: Build and deploy Desktop needs: prep @@ -329,6 +471,166 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + iOS_hybrid: + name: Build and deploy iOS HybridApp + needs: prep + runs-on: macos-13-xlarge + env: + DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + defaults: + run: + working-directory: Mobile-Expensify/react-native + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch + fetch-depth: 0 + + - name: Update submodule + run: | + git submodule update --init + # Update submodule to latest on staging + git fetch + git checkout staging + + - name: Configure MapBox SDK + run: | + ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - uses: actions/setup-node@v4 + id: setup-node + with: + node-version-file: 'Mobile-Expensify/react-native/.nvmrc' + cache-dependency-path: 'Mobile-Expensify/react-native' + + - name: Install node modules + run: | + npm install + cd .. && npm install + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + working-directory: 'Mobile-Expensify/react-native' + + - name: Install New Expensify Gems + run: bundle install + + - name: Cache Pod dependencies + uses: actions/cache@v4 + id: pods-cache + with: + path: ios/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} + + - name: Compare Podfile.lock and Manifest.lock + id: compare-podfile-and-manifest + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Install cocoapods + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' + with: + timeout_minutes: 10 + max_attempts: 5 + command: cd Mobile-Expensify/iOS && pod install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./OldApp_AppStore.mobileprovision OldApp_AppStore + op document get --output ./OldApp_AppStore_Share_Extension.mobileprovision OldApp_AppStore_Share_Extension + + - name: Decrypt AppStore profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt AppStore Notification Service profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt certificate + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt App Store Connect API key + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Get iOS native version + id: getIOSVersion + run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" + + - name: Build iOS HybridApp + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios build_hybrid + + - name: Upload release build to TestFlight + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios upload_testflight_hybrid + env: + APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} + APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }} + APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} + APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} + + - name: Upload iOS build to Browser Stack + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.ipaPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload iOS build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: ios-hybrid-build-artifact + path: ${{ env.ipaPath }} + + - name: Upload iOS sourcemap artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: ios-hybrid-sourcemap-artifact + path: /Users/runner/work/App/App/Mobile-Expensify/main.jsbundle.map + + - name: Warn deployers if iOS production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 iOS HybridApp production deploy failed. Please manually submit ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the . 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + web: name: Build and deploy Web needs: prep @@ -415,7 +717,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -425,23 +727,12 @@ jobs: with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - # Build a version of iOS and Android HybridApp if we are deploying to staging - hybridApp: - runs-on: ubuntu-latest - needs: prep - if: ${{ github.ref == 'refs/heads/staging' }} - steps: - - name: 'Deploy HybridApp' - run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true -f build_version="${{ needs.prep.outputs.APP_VERSION }}" - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - checkDeploymentSuccess: runs-on: ubuntu-latest outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform @@ -457,8 +748,10 @@ jobs: isAtLeastOnePlatformDeployed="true" fi fi - + if [ "${{ needs.iOS.result }}" == "success" ] || \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] || \ + [ "${{ needs.android_hybrid.result }}" == "success" ] || \ [ "${{ needs.desktop.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" @@ -471,6 +764,8 @@ jobs: run: | isAllPlatformsDeployed="false" if [ "${{ needs.iOS.result }}" == "success" ] && \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] && \ + [ "${{ needs.android_hybrid.result }}" == "success" ] && \ [ "${{ needs.desktop.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" @@ -518,16 +813,31 @@ jobs: - name: Upload artifacts to GitHub Release continue-on-error: true run: | - gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \ - ./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./android-build-artifact/app-production-release.aab \ - ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./desktop-staging-build-artifact/NewExpensify.dmg#NewExpensifyStaging.dmg \ - ./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./ios-build-artifact/New\ Expensify.ipa \ - ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./web-staging-build-tar-gz-artifact/webBuild.tar.gz#stagingWebBuild.tar.gz \ - ./web-staging-build-zip-artifact/webBuild.zip#stagingWebBuild.zip + # Release asset name should follow the template: [platform]-[hybrid, staging, production or blank]-[sourcemap or blank].[file extension] + files=( + "./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap.js.map" + "./android-aab-artifact/app-production-release.aab#android.aab" + "./android-hybrid-build-artifact/Expensify-release.aab#android-hybrid.aab" + "./android-hybrid-sourcemap-artifact/index.android.bundle.map#android-hybrid-sourcemap.js.map" + "./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap.js.map" + "./desktop-staging-build-artifact/NewExpensify.dmg#desktop-staging.dmg" + "./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap.js.map" + "./ios-build-artifact/New Expensify.ipa#ios.ipa" + "./ios-hybrid-build-artifact/Expensify.ipa#ios-hybrid.ipa" + "./ios-hybrid-sourcemap-artifact/main.jsbundle.map#ios-hybrid-sourcemap.js.map" + "./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap.js.map" + "./web-staging-build-tar-gz-artifact/webBuild.tar.gz#web-staging.tar.gz" + "./web-staging-build-zip-artifact/webBuild.zip#web-staging.zip" + ) + + # Loop through each file and upload individually (so if one fails, we still have other platforms uploaded) + for file_entry in "${files[@]}"; do + gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" || { + echo "Failed to upload $file_entry. Continuing with the next file." + continue + } + echo "Successfully uploaded $file_entry." + done env: GITHUB_TOKEN: ${{ github.token }} @@ -574,12 +884,23 @@ jobs: - name: Upload artifacts to GitHub Release continue-on-error: true run: | - gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \ - ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./desktop-build-artifact/NewExpensify.dmg \ - ./web-sourcemaps-artifact/web-merged-source-map.js.map#web-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./web-build-tar-gz-artifact/webBuild.tar.gz \ - ./web-build-zip-artifact/webBuild.zip + # Release asset name should follow the template: [platform]-[hybrid, staging, production or blank]-[sourcemap or blank].[file extension] + files=( + "./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-production-sourcemap.js.map" + "./desktop-build-artifact/NewExpensify.dmg#desktop-production.dmg" + "./web-sourcemaps-artifact/web-merged-source-map.js.map#web-production-sourcemap.js.map" + "./web-build-tar-gz-artifact/webBuild.tar.gz#web-production.tar.gz" + "./web-build-zip-artifact/webBuild.zip#web-production.zip" + ) + + # Loop through each file and upload individually (so if one fails, we still have other platforms uploaded) + for file_entry in "${files[@]}"; do + gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber "$file_entry" || { + echo "Failed to upload $file_entry. Continuing with the next file." + continue + } + echo "Successfully uploaded $file_entry." + done env: GITHUB_TOKEN: ${{ github.token }} @@ -605,7 +926,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -659,11 +980,13 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} + android_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} + ios_hybrid: ${{ needs.iOS_hybrid.result }} web: ${{ needs.web.result }} desktop: ${{ needs.desktop.result }} diff --git a/.github/workflows/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml index 2d2f551482d2..8e455979a50e 100644 --- a/.github/workflows/deployNewHelp.yml +++ b/.github/workflows/deployNewHelp.yml @@ -53,7 +53,7 @@ jobs: # Install Node for _scripts/*.js - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20.18.0' diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index c92ab83d1178..c7b8a1bb7e01 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -43,6 +43,7 @@ jobs: previous_merge=$(git rev-list --merges HEAD~1 | head -n 1) else # On a feature branch, find the common ancestor of the current branch and main + git fetch origin main:main previous_merge=$(git merge-base HEAD main) fi echo "$previous_merge" @@ -60,7 +61,7 @@ jobs: id: getDeltaRef run: | if [ '${{ steps.getPullRequestDetails.outputs.IS_MERGED }}' == 'true' ]; then - echo "DELTA_REF=${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" + echo "DELTA_REF=${{ steps.getPullRequestDetails.outputs.MERGE_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" else # Set dummy git credentials git config --global user.email "test@test.com" @@ -157,26 +158,28 @@ jobs: aws-region: us-west-2 - name: Schedule AWS Device Farm test run on main branch - uses: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b + uses: Wandalen/wretry.action@v3.5.0 id: schedule-awsdf-main with: - name: App E2E Performance Regression Tests - project_arn: ${{ secrets.AWS_PROJECT_ARN }} - device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease.apk - app_type: ANDROID_APP - test_type: APPIUM_NODE - test_package_file: App.zip - test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpec.yml - test_spec_type: APPIUM_NODE_TEST_SPEC - remote_src: false - file_artifacts: | - Customer Artifacts.zip - Test spec output.txt - log_artifacts: debug.log - cleanup: true - timeout: 7200 + action: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b + with: | + name: App E2E Performance Regression Tests + project_arn: ${{ secrets.AWS_PROJECT_ARN }} + device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} + app_file: zip/app-e2eRelease.apk + app_type: ANDROID_APP + test_type: APPIUM_NODE + test_package_file: App.zip + test_package_type: APPIUM_NODE_TEST_PACKAGE + test_spec_file: tests/e2e/TestSpec.yml + test_spec_type: APPIUM_NODE_TEST_SPEC + remote_src: false + file_artifacts: | + Customer Artifacts.zip + Test spec output.txt + log_artifacts: debug.log + cleanup: true + timeout: 7200 - name: Print logs if run failed if: failure() @@ -229,6 +232,36 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} + - name: Check if test has skipped tests + id: checkIfSkippedTestsDetected + run: | + if grep -q '⚠️' "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md"; then + # Create an output to the GH action that the tests were skipped: + echo "skippedTestsDetected=true" >> "$GITHUB_OUTPUT" + else + echo "skippedTestsDetected=false" >> "$GITHUB_OUTPUT" + echo '✅ no skipped tests detected' + fi + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: 'Announce skipped tests in Slack' + if: ${{ steps.checkIfSkippedTestsDetected.outputs.skippedTestsDetected == 'true' }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#e2e-announce', + attachments: [{ + color: 'danger', + text: `⚠️ ${process.env.AS_REPO} Some of E2E tests were skipped on workflow ⚠️`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + - name: 'Announce regression in Slack' if: ${{ steps.checkIfRegressionDetected.outputs.performanceRegressionDetected == 'true' }} uses: 8398a7/action-slack@v3 @@ -236,7 +269,7 @@ jobs: status: custom custom_payload: | { - channel: '#newdot-quality', + channel: '#quality', attachments: [{ color: 'danger', text: `🔴 Performance regression detected in PR ${{ inputs.PR_NUMBER }}\nDetected in workflow.`, diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml index 3893d3cf3f7c..ca138be0888b 100644 --- a/.github/workflows/postDeployComments.yml +++ b/.github/workflows/postDeployComments.yml @@ -15,10 +15,18 @@ on: description: Android deploy status required: true type: string + android_hybrid: + description: Android HybridApp deploy status + required: true + type: string ios: description: iOS deploy status required: true type: string + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: string web: description: Web deploy status required: true @@ -49,6 +57,15 @@ on: - failure - cancelled - skipped + android_hybrid: + description: Android HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped ios: description: iOS deploy status required: true @@ -58,6 +75,15 @@ on: - failure - cancelled - skipped + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped web: description: Web deploy status required: true @@ -110,9 +136,11 @@ jobs: IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }} DEPLOY_VERSION: ${{ inputs.version }} GITHUB_TOKEN: ${{ github.token }} + ANDROID_HYBRID: ${{ inputs.android_hybrid }} ANDROID: ${{ inputs.android }} DESKTOP: ${{ inputs.desktop }} IOS: ${{ inputs.ios }} + IOS_HYBRID: ${{ inputs.ios_hybrid }} WEB: ${{ inputs.web }} DATE: ${{ inputs.date }} NOTE: ${{ inputs.note }} diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 672d468ed3b1..1bba3e96735a 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -92,7 +92,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/artifacts - pattern: android-artifact-* + pattern: android-*-artifact merge-multiple: true - name: Log downloaded artifact paths diff --git a/.well-known/assetlinks.json b/.well-known/assetlinks.json index a28004b45b08..803946f438d6 100644 --- a/.well-known/assetlinks.json +++ b/.well-known/assetlinks.json @@ -5,4 +5,12 @@ "package_name": "com.expensify.chat", "sha256_cert_fingerprints": ["2E:65:6F:1C:34:F5:7E:BF:FC:C0:2D:A3:14:0E:83:FE:61:51:F2:9B:5D:59:58:61:C4:4D:A9:99:0C:CA:F4:8E"] } -}] \ No newline at end of file + }, + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "org.me.mobiexpensifyg", + "sha256_cert_fingerprints": ["87:03:DC:2B:20:99:CB:F7:AF:39:0C:8F:F2:E4:78:F2:61:E9:D1:7E:F4:AF:E5:02:D9:72:F2:4D:1F:29:FF:65"] + } +}] diff --git a/Gemfile.lock b/Gemfile.lock index 00232570d5de..71f4fd64bc0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,20 +20,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.948.0) - aws-sdk-core (3.199.0) + aws-partitions (1.1001.0) + aws-sdk-core (3.211.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.87.0) - aws-sdk-core (~> 3, >= 3.199.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.154.0) - aws-sdk-core (~> 3, >= 3.199.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -89,8 +89,8 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -116,10 +116,10 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -135,6 +135,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -164,6 +165,8 @@ GEM apktools (~> 0.7) aws-sdk-s3 (~> 1) mime-types (~> 3.3) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) ffi (1.17.0) ffi (1.17.0-arm64-darwin) ffi (1.17.0-x86_64-darwin) @@ -187,7 +190,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -208,14 +211,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.2) + json (2.7.6) + jwt (2.9.3) base64 mime-types (3.5.1) mime-types-data (~> 3.2015) @@ -226,7 +229,7 @@ GEM molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) @@ -241,8 +244,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.9) - strscan + rexml (3.3.9) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -256,7 +258,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -270,15 +272,15 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/README.md b/README.md index 730e745e368a..6b75fbed1b2c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ * [Expensify Code of Conduct](CODE_OF_CONDUCT.md) * [Contributor License Agreement](contributingGuides/CLA.md) * [React StrictMode](contributingGuides/STRICT_MODE.md) +* [Left Hand Navigation(LHN)](contributingGuides/LEFT_HAND_NAVIGATION.md) ---- diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 5bcafdc1856c..55d19124e65e 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -16,9 +16,10 @@ const {triggerTransitionEnd, addListener} = isJestEnv addListener: () => {}, }; +const realOrMockedUseNavigation = isJestEnv ? realReactNavigation.useNavigation : {}; const useNavigation = () => ({ - ...realReactNavigation.useNavigation, - navigate: jest.fn(), + ...realOrMockedUseNavigation, + navigate: isJestEnv ? jest.fn() : () => {}, getState: () => ({ routes: [], }), @@ -30,17 +31,20 @@ type NativeNavigationMock = typeof ReactNavigation & { }; export * from '@react-navigation/core'; -const Link = realReactNavigation.Link; -const LinkingContext = realReactNavigation.LinkingContext; -const NavigationContainer = realReactNavigation.NavigationContainer; -const ServerContainer = realReactNavigation.ServerContainer; -const DarkTheme = realReactNavigation.DarkTheme; -const DefaultTheme = realReactNavigation.DefaultTheme; -const ThemeProvider = realReactNavigation.ThemeProvider; -const useLinkBuilder = realReactNavigation.useLinkBuilder; -const useLinkProps = realReactNavigation.useLinkProps; -const useLinkTo = realReactNavigation.useLinkTo; -const useScrollToTop = realReactNavigation.useScrollToTop; +const Link = isJestEnv ? realReactNavigation.Link : () => null; +const LinkingContext = isJestEnv ? realReactNavigation.LinkingContext : () => null; +const NavigationContainer = isJestEnv ? realReactNavigation.NavigationContainer : () => null; +const ServerContainer = isJestEnv ? realReactNavigation.ServerContainer : () => null; +const DarkTheme = isJestEnv ? realReactNavigation.DarkTheme : {}; +const DefaultTheme = isJestEnv ? realReactNavigation.DefaultTheme : {}; +const ThemeProvider = isJestEnv ? realReactNavigation.ThemeProvider : () => null; +const useLinkBuilder = isJestEnv ? realReactNavigation.useLinkBuilder : () => null; +const useLinkProps = isJestEnv ? realReactNavigation.useLinkProps : () => null; +const useLinkTo = isJestEnv ? realReactNavigation.useLinkTo : () => null; +const useScrollToTop = isJestEnv ? realReactNavigation.useScrollToTop : () => null; +const useRoute = isJestEnv ? realReactNavigation.useRoute : () => ({params: {}}); +const useFocusEffect = isJestEnv ? realReactNavigation.useFocusEffect : (callback: () => void) => callback(); + export { // Overriden modules useIsFocused, @@ -60,6 +64,8 @@ export { useLinkProps, useLinkTo, useScrollToTop, + useRoute, + useFocusEffect, }; export type {NativeNavigationMock}; diff --git a/__mocks__/react-native-dev-menu.ts b/__mocks__/react-native-dev-menu.ts deleted file mode 100644 index 0d35d5c32723..000000000000 --- a/__mocks__/react-native-dev-menu.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {addItem} from 'react-native-dev-menu'; - -type ReactNativeDevMenuMock = { - addItem: typeof addItem; -}; - -const reactNativeDevMenuMock: ReactNativeDevMenuMock = { - addItem: jest.fn(), -}; - -export default reactNativeDevMenuMock; diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ce4d0e5fddf..64e6ec12309b 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 1009005401 - versionName "9.0.54-1" + versionCode 1009005900 + versionName "9.0.59-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/android/app/google-services-DEV.json b/android/app/google-services-DEV.json new file mode 100644 index 000000000000..3c14cccd9c09 --- /dev/null +++ b/android/app/google-services-DEV.json @@ -0,0 +1,127 @@ +{ + "project_info": { + "project_number": "921154746561", + "firebase_url": "https://expensify-chat.firebaseio.com", + "project_id": "expensify-chat", + "storage_bucket": "expensify-chat.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:4f04268f25f84eaf027c40", + "android_client_info": { + "package_name": "com.expensify.chat" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:333e293a7fef83a8027c40", + "android_client_info": { + "package_name": "com.expensify.chat.adhoc" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:921154746561:android:3b19fdbaedb5b586027c40", + "android_client_info": { + "package_name": "com.expensify.chat.dev" + } + }, + "oauth_client": [ + { + "client_id": "921154746561-svjnccrcn6vet45kn9o7sibb3jemipa6.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.expensify.chat.dev", + "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625" + } + }, + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCVwQb9lBI06bDIwHOw10AkdJyquXoMngk" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "921154746561-080fav7kvk6s70k6nd70mt50isubgff4.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.expensify.chat.adhoc" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index 4f758f27d255..1347572bcc91 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,8 +10,6 @@ include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') include ':react-native-plaid-link-sdk' project(':react-native-plaid-link-sdk').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-plaid-link-sdk/android') -include ':react-native-dev-menu' -project(':react-native-dev-menu').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-dev-menu/android') include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') includeBuild('../node_modules/react-native') { diff --git a/assets/images/Star.svg b/assets/images/Star.svg index 71fdfde500a0..53666354f62a 100644 --- a/assets/images/Star.svg +++ b/assets/images/Star.svg @@ -1,9 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/binoculars.svg b/assets/images/binoculars.svg new file mode 100644 index 000000000000..64977dee38b5 --- /dev/null +++ b/assets/images/binoculars.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/bookmark.svg b/assets/images/bookmark.svg index d7c1a8397b37..7e1cb61e40bf 100644 --- a/assets/images/bookmark.svg +++ b/assets/images/bookmark.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/caret-up-down.svg b/assets/images/caret-up-down.svg index d08aa2a1ebbd..054aa53e8f75 100644 --- a/assets/images/caret-up-down.svg +++ b/assets/images/caret-up-down.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/companyCards/amex.svg b/assets/images/companyCards/amex.svg index 73e8164cdc63..61a7561a0622 100644 --- a/assets/images/companyCards/amex.svg +++ b/assets/images/companyCards/amex.svg @@ -1,40 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-amex.svg b/assets/images/companyCards/card-amex.svg index 0e8b2d22e9b4..816b3ce3d9f3 100644 --- a/assets/images/companyCards/card-amex.svg +++ b/assets/images/companyCards/card-amex.svg @@ -1,32 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-bofa.svg b/assets/images/companyCards/card-bofa.svg index 469142e4d6ff..3cc7cf1de2cc 100644 --- a/assets/images/companyCards/card-bofa.svg +++ b/assets/images/companyCards/card-bofa.svg @@ -1,32 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-brex.svg b/assets/images/companyCards/card-brex.svg index dd19403d5837..d2511fb4bf31 100644 --- a/assets/images/companyCards/card-brex.svg +++ b/assets/images/companyCards/card-brex.svg @@ -1,27 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-capital_one.svg b/assets/images/companyCards/card-capital_one.svg index 0a324710ae5d..64e79b8745db 100644 --- a/assets/images/companyCards/card-capital_one.svg +++ b/assets/images/companyCards/card-capital_one.svg @@ -1,42 +1 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/companyCards/card-capitalone.svg b/assets/images/companyCards/card-capitalone.svg index 95948992383b..a7c54c7bf529 100644 --- a/assets/images/companyCards/card-capitalone.svg +++ b/assets/images/companyCards/card-capitalone.svg @@ -1,27 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-chase.svg b/assets/images/companyCards/card-chase.svg index 7bea71bd66ec..e0f539eeb766 100644 --- a/assets/images/companyCards/card-chase.svg +++ b/assets/images/companyCards/card-chase.svg @@ -1,24 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-citi.svg b/assets/images/companyCards/card-citi.svg index c8d71afd7798..9c35e1b1ea4f 100644 --- a/assets/images/companyCards/card-citi.svg +++ b/assets/images/companyCards/card-citi.svg @@ -1,32 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-expensify.svg b/assets/images/companyCards/card-expensify.svg index 9fd29b511c7b..3763b50e4b8a 100644 --- a/assets/images/companyCards/card-expensify.svg +++ b/assets/images/companyCards/card-expensify.svg @@ -1,99 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-mastercard.svg b/assets/images/companyCards/card-mastercard.svg index e8d3cf8f4096..d8f90ea1f186 100644 --- a/assets/images/companyCards/card-mastercard.svg +++ b/assets/images/companyCards/card-mastercard.svg @@ -1,27 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-stripe.svg b/assets/images/companyCards/card-stripe.svg index 608f067a1854..a618dc96af78 100644 --- a/assets/images/companyCards/card-stripe.svg +++ b/assets/images/companyCards/card-stripe.svg @@ -1,39 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-visa.svg b/assets/images/companyCards/card-visa.svg index 9e2eae97ba90..dd8ca795403d 100644 --- a/assets/images/companyCards/card-visa.svg +++ b/assets/images/companyCards/card-visa.svg @@ -1,73 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card-wells_fargo.svg b/assets/images/companyCards/card-wells_fargo.svg index 66402710de97..8bb8b54bbbd4 100644 --- a/assets/images/companyCards/card-wells_fargo.svg +++ b/assets/images/companyCards/card-wells_fargo.svg @@ -1,35 +1 @@ - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/companyCards/card-wellsfargo.svg b/assets/images/companyCards/card-wellsfargo.svg index 086f66cc0423..bf9ea49ee2bd 100644 --- a/assets/images/companyCards/card-wellsfargo.svg +++ b/assets/images/companyCards/card-wellsfargo.svg @@ -1,57 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/card=-generic.svg b/assets/images/companyCards/card=-generic.svg index 61e4296f7779..192c194da9e7 100644 --- a/assets/images/companyCards/card=-generic.svg +++ b/assets/images/companyCards/card=-generic.svg @@ -1,25 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/emptystate__card-pos.svg b/assets/images/companyCards/emptystate__card-pos.svg index 6a6fbae74a04..e7f8429c254c 100644 --- a/assets/images/companyCards/emptystate__card-pos.svg +++ b/assets/images/companyCards/emptystate__card-pos.svg @@ -1,643 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/mastercard.svg b/assets/images/companyCards/mastercard.svg index dcfac5eb33dd..24ff5d159c0b 100644 --- a/assets/images/companyCards/mastercard.svg +++ b/assets/images/companyCards/mastercard.svg @@ -1,40 +1 @@ - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/pending-bank.svg b/assets/images/companyCards/pending-bank.svg index dc265466d53f..58b7b96dab28 100644 --- a/assets/images/companyCards/pending-bank.svg +++ b/assets/images/companyCards/pending-bank.svg @@ -1,263 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg b/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg index 0f40859c8839..258b0d0bb7b4 100644 --- a/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg +++ b/assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg @@ -1,244 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/companyCards/visa.svg b/assets/images/companyCards/visa.svg index 4a7a73b66639..4195eb76442a 100644 --- a/assets/images/companyCards/visa.svg +++ b/assets/images/companyCards/visa.svg @@ -1,74 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/expensify-card-icon.svg b/assets/images/expensify-card-icon.svg index 8680b7a22878..ab78635a8d23 100644 --- a/assets/images/expensify-card-icon.svg +++ b/assets/images/expensify-card-icon.svg @@ -1,16 +1 @@ - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/expensify-card.svg b/assets/images/expensify-card.svg index 2989f5025ae4..9614ef4955cc 100644 --- a/assets/images/expensify-card.svg +++ b/assets/images/expensify-card.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/gallery-not-found.svg b/assets/images/gallery-not-found.svg index 25da973ce9cb..87231be3741b 100644 --- a/assets/images/gallery-not-found.svg +++ b/assets/images/gallery-not-found.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/laptop-with-second-screen-sync.svg b/assets/images/laptop-with-second-screen-sync.svg index a74048795dbf..153825d36415 100644 --- a/assets/images/laptop-with-second-screen-sync.svg +++ b/assets/images/laptop-with-second-screen-sync.svg @@ -1,213 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/laptop-with-second-screen-x.svg b/assets/images/laptop-with-second-screen-x.svg index f4b6b77f70f1..8d051989bca4 100644 --- a/assets/images/laptop-with-second-screen-x.svg +++ b/assets/images/laptop-with-second-screen-x.svg @@ -1,150 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg index 0b85744c1869..14de9eff24c1 100644 --- a/assets/images/product-illustrations/broken-magnifying-glass.svg +++ b/assets/images/product-illustrations/broken-magnifying-glass.svg @@ -1,28 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/emptystate__puzzlepieces.svg b/assets/images/simple-illustrations/emptystate__puzzlepieces.svg new file mode 100644 index 000000000000..d137ce5dcff2 --- /dev/null +++ b/assets/images/simple-illustrations/emptystate__puzzlepieces.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg index 9c0711fcaedc..1a99094d07d9 100644 --- a/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg +++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles_blue.svg @@ -1,22 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg index eb2bad31620d..496255692f8c 100644 --- a/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg +++ b/assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg @@ -1,49 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg b/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg index e7f64f69305a..3bb3514f1ebc 100644 --- a/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg +++ b/assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg @@ -1,49 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__perdiem.svg b/assets/images/simple-illustrations/simple-illustration__perdiem.svg new file mode 100644 index 000000000000..ea5a865a2694 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__perdiem.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__rules.svg b/assets/images/simple-illustrations/simple-illustration__rules.svg index 6432f26d9ac6..5646cc0f5c2a 100644 --- a/assets/images/simple-illustrations/simple-illustration__rules.svg +++ b/assets/images/simple-illustrations/simple-illustration__rules.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/spreadsheet-computer.svg b/assets/images/spreadsheet-computer.svg index 74cac455537a..1a42220c8d86 100644 --- a/assets/images/spreadsheet-computer.svg +++ b/assets/images/spreadsheet-computer.svg @@ -1,186 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/table.svg b/assets/images/table.svg index dea1e990b97d..8a77919aa5a5 100644 --- a/assets/images/table.svg +++ b/assets/images/table.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/images/turtle-in-shell.svg b/assets/images/turtle-in-shell.svg index 6c5a8e74bb31..631aeb6b0940 100644 --- a/assets/images/turtle-in-shell.svg +++ b/assets/images/turtle-in-shell.svg @@ -1,87 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/user-eye.svg b/assets/images/user-eye.svg index 2265b4892ded..7aa640b180d1 100644 --- a/assets/images/user-eye.svg +++ b/assets/images/user-eye.svg @@ -1,12 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/user-plus.svg b/assets/images/user-plus.svg index bd49633bf738..84af850da735 100644 --- a/assets/images/user-plus.svg +++ b/assets/images/user-plus.svg @@ -1,11 +1 @@ - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/config/webpack/CustomVersionFilePlugin.ts b/config/webpack/CustomVersionFilePlugin.ts index 96ab8e61e480..1e442d55325e 100644 --- a/config/webpack/CustomVersionFilePlugin.ts +++ b/config/webpack/CustomVersionFilePlugin.ts @@ -4,23 +4,31 @@ import type {Compiler} from 'webpack'; import {version as APP_VERSION} from '../../package.json'; /** - * Simple webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json' + * Custom webpack plugin that writes the app version (from package.json) and the webpack hash to './version.json' */ class CustomVersionFilePlugin { apply(compiler: Compiler) { compiler.hooks.done.tap(this.constructor.name, () => { const versionPath = path.join(__dirname, '/../../dist/version.json'); - fs.mkdir(path.dirname(versionPath), {recursive: true}, (directoryError) => { - if (directoryError) { - throw directoryError; - } - fs.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), {encoding: 'utf8'}, (error) => { - if (!error) { - return; + + fs.promises + .mkdir(path.dirname(versionPath), {recursive: true}) + .then(() => fs.promises.readFile(versionPath, 'utf8')) + .then((existingVersion) => { + const {version} = JSON.parse(existingVersion) as {version: string}; + + if (version !== APP_VERSION) { + fs.promises.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), 'utf8'); + } + }) + .catch((err: NodeJS.ErrnoException) => { + if (err.code === 'ENOENT') { + // if file doesn't exist + fs.promises.writeFile(versionPath, JSON.stringify({version: APP_VERSION}), 'utf8'); + } else { + throw err; } - throw error; }); - }); }); } } diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 2d8e27fd453e..ab5c304fcd1e 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -11,6 +11,8 @@ import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import CustomVersionFilePlugin from './CustomVersionFilePlugin'; import type Environment from './types'; +dotenv.config(); + type Options = { rel: string; as: string; @@ -82,6 +84,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): isWeb: platform === 'web', isProduction: file === '.env.production', isStaging: file === '.env.staging', + useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && file === '.env.production'), }), new PreloadWebpackPlugin({ rel: 'preload', diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 80813adc1e3a..2279082024d1 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import path from 'path'; import portfinder from 'portfinder'; import {TimeAnalyticsPlugin} from 'time-analytics-webpack-plugin'; @@ -54,15 +56,15 @@ const getConfiguration = (environment: Environment): Promise => }, }, headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention 'Document-Policy': 'js-profiling', }, }, plugins: [ new DefinePlugin({ - // eslint-disable-next-line @typescript-eslint/naming-convention 'process.env.PORT': port, + 'process.env.NODE_ENV': JSON.stringify('development'), }), + new ReactRefreshWebpackPlugin({overlay: {sockProtocol: 'wss'}}), ], cache: { type: 'filesystem', @@ -82,7 +84,7 @@ const getConfiguration = (environment: Environment): Promise => }, }); - return TimeAnalyticsPlugin.wrap(config); + return TimeAnalyticsPlugin.wrap(config, {plugin: {exclude: ['ReactRefreshPlugin']}}); }); export default getConfiguration; diff --git a/contributingGuides/BUGZERO_CHECKLIST.md b/contributingGuides/BUGZERO_CHECKLIST.md new file mode 100644 index 000000000000..00075620641c --- /dev/null +++ b/contributingGuides/BUGZERO_CHECKLIST.md @@ -0,0 +1,62 @@ +# BugZero Checklist: + +- [ ] **[Contributor]** Classify the bug: + +
+Bug classification + + +Source of bug: + - [ ] 1a. Result of the original design (eg. a case wasn't considered) + - [ ] 1b. Mistake during implementation + - [ ] 1c. Backend bug + - [ ] 1z. Other: + +Where bug was reported: + - [ ] 2a. Reported on production + - [ ] 2b. Reported on staging (deploy blocker) + - [ ] 2c. Reported on a PR + - [ ] 2z. Other: + +Who reported the bug: + - [ ] 3a. Expensify user + - [ ] 3b. Expensify employee + - [ ] 3c. Contributor + - [ ] 3d. QA + - [ ] 3z. Other: + +
+ +- [ ] **[Contributor]** The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake. + + Link to comment: + +- [ ] **[Contributor]** If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in [#expensify-open-source](https://app.slack.com/client/E047TPA624F/C01GTK53T8Q) has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner. + + Link to discussion: + +- [ ] **[Contributor]** If it was decided to create a regression test for the bug, please propose the [regression test](https://github.com/Expensify/App/blob/main/contributingGuides/REGRESSION_TEST_BEST_PRACTICES.md) steps using the template below to ensure the same bug will not reach production again. + +
+Regression Test Proposal Template + + +- [ ] **[BugZero Assignee]** Create a GH issue for creating/updating the regression test once above steps have been agreed upon. + + Link to issue: + +## Regression Test Proposal +### Precondition: + + +- + +### Test: + + +1. + +Do we agree 👍 or 👎 + + +
diff --git a/contributingGuides/LEFT_HAND_NAVIGATION.md b/contributingGuides/LEFT_HAND_NAVIGATION.md new file mode 100644 index 000000000000..344b0c4f8ecf --- /dev/null +++ b/contributingGuides/LEFT_HAND_NAVIGATION.md @@ -0,0 +1,44 @@ +## OVERVIEW + +The Left Hand Navigation (LHN) is designed to show different types of reports based on their status, user settings, and specific conditions. Each report type has unique visual indicators and sorting rules to help users quickly identify and prioritize their tasks. + +### Types of report displayed in the LHN + +The following outlines the expected behavior regarding which reports are displayed in the LHN: + +- The report currently being viewed by the user is highlighted as the active report in the LHN, making it easy for users to locate their focus point within the navigation. +If a report has unresolved issues, like an unapproved expense or outstanding violations, it will display a red dot next to it, indicating urgent action is required. These reports are displayed at the top of the LHN list (under pinned chats) and sorted alphabetically by report name for easy access. +- Reports that need user action, such as responding to a message that mentions them, completing an assigned task, or addressing an expense, will display a green dot next to them. Additionally, if a system or concierge message indicates a trial period has expired and a payment method is missing, it will prompt the user with a similar green dot. This visual indicator helps users quickly identify where their attention is required. +- If a user has started drafting a comment in a report, a pencil icon as indicator appears next to it in the LHN, letting users know there is an incomplete draft. These reports are sorted alphabetically by report name. +- Pinned reports are always displayed at the top of the LHN list and are sorted alphabetically by name, giving quick access to reports the user wants to keep top-of-mind. +- When the user has focus mode enabled, unread chat messages will display in bold in the LHN. This also applies to reports where notifications are hidden. Unread chats in focus mode are sorted alphabetically by report name to help users locate them more easily. +- Archived reports are displayed in the LHN when the user is in default mode. These reports are shown with an indication that they are archived and are sorted by the date of the last visible action, with the most recent appearing first. +- Self-DM messages will now be displayed in LHN. This allows users who want to track their own notes or messages in the LHN to do so without needing to look elsewhere. + +### Types of report excluded from the LHN + +Certain reports are excluded from the LHN to avoid clutter and to focus on relevant content for the user: + +- Reports that are explicitly marked as hidden. +- Reports with no participants are not displayed, as they lack meaningful content. +- If the user does not have permission to access a report (due to policy restrictions), it will not be shown. +- Transaction threads that contain only one transaction are excluded. +- If a report is an empty chat, unless it's a report user is actively looking at. +- For users with domain-based email addresses, reports are hidden if the includeDomainEmail setting is disabled. +- Reports with a parent message pending deletion. +- When focus mode is enabled and there are no unread messages. + +### Sorting priorities for displayed report groups + +1. Pinned, RBR and attention-required (GBR) reports: + - Always sorted alphabetically by report name. +2. Error reports: + - Sorted alphabetically by report name. +3. Draft reports: + - Sorted alphabetically by report name. +4. Non-Archived reports: + - In default mode, these are sorted by the lastVisibleActionCreated date, so the most recently updated reports appear first. + - In focus mode, these reports are sorted alphabetically by name for quicker navigation. +5. Archived eports: + - In default mode, these are sorted by lastVisibleActionCreated, with recent reports displayed first. + - In focus mode, archived reports are sorted alphabetically by name. diff --git a/tests/perf-test/README.md b/contributingGuides/REASSURE_PERFORMANCE_TEST.md similarity index 90% rename from tests/perf-test/README.md rename to contributingGuides/REASSURE_PERFORMANCE_TEST.md index 2b66f7c147f3..0de450b78875 100644 --- a/tests/perf-test/README.md +++ b/contributingGuides/REASSURE_PERFORMANCE_TEST.md @@ -7,8 +7,11 @@ We use Reassure for monitoring performance regression. It helps us check if our - Reassure builds on the existing React Testing Library setup and adds a performance measurement functionality. It's intended to be used on local machine and on a remote server as part of your continuous integration setup. - To make sure the results are reliable and consistent, Reassure runs tests twice – once for the current branch and once for the base branch. -## Performance Testing Strategy (`measurePerformance`) +## Performance Testing Strategy (`measureRenders`) +- Before adding new tests, check if the proposed scenario or component is already covered in existing tests. Duplicate tests can slow down the CI suite, making it harder to spot meaningful regressions. +- Test only scenarios that cover new or unique interactions. Avoid testing repetitive user actions that could be captured within a single, comprehensive scenario. +- Where applicable, use utility functions and helper methods to consolidate common actions (e.g., data mocking, scenario setup) across tests. This reduces redundancy and allows tests to be more focused and reusable. - The primary focus is on testing business cases rather than small, reusable parts that typically don't introduce regressions, although some tests in that area are still necessary. - To achieve this goal, it's recommended to stay relatively high up in the React tree, targeting whole screens to recreate real-life scenarios that users may encounter. - For example, consider scenarios where an additional `useMemo` call could impact performance negatively. @@ -84,7 +87,7 @@ test('Count increments on press', async () => { await screen.findByText('Count: 2'); }; - await measurePerformance( + await measureRenders( , { scenario, runs: 20 } ); diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 4ff1f01b1475..5fc14328f3b4 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -19,7 +19,6 @@ - [ ] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack - [ ] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code)) - [ ] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`). - - [ ] I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. `myBool && `. - [ ] I verified that comments were added to code that is not self explanatory - [ ] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing. - [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 75cb080f1349..926fb1e24d22 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.8", + "electron-updater": "^6.3.9", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, @@ -59,9 +59,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz", - "integrity": "sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==", + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -156,12 +156,12 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "node_modules/electron-updater": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.8.tgz", - "integrity": "sha512-OFsA2vyuOZgsqq4EW6tgW8X8e521ybDmQyIYLqss7HdXev+Ak90YatzpIECOBJXpmro5YDq4yZ2xFsKXqPt1DQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", "license": "MIT", "dependencies": { - "builder-util-runtime": "9.2.9", + "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -469,9 +469,9 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "builder-util-runtime": { - "version": "9.2.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.9.tgz", - "integrity": "sha512-DWeHdrRFVvNnVyD4+vMztRpXegOGaQHodsAjyhstTbUNBIjebxM1ahxokQL+T1v8vpW8SY7aJ5is/zILH82lAw==", + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -538,11 +538,11 @@ "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "electron-updater": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.8.tgz", - "integrity": "sha512-OFsA2vyuOZgsqq4EW6tgW8X8e521ybDmQyIYLqss7HdXev+Ak90YatzpIECOBJXpmro5YDq4yZ2xFsKXqPt1DQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", "requires": { - "builder-util-runtime": "9.2.9", + "builder-util-runtime": "9.2.10", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", diff --git a/desktop/package.json b/desktop/package.json index 326d6f24f740..ac66df7e9aed 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -6,7 +6,7 @@ "dependencies": { "electron-context-menu": "^2.3.0", "electron-log": "^4.4.8", - "electron-updater": "^6.3.8", + "electron-updater": "^6.3.9", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12" }, diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 4c492ae1e251..9932c5f85699 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,70 +1,85 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + bigdecimal (3.1.8) colorator (1.1.0) - concurrent-ruby (1.1.10) + concurrent-ruby (1.3.4) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) eventmachine (1.2.7) - ffi (1.15.5) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-darwin) forwardable-extended (2.6.0) + google-protobuf (4.28.3-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.28.3-x86_64-darwin) + bigdecimal + rake (>= 13) http_parser.rb (0.8.0) - i18n (0.9.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jekyll (3.9.3) + jekyll (4.3.4) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (>= 0.7, < 2) - jekyll-sass-converter (~> 1.0) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (~> 0.3.3) + mercenary (>= 0.3.6, < 0.5) pathutil (~> 0.9) - rouge (>= 1.7, < 4) + rouge (>= 3.0, < 5.0) safe_yaml (~> 1.0) - jekyll-feed (0.15.1) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) jekyll-redirect-from (0.16.0) jekyll (>= 3.3, < 5.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) jekyll-seo-tag (2.8.0) jekyll (>= 3.8, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - kramdown (2.3.2) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.7.1) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - mercenary (0.3.6) + mercenary (0.4.0) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.7) - rb-fsevent (0.11.1) - rb-inotify (0.10.1) + public_suffix (6.0.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.2.5) - rouge (3.26.0) + rexml (3.3.9) + rouge (4.4.0) safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - webrick (1.7.0) + sass-embedded (1.80.6-arm64-darwin) + google-protobuf (~> 4.28) + sass-embedded (1.80.6-x86_64-darwin) + google-protobuf (~> 4.28) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.0) PLATFORMS arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-darwin-20 x86_64-darwin-21 diff --git a/docs/Hidden/Expensify-Lounge.md b/docs/Hidden/Expensify-Lounge.md new file mode 100644 index 000000000000..716040ba2078 --- /dev/null +++ b/docs/Hidden/Expensify-Lounge.md @@ -0,0 +1,66 @@ +--- +title: Expensify Lounge +description: Explore the Expensify Lounge - A stylish space to work, relax, and connect. +--- + +The Expensify Lounge is a place where people come to Get Shit Done. With beautiful surroundings, great coffee, and a collaborative community, it's the perfect environment to fuel productivity. Check out this guide on how to make the most of the Expensify Lounge! + +# The Two Rules + +## Rule #1 - Get Shit Done +The Lounge is designed to help you focus, collaborate, and bring your boldest ideas to life. To keep this environment productive, we ask our members to remember: + +- **#focus** - Use the space as it’s intended, without disrupting others. The Lounge is social and collaborative but ultimately meant to support productive work. +- **#urgency** - Remote work is fantastic, but face-to-face collaboration is unmatched. Use the Lounge to meet co-workers in person and drive your projects forward. +- **#results** - Don’t confuse time spent with effort or effort with results. Visualize what you want to accomplish and don’t leave until it’s done. + +## Rule #2 - Don’t Ruin It for Everyone Else +We want the Lounge to be an incredible, ever-evolving space. To achieve this, please follow these guidelines: + +- **#writeitdown** - If you can share knowledge, do it! Write a blog post, document, or post in Expensify Chat to help others learn from your experience. Suggestions to improve the Lounge are always welcome. +- **#showup** - Be fully present when you’re here. Engage with others and collaborate in social spaces. This is a community built to get shit done; the more you contribute, the more you gain. +- **#oneteam** - Inclusivity is a priority. We do not tolerate any form of discrimination. Make an effort to include those who want to join. +- **#nocreeps** - Don’t make others feel uncomfortable with your words or actions. If you feel uncomfortable or notice it happening to someone else, use the escalation process in the FAQ. + +--- + +# How to Use the Expensify Lounge +With these two rules in mind, here’s how to get the most from the Lounge: + +## Rule #1 - Getting Shit Done +- **Order drinks from Concierge** - Contact Concierge here to ask questions or order beverages, and they’ll deliver your order to you. +- **Using an office** - Offices are first-come, first-serve, and ideal for brief calls or meetings. Please keep usage to under an hour. Offices cannot be reserved. +- **Lounge hours** - The Lounge is open from 8am-6pm PT, Monday through Friday, and closed on some major holidays. Check our Google Maps profile for holiday hours. +- **Suggest improvements** - Post any ideas to enhance the Lounge experience in #announce - Expensify Lounge. + +## Rule #2 - Not Ruining It for Everyone Else +- **Offices are for calls** - Only use an office if you have a call or meeting, and try to keep it under an hour. +- **Respect others** - Avoid being too loud or distracting while others work. When collaborating in Expensify Chat, be respectful and maintain a positive environment. +- **Stay home if you’re sick** - If you’re feeling unwell, please skip the Lounge or wear a mask in public areas. +- **If you see something, say something** - If you feel uncomfortable or notice others in discomfort, notify Concierge. In Expensify Chat, you can also use our moderation tools (outlined in the FAQ). + +We’re thrilled to have you here to live richly, have fun, and help save the world with us. Now, go enjoy the Expensify Lounge, and let’s Get Shit Done! + +--- + +{% include faq-begin.md %} + +## What is Concierge? +Concierge is our automated system that answers member questions in real-time. Local lounge questions are routed to the Lounge’s Concierge. Message Concierge for drink requests or general inquiries—they’ll handle it for you! + +## Who is invited to the Expensify Lounge? +Everyone is invited! Whether you’re a current customer or just need a productive space, we’d love to have you. + +## How do I escalate something that’s making me or someone else uncomfortable? +In Expensify Chat, use the escalation feature to flag messages as: + +- **Spam or Inconsiderate**: This sends a whisper to the sender and flags the message. These flags are visible to all users but not reviewed by Concierge. +- **Intimidating or Bullying**: The message is hidden and reviewed. If confirmed, it will remain hidden, and we’ll communicate the violation to the sender. +- **Harassment or Assault**: The message is hidden immediately, and our team reviews it. The sender receives a warning, and Concierge may block the user if needed. + +In person, please notify Concierge with your lounge location, and they’ll escalate the issue accordingly. + +## Where are other Expensify Lounge locations? +Currently, we only have the San Francisco Lounge, but stay tuned for more locations coming soon! +{% include faq-end.md %} + diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html index 798fb2cf7e96..c7b55b28cfd5 100644 --- a/docs/_includes/footer.html +++ b/docs/_includes/footer.html @@ -50,10 +50,10 @@

Resources

ExpensifyHelp
  • - Community + Terms of Service
  • - Privacy + Privacy
  • diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md index 84fafc949527..18020402f7de 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice.md @@ -10,9 +10,15 @@ There are multiple ways to pay Invoices in Expensify. Let’s go over each metho # How to Pay Invoices 1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Invoice you’d like to pay to see the details. -3. Click on the **Pay** button. -4. Follow the prompts to pay through one of the following methods. +2. Click on **Home** and find the pending Invoice payment +3. Click **Pay** to be redirected to the Invoice +4. Review the Invoice +5. When you are ready to pay, click the **Pay** button at the top of the Invoice +6. Follow the prompts to pay through one of the following methods. + +![Click Home and Pay on the invoice](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png){:width="100%"} + +![Click Pay on Invoice and choose a method of payment](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png){:width="100%"} ### ACH bank-to-bank transfer diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md index afe366fb1dbe..41dc52a4239c 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports.md @@ -14,7 +14,7 @@ Before a report can be reimbursed via direct deposit: To reimburse a report via direct deposit (USD): 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. @@ -27,7 +27,7 @@ Before a report can be reimbursed via global reimbursement: To reimburse a report via global reimbursement: 1. Open the report. -2. Click the **Reimburse** button and select **Via Direct Deposit (ACH)**. +2. Click the **Reimburse** button and select **Via Direct Deposit**. 3. Confirm that the correct bank account is listed in the dropdown menu. 4. Click **Accept Terms & Pay**. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md index bda84eb0a49f..30785330a9ad 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md @@ -52,6 +52,10 @@ For this step, it is key to ensure that the correct company file is open in Quic ![The Web Connector pop-up, where you will need to click "Yes"](https://help.expensify.com/assets/images/QBO_desktop_07.png){:width="100%"} +{% include info.html %} +Be sure to securely save this password in a trusted password manager. You'll need it for future configuration updates or troubleshooting. Having it easily accessible will help avoid delays and ensure a smoother workflow. +{% include end-info.html %} + # FAQ ## What are the hardware and software requirements for the QuickBooks Desktop connector? diff --git a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md index 6257c1e6d84d..3e9b6c0397db 100644 --- a/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md +++ b/docs/articles/expensify-classic/expenses/Add-Invoices-in-Bulk.md @@ -14,6 +14,13 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f 5. Add the invoice details following the formatting rules (see below **CSV formatting guide** section) 6. Click **Upload CSV** +![Click Reports, New Reports, choose Bulk Import Invoices](https://help.expensify.com/assets/images/invoice-bulk-01.png){:width="100%"} + +![Download Sample CSV](https://help.expensify.com/assets/images/invoice-bulk-02.png){:width="100%"} + +![Format CSV following our guidelines](https://help.expensify.com/assets/images/invoice-bulk-03.png){:width="100%"} + + ## CSV formatting guide - Send to: recipient's email address (ex: john.smith@companydomain.com) @@ -27,10 +34,15 @@ Expensify offers importing multiple invoices (bulk import) via CSV to save you f ## After the Invoices are uploaded - After you click **Upload**, the invoices will automatically be created and viewable on the **Reports** page. +- Set the **Reports page** filter to Invoices to narrow down your search. - The **Send To** contact will get an email notifying them of the invoice you sent. - You can manually edit the invoice details. - You can manually upload a PDF of the invoice to the report. +![Search for Invoices on Reports page](https://help.expensify.com/assets/images/invoice-bulk-04.png){:width="100%"} + +![Invoices will indicate next steps at the top of each report](https://help.expensify.com/assets/images/invoice-bulk-05.png){:width="100%"} + {% include faq-begin.md %} ## Are there any fees associated with Invoices in Expensify? diff --git a/docs/articles/expensify-classic/expenses/Add-an-expense.md b/docs/articles/expensify-classic/expenses/Add-an-expense.md index 461748c6af9e..5f40ff377be6 100644 --- a/docs/articles/expensify-classic/expenses/Add-an-expense.md +++ b/docs/articles/expensify-classic/expenses/Add-an-expense.md @@ -2,7 +2,6 @@ title: Add an expense description: Create a new expense in Expensify --- -
    You can add an expense automatically with SmartScan or enter the expense details manually. @@ -41,62 +40,189 @@ You can open any receipt and click **Fill out details myself** to add or edit th {% include end-selector.html %} -# Email a receipt - You can also email receipts to SmartScan by sending them to receipts@expensify.com from an email address tied to your Expensify account (either a primary or secondary email). SmartScan will automatically pull all of the details from the receipt, fill them in for you, and add the receipt to the Expenses tab on your account. {% include info.html %} **For copilots**: To ensure a receipt is routed to the Expensify account you are copiloting instead of your own account, email the receipt to receipts@expensify.com with the email address of the account you are copiloting as the subject line of the email. {% include end-info.html %} -# Add an expense manually +# Add a per diem expense + +A per diem (also called “per diem allowance” or “daily allowance”) is a fixed daily payment provided by an employer to cover expenses during business or work-related travel. These allowances simplify travel expense tracking and reimbursement for meals, lodging, and incidental expenses. + +{% include info.html %} +Before you can add a per diem expense, a Workspace Admin must [enable per diem expenses](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses) for the workspace and add the per diem rates. If you do not see an option for per diem rates, it is currently unavailable for your workspace, and you’ll need to reach out to one of your Workspace Admins for guidance. +{% include end-info.html %} + +To add a per diem expense, + +1. Click the **Expenses** tab. +2. Click **New Expense** and choose **Per Diem**. +3. Select your travel destination. + - If your trip involves multiple stops, create a separate per diem expense for each destination. +4. Select the start date, end date, start time, and end time for the trip. +5. Select a sub-rate. The available sub-rates are dependent on the trip duration. + - You can include meal deductions or overnight lodging costs if allowed by your workspace. +6. Enter any other required coding information, such as the category, description, or report, and click **Save**. + +# Add a mileage expense + +You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance: + +- Web app: + - **Manually create**: Manually enter the number of miles for the trip + - **Create from map**: Automatically determine the trip distance based on the start and end location. +- Mobile app: + - **Manually create**: Manually enter the miles for the trip and your mileage rate + - **Odometer**: Enter your odometer reading before and after the trip + - **Start GPS**: Currently under development and unavailable for use. + +{% include info.html %} +When adding a distance expense, the rates available are determined by the rates set in your [workspace rate settings](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). To update these rates or add a new rate, you must be a Workspace Admin. +{% include end-info.html %} {% include selector.html values="desktop, mobile" %} {% include option.html value="desktop" %} 1. Click the **Expenses** tab. -2. Click the + icon in the top right. -3. Select the type of expense. - - **Manually create**: Manually enter receipt details. - - **Scan receipt**: Upload a saved image of a receipt. - - **Create multiple**: Manually enter multiple expenses at once. - - **Time**: Create an expense based on hours. - - **Distance**: Create an expense based on distance. - - Manually Create: Manually enter the distance details for the expense. - - Create from Map: Enter the start and end destination and Expensify will help you create a receipt for the trip. -4. Click **Save**. +2. Click **New Expense**. +3. Select the expense type. + - **Manually create**: + - Enter the number of miles for the trip. + - Select your rate. + - If desired, select the category, add a description, or select a report to add the expense to. + - Click **Save**. + - **Create from map**: + - Add your start location as point A. + - Add your end location as point B. + - If applicable, click **Add Destination** to add additional stops. + - To generate a map receipt, leave the Create Receipt checkbox selected. + - Click **Save**. + - Select your rate. + - If desired, select the category, add a description, or select a report to add the expense to. + - Click **Save**. + {% include end-option.html %} {% include option.html value="mobile" %} -1. Tap the ☰ menu icon in the top left. -2. Tap **Expenses**. -3. Tap the + icon in the top right. -4. Tap the correct expense type and enter the expense details. - - **Manually create**: Manually enter receipt details. - - **Time**: Enter work time and rate. - - **Manually create (Distance)**: Manually enter trip details by total distance. - - **Odometer**: Manually enter trip details by start and end odometer readings. - - **Start GPS**: Track distance while using the Expensify app to automatically calculate the distance in real time during the trip. -5. Tap **Save**. +1. Click the + icon in the top right corner. +2. Under the Distance section, select the expense type. + - **Manually create**: + - Enter your mileage. + - Select your rate. + - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. + - Click **Save**. + - **Odometer**: + - Enter your vehicle’s odometer reading before the trip. + - Enter your vehicle’s odometer reading after the trip. + - Select your rate. + - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. + - Click **Save**. {% include end-option.html %} {% include end-selector.html %} +# Add a group expense + +Capture group and event expenses with Attendee Tracking by documenting who attended and the cost per attendee. The amount is always divided evenly between all attendees—different amounts cannot be allocated to specific attendees. To divide the amounts differently, you’ll first have to split the expense. + {% include info.html %} -If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings. +Attendees added to an expense will not be notified that they were added to an expense, nor will they share in the expense or be requested to pay for any portion of the expense. {% include end-info.html %} -# FAQs +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the **Expenses** tab. +2. Click the expense you want to add attendees to. +3. Click the attendees field and enter the name or email address of the attendee. + - If the attendee is a member of your workspace, you can select their name from the list. + - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. +4. Click **Save**. + +Once added, you’ll also see the list of attendees in the expense overview on the Expenses tab. To see the cost per employee, hover over the receipt total. These details are also available on any report that you add the expense to. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the **Expenses** tab. +2. Tap the expense you want to add attendees to. +3. Scroll down to the bottom and tap **More Options**. +4. Tap the attendees field and enter the name or email address of the attendee. + - If the attendee is a member of your workspace, you can select their name from the list. + - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. +5. Tap **Save**. + +Attendees will also be listed on any report that you add the expense to. + +{% include end-option.html %} + +{% include end-selector.html %} + +# Add expenses in bulk + +You can upload bulk receipt images or add receipt details in bulk. + +## SmartScan receipt images in bulk + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the **Expenses** tab. +2. Drag and drop up to 10 images or PDF receipts at once from your computer’s files. You can drop them anywhere on the Expense page where you see a green plus icon next to your mouse cursor. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the mobile app and tap the camera icon in the bottom right corner. +2. Tap the camera icon in the right corner to select the Rapid Fire mode. +3. Take a clear photo of each receipt. +4. When all receipts are captured, tap the X in the left corner to close the camera. +{% include end-option.html %} + +{% include end-selector.html %} + +## Manually add receipt details in bulk + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Click the **Expenses** tab. +2. Click **New Expense** and select **Create Multiple**. +3. Enter the expense details for up to 10 expenses and click **Save**. + +## Upload personal expenses via CSV, XLS, etc. + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Under Personal Cards, click **Import Transactions from File**. +4. Click **Upload** and select a .csv, .xls, .ofx, or a .qfx file. + +{% include faq-begin.md %} **What’s the difference between a reimbursable and non-reimbursable expense?** -- Reimbursable expenses are things that you pay for with your own money that the company has agreed to pay you back for (like business travel paid for with personal funds). -- Non-reimbursable expenses are things you pay for with company money that need to be documented for accounting purposes (like a lunch paid for with a company card). +- **Reimbursable expenses**: Expenses that the company has agreed to pay you back for. This may include: + - Cash & personal card: Expenses paid for by the employee on behalf of the business. + - Per diem: Expenses for a daily or partial daily rate [configured in your Workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses). + - Time: An hourly rate for your employees or jobs as [set for your workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). This expense type is usually used by contractors or small businesses billing the customer via [Expensify Invoicing](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing). + - Distance: Expenses related to business travel. +- **Non-reimbursable expenses**: Expenses are things you pay for with company money that need to be documented for accounting purposes (like a lunch paid for with a company card). +- **Billable expenses**: Business or employee expenses that must be billed to a specific client or vendor. This option is for tracking expenses for invoicing to customers, clients, or other departments. Any kind of expense can be billable, in _addition_ to being either reimbursable or non-reimbursable. + +You can also see a breakdown of these expense types on your report and can even organize the report by them. {% include info.html %} If you are an employee under a company workspace, your expenses may automatically be configured as reimbursable or non-reimbursable depending on the details that are entered. If an expense is incorrectly labeled, you must reach out to an admin to have it corrected. {% include end-info.html %} -
    +**Why don't I see the option for one of these types of expenses?** + +If you are an employee under a company workspace, you may not see all of the different expense type options depending on your company’s workspace settings. + +**How do I edit my per diem expenses?** + +Per diem expenses cannot be amended. To make changes, you must delete the expense and recreate it. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md b/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md deleted file mode 100644 index 6ee84e1ead15..000000000000 --- a/docs/articles/expensify-classic/expenses/Add-expenses-in-bulk.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Add expenses in bulk -description: Add multiple expenses at one time ---- -
    - -You can upload bulk receipt images or add receipt details in bulk. - -# SmartScan receipt images in bulk - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click the **Expenses** tab. -2. Drag and drop up to 10 images or PDF receipts at once from your computer’s files. You can drop them anywhere on the Expense page where you see a green plus icon next to your mouse cursor. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Open the mobile app and tap the camera icon in the bottom right corner. -2. Tap the camera icon in the right corner to select the Rapid Fire mode. -3. Take a clear photo of each receipt. -4. When all receipts are captured, tap the X in the left corner to close the camera. -{% include end-option.html %} - -{% include end-selector.html %} - -# Manually add receipt details in bulk - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -1. Click the **Expenses** tab. -2. Click **New Expense** and select **Create Multiple**. -3. Enter the expense details for up to 10 expenses and click **Save**. - -# Upload personal expenses via CSV, XLS, etc. - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -1. Hover over Settings, then click **Account**. -2. Click the **Credit Card Import** tab. -3. Under Personal Cards, click **Import Transactions from File**. -4. Click **Upload** and select a .csv, .xls, .ofx, or a .qfx file. - -
    diff --git a/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md b/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md index c2ebb64b0af6..fde2c43e9d95 100644 --- a/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md +++ b/docs/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md @@ -11,9 +11,18 @@ Invoices can be sent to anyone with or without an Expensify account and paid dir 1. Sign in to your [Expensify web account](www.expensify.com) 2. Customize your company invoices following the steps in this [help article](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing). (Optional) 3. From the **Reports** page, click the drop-down and select **Invoice**. -4. Upload a PDF/image of the invoice. -5. Add applicable tags and categories based on your workspace settings. -6. Click **Send**. +4. Click **Add Expense** to upload an invoice or drag and drop the invoice as a pdf into the report to start the SmartScan process. +5. Once the SmartScan process is complete, the invoice PDF will be added as a receipt to the expense +6. Add applicable tags and categories based on your workspace settings. +7. Click **Send** +8. Enter the recipient's email address +9. Add a memo, due date, attach a PDF of the invoice (Optional) +10. Click **Send** +11. The recipient will receive an email about the invoice and can pay through Expensify following these [steps](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-an-Invoice). + +![From the Reports page, click New Report and select Invoice](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_SendInvoice.png){:width="100%"} + +![Click Send and enter the recipients email address](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png){:width="100%"} ## How to Receive an Invoice Payment in Expensify diff --git a/docs/articles/expensify-classic/expenses/Track-group-expenses.md b/docs/articles/expensify-classic/expenses/Track-group-expenses.md deleted file mode 100644 index 82921b0e8cd3..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-group-expenses.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Track group expenses -description: Use Attendee Tracking to track group expenses ---- -
    - -Capture group and event expenses with Attendee Tracking by documenting who attended and the cost per attendee. The amount is always divided evenly between all attendees—different amounts cannot be allocated to specific attendees. To divide the amounts differently, you’ll first have to split the expense. - -{% include info.html %} -Attendees added to an expense will not be notified that they were added to an expense, nor will they share in the expense or be requested to pay for any portion of the expense. -{% include end-info.html %} - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click the **Expenses** tab. -2. Click the expense you want to add attendees to. -3. Click the attendees field and enter the name or email address of the attendee. - - If the attendee is a member of your workspace, you can select their name from the list. - - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. -4. Click **Save**. - -Once added, you’ll also see the list of attendees in the expense overview on the Expenses tab. To see the cost per employee, hover over the receipt total. These details are also available on any report that you add the expense to. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap the **Expenses** tab. -2. Tap the expense you want to add attendees to. -3. Scroll down to the bottom and tap **More Options**. -4. Tap the attendees field and enter the name or email address of the attendee. - - If the attendee is a member of your workspace, you can select their name from the list. - - If the attendee is not a member of your workspace, enter their full name or email address and press Enter on your keyboard to add them as a new attendee. -5. Tap **Save**. - -Attendees will also be listed on any report that you add the expense to. - -{% include end-option.html %} - -{% include end-selector.html %} - -
    diff --git a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md b/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md deleted file mode 100644 index e8b9ab0eac75..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-mileage-expenses.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Track mileage expenses -description: Add mileage-related expenses ---- - -
    - -You can track your mileage-related expenses by logging your trips in Expensify. You have a couple of different options for logging distance: - -- Web app: - - **Manually create**: Manually enter the number of miles for the trip - - **Create from map**: Automatically determine the trip distance based on the start and end location. -- Mobile app: - - **Manually create**: Manually enter the miles for the trip and your mileage rate - - **Odometer**: Enter your odometer reading before and after the trip - - **Start GPS**: Currently under development and unavailable for use. - -{% include info.html %} -When adding a distance expense, the rates available are determined by the rates set in your workspace rate settings. To update these rates or add a new rate, you must be a Workspace Admin. -{% include end-info.html %} - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} - -1. Click the **Expenses** tab. -2. Click **New Expense**. -3. Select the expense type. - - **Manually create**: - - Enter the number of miles for the trip. - - Select your rate. - - If desired, select the category, add a description, or select a report to add the expense to. - - Click **Save**. - - **Create from map**: - - Add your start location as point A. - - Add your end location as point B. - - If applicable, click **Add Destination** to add additional stops. - - To generate a map receipt, leave the Create Receipt checkbox selected. - - Click **Save**. - - Select your rate. - - If desired, select the category, add a description, or select a report to add the expense to. - - Click **Save**. - -{% include end-option.html %} - -{% include option.html value="mobile" %} - -1. Click the + icon in the top right corner. -2. Under the Distance section, select the expense type. - - **Manually create**: - - Enter your mileage. - - Select your rate. - - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. - - Click **Save**. - - **Odometer**: - - Enter your vehicle’s odometer reading before the trip. - - Enter your vehicle’s odometer reading after the trip. - - Select your rate. - - If desired, click **More Options** to select the category, add a description, or select a report to add the expense to. - - Click **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
    - diff --git a/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md b/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md deleted file mode 100644 index 88dd91997592..000000000000 --- a/docs/articles/expensify-classic/expenses/Track-per-diem-expenses.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Track per diem expenses -description: Add daily allowance expenses for business travel ---- -
    - -A per diem (also called “per diem allowance” or “daily allowance”) is a fixed daily payment provided by an employer to cover expenses during business or work-related travel. These allowances simplify travel expense tracking and reimbursement for meals, lodging, and incidental expenses. - -{% include info.html %} -Before you can add a per diem expense, a Workspace Admin must enable per diem expenses for the workspace and add the per diem rates. If you do not see an option for per diem rates, it is currently unavailable for your workspace, and you’ll need to reach out to one of your Workspace Admins for guidance. -{% include end-info.html %} - -To add a per diem expense, - -1. Click the **Expenses** tab. -2. Click **New Expense** and choose **Per Diem**. -3. Select your travel destination. - - If your trip involves multiple stops, create a separate per diem expense for each destination. -4. Select the start date, end date, start time, and end time for the trip. -5. Select a sub-rate. The available sub-rates are dependent on the trip duration. - - You can include meal deductions or overnight lodging costs if allowed by your workspace. -6. Enter any other required coding information, such as the category, description, or report, and click **Save**. - -# FAQs - -**How do I edit my per diem expenses?** - -Per diem expenses cannot be amended. To make changes, you must delete the expense and recreate it. - -**What if my admin requires daily per diem submissions?** - -No problem! Create a separate per diem expense for each day of your trip. - -
    diff --git a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md index 1e631a53b0b3..b245a26d10a0 100644 --- a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md @@ -50,7 +50,7 @@ If Auto Renew is disabled then the last bill at the annual rate will be issued o # How to downgrade to a free account from an Individual Plan ## Web 1. Log in to your account through a web browser. -1. Go to **Settings > Policies > Individual > Subscription**. +1. Go to **Settings > Workspaces > Individual > Subscription**. 1. Click "Cancel Subscription" to end your Monthly Subscription. Note: Your subscription is a pre-purchase for 30 days of unlimited SmartScanning. This means that when you cancel, you do not get a refund and instead get to use the remainder of the month of unlimited SmartScanning you purchased. diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index 1f412665fc2f..1272cbd1f117 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -2,6 +2,11 @@ title: Request the Card description: Details on requesting the Expensify Card as an employee --- +_Note: The Expensify Card is currently only available to companies that have:_ +_- A US Bank Account_ +_- US documentation_ +_- A private email domain i.e. we cannot provision Expensify cards for users with gmail.com, hotmail.com, yahoo.com etc_ + To start using the Expensify Card, do the following: 1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card. 2. **Request the Card:** diff --git a/docs/articles/expensify-classic/settings/Set-Notifications.md b/docs/articles/expensify-classic/settings/Set-Notifications.md index 0e18d6f22cf5..da55dafb833c 100644 --- a/docs/articles/expensify-classic/settings/Set-Notifications.md +++ b/docs/articles/expensify-classic/settings/Set-Notifications.md @@ -4,72 +4,66 @@ description: This article is about how to troubleshoot notifications from Expens --- # Overview -Sometimes, members may have trouble receiving important email notifications from Expensify, such as Expensify Magic Code emails, account validation emails, secondary login validations, integration emails, or report action notifications (rejections, approvals, etc.). - -# Here's how to troubleshoot missing Expensify notifications: - -1. **No error message, but the email is never received** -The email might be delayed; give it 30-60 minutes to arrive in your inbox. -Check **Email Preferences** on the web via **Settings > Your Account > Preferences**In the **Contact Preferences** section. Ensure that the relevant boxes are checked for the email type you're missing. Check your email spam and trash folders, as Expensify messages might end up there inadvertently. -Check to make sure you haven't unintentionally blocked Expensify emails and whitelist [expensify.com](https://community.expensify.com/home/leaving?allowTrusted=1&target=http%3A%2F%2Fexpensify.com%2F), mg.expensify.com, and [amazonSES.com](https://community.expensify.com/home/leaving?allowTrusted=1&target=http%3A%2F%2Famazonses.com%2F) with your email provider. - -2. **A "We're having trouble emailing you" banner at the top of your screen** -Verify that your email address in your account settings is correct and is a real deliverable email address. -Re-send Verification Email: Look for an option to re-send a verification email, usually provided when this banner appears. - -![ExpensifyHelp_EmailError]({{site.url}}/assets/images/ExpensifyHelp_EmailError.png){:width="100%"} - -# Deep Dive +Sometimes members may have trouble receiving important email notifications from Expensify, such as Expensify Magic Code emails, account validation emails, secondary login validations, integration emails, or report action notifications (rejections, approvals, etc.). + +# Troubleshooting missing Expensify notifications + +## Issue: The email or notification is never received, and no message, banner, or additional context is provided +Emails can sometimes be delayed and could take up to 30-60 minutes to arrive in your inbox. If you're expecting a notification that still hasn't arrived after waiting: + - Check your **Email Preferences** on the web via **Settings > Account > Preferences**. In the **Contact Preferences** section, ensure that the relevant boxes are checked for the email type you're missing. + - Check your email spam and trash folders, as Expensify messages might end up there inadvertently. + - Check to make sure you haven't unintentionally blocked Expensify emails. Allowlist the domain expensify.com with your email provider. + +## Issue: A banner that says “We’re having trouble emailing you” shows the top of your screen. +Confirm the email address on your Expensify account is a deliverable email address, and then click the link in the banner that says "here". If successful, you will see a confirmation that your email was unblocked. + + ![ExpensifyHelp_EmailError]({{site.url}}/assets/images/ExpensifyHelp_EmailError.png){:width="100%"} + + **If unsuccessful, you will see another error:** + - If the new error or SMTP message includes a URL, navigate to that URL for further instructions. + - If the new error or SMTP message includes "mimecast.com", consult with your company's IT team. + - If the new error or SMTP message includes "blacklist", it means your company has configured their email servers to use a third-party email reputation or blocklisting service. Consult with your company's IT team. + +![ExpensifyHelp_SMTPError]({{site.url}}/assets/images/ExpensifyHelp_SMTPError.png){:width="100%"} -**For Private Domains**: +# Further troubleshooting for public domains -If your organization uses a private domain, consult your IT department or IT person to ensure that the following domains are whitelisted to receive our emails: expensify.com, mg.expensify.com, and amazonSES.com. These domains are the sources of various notification emails, so make sure they aren't being blocked. +If you are still not receiving Expensify notifications and have an email address on a public domain such as gmail.com or yahoo.com, you may need to add Expensify's domain expensify.com to your email's allowlist by taking the following steps: -**For Public Domains (e.g., Gmail, Yahoo, Hotmail)**: + - Search for messages from expensify.com in your spam folder, open them, and click “Not Spam” at the top of each message. + - Configure an email filter that identifies Expensify's email domain expensify.com and directs all incoming messages to your inbox, to avoid messages going to spam. + - Add specific known Expensify email addresses such as concierge@expensify.com to your email contacts list. -To whitelist our emails on public email services: +# Further troubleshooting for private domains -1. Check your Spam Folder: Search for messages from expensify.com in your Spam folder, open them, and click "Not Spam" at the top of the message. -2. Create a Filter: Set up a filter that identifies the entire expensify.com domain and directs all incoming messages to your inbox, preventing them from going to Spam. -3. Add Specific Contacts: While optional, adding specific email addresses from Expensify as contacts can further prevent emails from going to Spam. +If your organization uses a private domain, Expensify emails may be blocked at the server level. This can sometimes happen unexpectedly due to broader changes in email provider's handling or filtering of incoming messages. Consult your internal IT team to assist with the following: -Please note that even if you receive emails from our Concierge support communication, ensure that both expensify.com and mg.expensify.com are whitelisted as they use different servers. + - Ensure that the domain expensify.com is allowlisted on domain email servers. This domains is the sources of various notification emails, so it's important it is allowlisted. + - Confirm there is no server-level email blocking and that spam filters are not blocking Expensify emails. Even if you have received messages from our Concierge support in the past, ensure that expensify.com is allowlisted. -**Email Server Blocking**: -Your email server may be blocking our emails due to spam filters or other services. Check with your IT department to investigate and resolve any server-level email blocking issues. +## Companies using Outlook -**Mimecast**: -If your company uses Mimecast, a service that can affect email deliverability, check with your IT department. If Mimecast is in use, reach out to us at concierge@expensify.com through a new email, as this should ensure delivery to your inbox. Mimecast should eventually recognize the Expensify domain, preventing future filtering. +- Add Expensify to your personal Safe Senders list by following these steps: [Outlook email client](https://support.microsoft.com/en-us/office/add-recipients-of-my-email-messages-to-the-safe-senders-list-be1baea0-beab-4a30-b968-9004332336ce) / [Outlook.com](https://support.microsoft.com/en-us/office/safe-senders-in-outlook-com-470d4ee6-e3b6-402b-8cd9-a6f00eda7339) +- **Company IT administrators:** Add Expensify to your domain's Safe Sender list by following the steps here: [Create safe sender lists in EOP](https://learn.microsoft.com/en-us/defender-office-365/create-safe-sender-lists-in-office-365) +- **Company IT administrators:** Add expensify.com to the domain's explicit allowlist. You may need to contact Outlook support for specific instructions, as each company's setup varies. +- **Company administrators:** Contact Outlook support to see if there are additional steps to take based on your domain's email configuration. -**For Outlook Users**: -For Outlook users specifically: +## Companies using Google Workspaces: -1. Click the gear icon in Outlook and select "View all Outlook settings." -2. Choose "Mail" from the settings menu. -3. Under the "Junk email" submenu, click "Add" under "Safe senders and domains." -4. Enter the email address you want to whitelist. -5. Click "Save." +- **Company IT administrators:** Adjust your domain's email allowlist and safe senders lists to include expensify.com by following these steps: [Allowlists, denylists, and approved senders](https://support.google.com/a/answer/60752) -When you click the "Settings" link in the banner in Expensify, you'll be directed to your account settings page, where you may encounter a few different scenarios: +{% include faq-begin.md %} -- "Temporarily Suspended Emails": If the message mentions "temporarily suspended emails to," follow the steps provided in the yellow box. This situation typically occurs when we can't find a valid inbox to send our emails to. Possible reasons include: - - A misspelled email address during account creation. - - Use of a distribution list email (acting as an "alias" email) without a linked inbox. - - An auto-responder that has been responding to our emails for an extended period. -- To resolve this issue, confirm that the email address is indeed associated with an active inbox. Then, click the link that says "here," and your email should be unblocked shortly. -- SMTP Error (Gray Box): In some cases, you might encounter a gray box with an SMTP error message. This error can vary, but it typically looks something like this: +## How can I be sure that emails from Expensify are legitimate and not spam? -![ExpensifyHelp_SMTPError]({{site.url}}/assets/images/ExpensifyHelp_SMTPError.png){:width="100%"} +Expensify's emails are SPF and DKIM-signed, meaning they are cryptographically signed and encrypted to prevent spoofing. -**These look a bit cryptic, yes, but hang in there!** +## Why do legitimate emails from Expensify sometimes end up marked as spam? -The error messages you see are the raw message text received from your email provider's server to Amazon. These messages can vary in text, but the best course of action is to follow the link provided (by copying and pasting) in the text for the next steps. +The problem typically arises when our domain or one of our sending IP addresses gets erroneously flagged by a 3rd party domain or IP reputation services. Many IT departments use lists published by such services to filter email for the entire company. -**Scenario 1**: If the message in the gray box includes "mimecast.com": It means that our emails are being blocked by the server. In this case, you should contact your IT person or team to address the issue. +## What is the best way to ensure emails are not accidentally marked as Spam? -**Scenario 2**: If the message in the gray box mentions "blacklist at org/.com/.net," or resembles the screenshot provided, it indicates that your IT team has configured your email to use a third-party email reputation or blacklisting service. Here's what you need to know: -- All our emails are SPF and DKIM-signed, meaning they are cryptographically signed as coming from us and are not spam. -- The problem arises because we send mail from a cloud-based service. This means that the sender's IP serves multiple vendors, including Expensify. If one of those vendors is marked as spam, it can block all messages from that IP, even if they're from different vendors (including us). -- The better approach is for the server to flag spam via DKIM and SPF (rather than solely relying on the sender's IP address), as our messages are correctly signed and encrypted to prevent spoofing. +For server-level spam detection, the safest approach to allowlisting email from Expensify is to verify DKIM and SPF, rather than solely relying on the third-party reputation of the sending IP address. -To resolve these issues, consider discussing them with your IT team, as they can help implement the necessary changes to ensure you receive our emails without interruption. +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md index 5c146b279163..ca6d9cf52f47 100644 --- a/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md +++ b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md @@ -23,6 +23,16 @@ For every $500 of expenses added, you’ll donate $1 to a related Expensify.org The fund from your Personal Karma is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. +## How do I opt-in to Personal Karma donations? + +You can enable Personal Karma donations from your personal workspace settings. + +- Sign in to your account at www.expensify.com. +- Go to **Settings** > **Workspaces** > click on your **Individual** workspace settings. +- Click Opt-in to Karma donations. + +![Settings > Workspaces > Individual workspace > enable Personal Karma in settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png){:width="100%"} + ## What is Corporate Karma? Corporate Karma is for companies that want to engage in social responsibility. Each month, the donation is calculated based on the total amount of all approved expense reports, including invoices, across all Workspace. @@ -31,12 +41,12 @@ For every $500 your team spends monthly, your company will donate $1 to a relate The fund to which your Corporate Karma goes is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. -{% include faq-begin.md %} - -**How do I opt-in to Personal or Corporate Karma donations?** +## How do I opt-in to Corporate Karma donations? -You can donate Personal and Corporate Karma to Expensify.org in your company or personal workspace settings. +As a [workspace billing owner](https://help.expensify.com/articles/expensify-classic/workspaces/Assign-billing-owner-and-payment-account), you can enable Corporate Karma from the group workspace settings. -Go to **Settings** > **Workspaces** > click on your Individual or Group workspace settings and Opt-in to Karma donations. +- Sign in to your account at www.expensify.com. +- Go to **Settings** > **Workspaces** > **Subscription**. +- Toggle on Karma donations. -{% include faq-end.md %} +![Settings > Workspaces > Group > enable Corporate Karma in subscription settings](https://help.expensify.com/assets/images/ExpensifyHelp_OldDot_Karma_Group.png){:width="100%"} diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md deleted file mode 100644 index 2ae2fcd2426d..000000000000 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Billing and Subscriptions -description: Coming soon ---- - -# Coming Soon diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md new file mode 100644 index 000000000000..f945840d65da --- /dev/null +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md @@ -0,0 +1,6 @@ +--- +title: Billing and Subscriptions +description: An overview of how billing works in Expensify. +--- + +# Coming Soon diff --git a/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md b/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md index 60fdbe94b33b..192f7bf172b6 100644 --- a/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md +++ b/docs/articles/new-expensify/connections/quickbooks-online/Connect-to-QuickBooks-Online.md @@ -56,73 +56,6 @@ Log in to QuickBooks Online and ensure all of your employees are setup as either ![The QuickBooks Online Connect Connect button]({{site.url}}/assets/images/ExpensifyHelp-QBO-5.png){:width="100%"} - - -# Step 3: Configure import settings - -The following steps help you determine how data will be imported from QuickBooks Online to Expensify. - -
      -
    1. Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
    2. -
    3. Review each of the following import settings:
    4. -
        -
      • Chart of accounts: The chart of accounts are automatically imported from QuickBooks Online as categories. This cannot be amended.
      • -
      • Classes: Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding.
      • -
      • Customers/projects: Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding.
      • -
      • Locations: Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding.
      • -{% include info.html %} -As Locations are only configurable as tags, you cannot export expense reports as vendor bills or checks to QuickBooks Online. To unlock these export options, either disable locations import or upgrade to the Control Plan to export locations encoded as a report field. -{% include end-info.html %} -
      • Taxes: Choose whether to import tax rates and defaults.
      • -
      -
    - -# Step 4: Configure export settings - -The following steps help you determine how data will be exported from Expensify to QuickBooks Online. - -
      -
    1. Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
    2. -
    3. Review each of the following export settings:
    4. -
        -
      • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
      • - -{% include info.html %} -* Other Workspace Admins will still be able to export to QuickBooks Online. -* If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. -{% include end-info.html %} - -
      • Date: Choose whether to use the date of last expense, export date, or submitted date.
      • -
      • Export Out-of-Pocket Expenses as: Select whether out-of-pocket expenses will be exported as a check, journal entry, or vendor bill.
      • - -{% include info.html %} -These settings may vary based on whether tax is enabled for your workspace. -* If tax is not enabled on the workspace, you’ll also select the Accounts Payable/AP. -* If tax is enabled on the workspace, journal entry will not be available as an option. If you select the journal entries option first and later enable tax on the workspace, you will see a red dot and an error message under the “Export Out-of-Pocket Expenses as” options. To resolve this error, you must change your export option to vendor bill or check to successfully code and export expense reports. -{% include end-info.html %} - -
      • Invoices: Select the QuickBooks Online invoice account that invoices will be exported to.
      • -
      • Export as: Select whether company cards export to QuickBooks Online as a credit card (the default), debit card, or vendor bill. Then select the account they will export to.
      • -
      • If you select vendor bill, you’ll also select the accounts payable account that vendor bills will be created from, as well as whether to set a default vendor for credit card transactions upon export. If this option is enabled, you will select the vendor that all credit card transactions will be applied to.
      • -
      -
    - -# Step 5: Configure advanced settings - -The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings. - -
      -
    1. Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
    2. -
    3. Select an option for each of the following settings:
    4. -
        -
      • Auto-sync: Choose whether to enable QuickBooks Online to automatically communicate changes with Expensify 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.
      • -
      • Invite Employees: Choose whether to enable Expensify to import employee records from QuickBooks Online and invite them to this workspace.
      • -
      • Automatically Create Entities: Choose whether to enable Expensify to automatically create vendors and customers in QuickBooks Online if a matching vendor or customer does not exist.
      • -
      • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in QuickBooks Online will also show in Expensify as Paid. If enabled, you must also select the QuickBooks Online account that reimbursements are coming out of, and Expensify will automatically create the payment in QuickBooks Online.
      • -
      • Invoice Collection Account: Select the invoice collection account that you want invoices to appear under once the invoice is marked as paid.
      • -
      -
    - {% include faq-begin.md %} **Why do I see a red dot next to my connection?** diff --git a/docs/articles/new-expensify/connections/xero/Configure-Xero.md b/docs/articles/new-expensify/connections/xero/Configure-Xero.md index 218e81c98707..b417d6169a1e 100644 --- a/docs/articles/new-expensify/connections/xero/Configure-Xero.md +++ b/docs/articles/new-expensify/connections/xero/Configure-Xero.md @@ -1,11 +1,75 @@ --- title: Configure Xero -description: Coming soon +description: How to configure your settings for Xero --- + +To configure your Xero settings, complete the steps below. -# FAQ +# Step 1: Configure import settings -## How do I know if a report successfully exported to Xero? +The following steps help you determine how data will be imported from Xero to Expensify. + +
      +
    1. Under the Accounting settings for your workspace, click Import under the Xero connection.
    2. +
    3. Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
    4. +
        +
      • Xero organization: Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
      • +
      • Chart of Accounts: Your Xero chart of accounts and any accounts marked as “Show In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
      • +
      • Tracking Categories: Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
      • +
      • Re-bill Customers: When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
      • +
      • Taxes: Choose whether to import tax rates and tax defaults from Xero.
      • +
      +
    + +# Step 2: Configure export settings +The following steps help you determine how data will be exported from Expensify to Xero. + +
      +
    1. Under the Accounting settings for your workspace, click Export under the Xero connection.
    2. +
    3. Review each of the following export settings:
    4. +
        +
      • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
      • +
      +
    +{% include info.html %} +- Other Workspace Admins will still be able to export to Xero. +- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. +{% include end-info.html %} + +
      +
        +
      • Export Out-of-Pocket Expenses as: All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
      • +
      • Purchase Bill Date: Choose whether to use the date of the last expense, export date, or submitted date.
      • +
      • Export invoices as: All invoices exported to Xero will be as sales invoices. This cannot be amended.
      • +
      • Export company card expenses as: All company card expenses are exported to Xero as bank transactions. This cannot be amended.
      • +
      • Xero Bank Account: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
      • +
      +
    + +# Step 3: Configure advanced settings + +The following steps help you determine the advanced settings for your connection, like auto-sync. + +
      +
    1. Under the Accounting settings for your workspace, click Advanced under the Xero connection.
    2. +
    3. Select an option for each of the following settings:
    4. +
        +
      • Auto-sync: Choose whether to enable Xero to automatically communicate changes with Expensify 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. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
      • +
      • Set Purchase Bill Status: Choose the status of your purchase bills:
      • +
          +
        • Draft
        • +
        • Awaiting Approval
        • +
        • Awaiting Payment
        • +
        +
      • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
      • +
      • Xero Bill Payment Account: If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
      • +
      • Xero Invoice Collections Account: If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
      • +
      +
    + +{% include faq-begin.md %} + +## How do I know if a report is successfully exported to Xero? When a report exports successfully, a message is posted in the related Expensify Chat room. @@ -23,3 +87,5 @@ When an admin manually exports a report, Expensify will warn them if the report - If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync. - If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync. - If a report has not yet been exported to Xero, it won’t be automatically exported. + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md deleted file mode 100644 index 26634d9a33df..000000000000 --- a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Approve and Pay Expenses -description: Approve, hold, or pay expenses submitted to you ---- -
    - -As a workspace admin, you can set an approval workflow for the expenses submitted to you. Expenses can be, - -- Instantly submitted without needing approval. -- Submitted at a desired frequency (daily, weekly, monthly) and follow an approval workflow. - -**Setting approval workflow and submission frequencies** - -Approval workflow settings and submission frequencies can be set in the Workflow settings of your workspace. - -# Manually approve expense - -When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. - -{% include info.html %} -If an expense is sent to you by a friend, you will not need to approve the expense. Instead, you can immediately pay the expense when you are ready. -{% include end-info.html %} - -# Approve expenses - -To approve an expense, - -1. Open the Expensify Chat thread for the expense. -2. Click the expense or group of expenses. -3. Review the expense details to ensure they are correct. Look at each receipt, the amount, the description, and any additional details. -4. Determine the next steps. - - **Approve**: When you’re satisfied with the expense, click **Approve**. - - **Handle holds**: If any of the expenses are on hold, you can choose to either approve only the expenses that are not on hold or approve the full amount, including any held expenses. - - **Request changes**: You can add a comment to the expense’s chat thread in your Expensify Chat inbox to request changes to the expense details. - -{% include info.html %} -If the transaction is pending (a common occurrence with recent company card expenses or SmartScan expenses), you’ll need to wait until the transaction posts before approving it. -{% include end-info.html %} - -![The approve button in an expense]({{site.url}}/assets/images/ExpensifyHelp_ApproveExpense_1.png){:width="100%"} - -![The approve button when you click into the expense]({{site.url}}/assets/images/ExpensifyHelp_ApproveExpense_2.png){:width="100%"} - -You’re now ready to pay the expense. - -# Hold an expense - -If you need to delay a payment or if you need more information on the expense before it can be approved, you can hold the expense. - -To hold an expense, - -1. Open the Expensify Chat thread for the expense. -2. Click the expense or group of expenses. -3. Click the three dot menu at the top right of the expense and select **Hold**. -4. Enter a reason for the delay. -5. Review the Hold Overview page and click **Got It**. - -When you’re ready, you can choose to: -- **Remove the hold**: Complete the steps above and select **Unhold**. -- **Approve the expense**: Complete the steps above for “Approve expenses.” -Once the expense has been approved, you can now pay the expense. - -{% include info.html %} -Held expenses will not be available for payment until they have been approved. -{% include end-info.html %} - -# Unapprove an expense - -Some details of approved expenses and reports cannot be edited. If you need to edit an expense that has been approved, admins and the last approver have the option to unapprove reports. - -1. Click the workspace logo in the top left corner. -2. Select the workspace associated with the expense report. -3. Find the approved report by searching for the submitter. -4. Click the dropdown arrow at the top of the report to view the report actions. -5. Click **Unapprove**. - -The unapproved report will return to an editable state, and the submitter will receive an email and chat notification that the expense has been unapproved. - -{% include info.html %} -Reports that have been paid cannot be unapproved. If the approved expense has already been exported to an accounting package, you’ll see a warning that unapproving an expense can cause data discrepancies and Expensify Card reconciliation issues. Ideally, you’ll want to delete the data that has already been exported to the accounting package before approving the expense again. -{% include end-info.html %} - -# Pay expenses - -Once you’ve approved an expense—or if the expense does not require approval—you’ll be able to pay it. - -{% include info.html %} -To pay expenses within Expensify, you’ll need to [set up your Expensify Wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet). -{% include end-info.html %} - -To pay an expense, - -1. Open the Expensify Chat thread for the expense. -2. Click the expense or group of expenses. -3. Select a payment option. - - Click **Pay** to pay the full expense within Expensify. If the expenses contain one that has been held, the pay amount will only include the expenses that have not been held. Then you’ll select your payment method. - - Click **Pay Elsewhere** to indicate that a payment has been sent using a method outside of Expensify, such as cash or a check. This will label the expense as Paid. - -# FAQ - -**Why was an expense automatically approved?** - -We refer to this as **Instant Submit**. If a workspace doesn’t have Delayed Submission enabled, an expense report will automatically be submitted. - -**Why is an employee expense showing as ‘pending?’** - -An Expensify Card expense will show as pending if the merchant hasn’t posted it. This is usually the case with hotel holds, or card rental holds. A hold will normally last no more than 7-10 business days unless it’s a hotel hold, which can last 31 days. - -
    diff --git a/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md new file mode 100644 index 000000000000..77587cc124f0 --- /dev/null +++ b/docs/articles/new-expensify/expenses-&-payments/Approve-expenses.md @@ -0,0 +1,139 @@ +--- +title: Approve Expenses +description: Approve, hold, and unapprove submitted expenses +--- +
    + +Expenses can be created through manual entry, tracking distance, or scanning a receipt. They can be submitted to an individual or a workspace. + +This help article has more details about creating and submitting an expense to an individual or a workspace. + +# Receiving an expense from an Individual + +When an expense is submitted to an individual, it doesn’t need approval. It only needs to be paid. + +This help article has the steps to pay the expense. + +# Receiving a workspace expense + +When an expense is submitted to a workspace with an “approval workflow”, it must be approved before it can be paid. + +As a workspace admin, you can set an [approval workflow](https://help.expensify.com/articles/new-expensify/workspaces/Add-approvals) in the workspace settings. For each expense report, you’ll have the option to: + +- **Approve:** Click Approve if you’re satisfied with the expense details. +- **Hold the expense:** If you need to delay a payment or provide more information before approval, you can hold an expense. +- **Unapprove the expense:** You can return the expense to the submitter for revisions. + +# Approve workspace expenses + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop or WebApp" %} +1. When an expense is submitted, you will receive an email and in-app notification with the details of the expense. +2. Click the expense in the email to be directed to New Expensify, where you can review it. +3. Click on the expense to view the receipt, amount, description, and additional details the submitter provides. +4. Click **Approve**. +5. When you are ready to pay the expense, follow the steps in this help article. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. When an expense is submitted, you will receive a text message and in-app notification with its details. +2. Tap on the expense in the text or notification to be directed to New Expensify, where you can review it. +3. Tap on the expense to view the receipt, amount, description, and any additional details the submitter provides. +4. Tap **Approve**. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include info.html %} +If the transaction is pending (a common occurrence with recent company cards or SmartScan expenses), you’ll need to wait until the transaction posts before approving it. +{% include end-info.html %} + + +# Hold a workspace expense + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Locate the expense on the **Search** page. +2. Click **View**. +3. Click the drop-down arrow at the top of the expense. +4. Click the **Hold** button. +5. Enter a reason for the delay. The reason for the hold will be added to the expense report. + +

     

    + +When you’re ready to remove the hold, + +1. Locate the expense on the Search page. +2. Click **View**. +3. Click the drop-down arrow at the top of the expense. +4. Select **UnHold**. +5. Complete the steps above to “Approve expenses.” Once the expense has been approved, you can pay it. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Locate the expense on the **Search** page. +2. Tap **View**. +3. Tap the drop-down arrow at the top of the expense. +4. Select the **Hold** button. +5. Enter a reason for the delay. The reason for the hold will be added to the expense report. + +

     

    + +When you’re ready to remove the hold, + +1. Tap **Search** and select the expense. +2. Tap the drop-down arrow at the top of the expense. +3. Select **UnHold**. +4. Complete the steps above to “Approve expenses.” Once the expense has been approved, you can pay it. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include info.html %} +Held expenses will not be available for payment until they have been approved. +{% include end-info.html %} + +# Unapprove a workspace expense + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Locate the expense on the **Search** page. +2. Click **View**. +3. Click the drop-down arrow at the top of the report +4. Click **Unapprove**. +5. The submitter will receive an email and in-app notification that the expense has been unapproved. +6. An unapproved expense can be deleted by clicking the drop-down arrow at the top of the expense. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Locate the expense on the **Search** page. +2. Tap **View**. +3. Tap the drop-down arrow at the top of the expense. +4. Tap **Unapprove**. +5. The submitter will receive a text and in-app notification that the expense has been unapproved. +6. An unapproved expense can be deleted by clicking the drop-down arrow at the top of the expense. +{% include end-option.html %} + +{% include end-selector.html %} + +Reports that have been paid cannot be unapproved. + +If the approved expense has already been exported to an accounting package, you’ll see a warning that unapproving an expense can cause data discrepancies and Expensify Card reconciliation issues. Ideally, you’ll want to delete the data already exported to the accounting package before approving the expense again. + +{% include faq-begin.md %} + +**Why is an employee expense showing as ‘pending?’** + +An Expensify Card expense will show as pending if the merchant hasn’t posted it. This is usually the case with hotel holds, or card rental holds. A hold will normally last no more than 7-10 business days unless it’s a hotel hold, which can last 31 days. + +**What are expense reports?** + +In Expensify, expense reports group expenses in a batch to be paid or reconciled. When a draft report is open, all new expenses are added to it. + +Once a report is submitted, you can track the status from the **Search** section. Click the **View** button for a specific expense or expense report. The status is displayed at the top of the expense or report. +{% include faq-end.md %} + +
    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 cf6a13f9d5ac..38f1e0fdd466 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -1,68 +1,83 @@ --- title: Create an expense -description: Request payment from an employer or a friend +description: How to create an expense as an individual or workspace member redirect_from: articles/request-money/Request-and-Split-Bills/ ---
    -You can create an expense to request payment from an employer’s workspace or from a friend using any of the following options: -- **SmartScan**: Take a picture of a receipt to capture the expense details automatically. -- **Add manually**: Manually enter the expense details. -- **Create a distance expense**: Capture mileage expenses by entering the addresses you traveled to. Expensify automatically calculates the distance, the rate per mile, and the total cost. +Expenses can be created through SmartScanning a receipt, emailing a receipt, tracking distance, and manually creating an expense. + +They can be submitted to an individual or a workspace. Before we outline the steps to create an expense, let’s go over the reasons to send an expense to an individual or a workspace. + +# Sending an expense to an Individual + +If you use Expensify for personal use, submitting to an individual is likely best. + +Once the expense is created, you will see the option to send it to an email or phone number. Alternatively, add an expense to a chat, which will go straight to the person you are chatting with. + +When an expense is submitted to an individual’s email or phone number, the payor will receive an email or text notification with the amount that needs to be paid. They can click on the amount in the email or text to pay the expense. + +# Submit an expense to a workspace or employer + +If you are an employee or a workspace member, you should submit the expense to the workspace instead of an individual. A workspace is designed to code expenses to the company's requirements. + +When an expense is submitted to a workspace, your approver will receive an email or text notification prompting them to approve and pay it. + +# How to Create an Expense # SmartScan a receipt {% include selector.html values="desktop, mobile" %} -{% include option.html value="desktop" %} -1. Click the + icon in the bottom left menu and select **Submit Expense**. +{% include option.html value="desktop or WebApp" %} +1. Click the **Global Create** button and select **Submit Expense**. 2. Click **Scan**. -3. Drag and drop the receipt into Expensify, or click **Choose File** to select it from your saved files. *Note: The SmartScan process will auto-populate the merchant, date, and amount.* -4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -5. Add a description, category, tags, or tax as desired, or as required by your workspace. +3. You can drag and drop the receipt into Expensify or click **Choose File** to select it from your saved files. _The SmartScan process will auto-populate the merchant, date, and amount._ +4. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +5. Add a description, category, tags, or tax as desired or as required by your workspace. 6. (Optional) Enable the expense as billable if it should be billed to a client. -7. Click **Submit Expense**. +7. Click **Submit expense**. {% include end-option.html %} {% include option.html value="mobile" %} -1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +1. ​​Tap the **Global Create** button and select **Submit Expense**. 2. Tap **Scan**. -3. Tap the green button to take a photo of a receipt, or tap the Image icon to the left of it to upload a receipt from your phone. *Note: The SmartScan process will auto-populate the merchant, date, and amount.* -4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -5. Add a description, category, tags, or tax as desired, or as required by your workspace. +3. Tap the green button to take a photo of a receipt, or tap the Image icon to upload a receipt from your phone. _The SmartScan process will auto-populate the merchant, date, and amount._ +4. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +5. Add a description, category, tags, or tax as desired or as required by your workspace. 6. (Optional) Enable the expense as billable if it should be billed to a client. -7. Tap **Submit**. +7. Tap **Submit expense**. {% include end-option.html %} {% include end-selector.html %} {% include info.html %} -You can also forward receipts to receipts@expensify.com using an email address that is your primary or secondary email address. SmartScan will automatically pull all of the details from the receipt and add it to your expenses. +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 %} # Manually add an expense {% include selector.html values="desktop, mobile" %} -{% include option.html value="desktop" %} -1. Click the + icon in the bottom left menu and select **Submit Expense**. +{% include option.html value="desktop or WebApp" %} +1. Click the **Global Create** button and select **Submit Expense**. 2. Click **Manual**. -3. Enter the amount on the receipt and click **Next**. *Note: Click the currency symbol to select a different currency.* -4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -5. (Optional) Add a description. -6. Add a merchant. -7. Click **Show more** to add additional fields (like a category) as desired, or as required by your workspace. +3. Enter the currency and amount. +4. Click **Next**. +5. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +6. Add a description, category, tags, or tax as desired or as required by your workspace. Click **Show More** to see all coding options. +7. (Optional) Enable the expense as billable if it should be billed to a client. 8. Click **Submit**. {% include end-option.html %} {% include option.html value="mobile" %} -1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +1. Tap the **Global Create** button and select **Submit Expense**. 2. Tap **Manual**. -3. Enter the amount on the receipt and tap **Next**. *Note: Click the currency symbol to select a different currency.* -4. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -5. (Optional) Add a description. -6. Add a merchant. -7. Tap **Show more** to add additional fields (like a category) as desired, or as required by your workspace. +3. Enter the currency and amount. +4. Tap **Next**. +5. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +6. Add a description, category, tags, or tax as desired or as required by your workspace. Tap **Show More** to see all coding options. +7. (Optional) Enable the expense as billable if it should be billed to a client. 8. Tap **Submit**. {% include end-option.html %} @@ -72,54 +87,65 @@ You can also forward receipts to receipts@expensify.com using an email address t {% include selector.html values="desktop, mobile" %} -{% include option.html value="desktop" %} -1. Click the + icon in the bottom left menu and select **Submit Expense**. +{% include option.html value="desktop or WebApp" %} +1. Click the **Global Create** button and select **Submit Expense**. 2. Click **Distance**. 3. Click **Start** and enter the starting location of your trip. -4. Click **Stop** and enter the ending location of your trip. -5. (Optional) Click **Add stop** to add additional stops, if applicable. +4. Click **Stop** and enter the ending location of your trip. +5. (Optional) Click **Add Stop** to add additional stops, if applicable. Drag and drop on the parallel lines (=) to reorder the stops if needed. 6. Tap **Next**. -7. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -8. (Optional) Add a description. -9. Click **Submit**. +7. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +8. Add a description, category, tags, or tax as desired or as required by your workspace. Click **Show More** to see all coding options. +9. (Optional) Enable the expense as billable if it should be billed to a client. +10. Click **Submit**. {% include end-option.html %} {% include option.html value="mobile" %} -1. Tap the + icon at the bottom of the screen and select **Submit Expense**. +1. Tap the **Global Create** button and select **Submit Expense**. 2. Tap **Distance**. 3. Tap **Start** and enter the starting location of your trip. -4. Tap **Stop** and enter the ending location of your trip. -5. (Optional) Tap **Add stop** to add additional stops, if applicable. -6. Tap **Next**. -7. Use the search field to find the desired workspace or an individual’s name, email, or phone number. -8.(Optional) Add a description. -9. Tap **Submit**. +4. Tap **Stop** and enter the ending location of your trip. +5. (Optional) Tap **Add Stop** to add additional stops, if applicable. Drag and drop on the parallel lines (=) to reorder the stops if needed. +6. Tap Next. +7. Enter the desired workspace or an individual’s email or phone number to receive the expense report. +8. Add a description, category, tags, or tax as desired or as required by your workspace. Tap **Show More** to see all coding options. +9. (Optional) Enable the expense as billable if it should be billed to a client. +10. Click **Submit**. {% include end-option.html %} {% include end-selector.html %} -# Next Steps +# Next Steps for expenses sent to an Individual -The next steps for the expense depend on whether it was submitted to a workspace or to an individual: -- **Expenses submitted to a workspace** are automatically added to a report and checked for any violations or inconsistencies. A chat thread for the expense is also added to your chat inbox. When you open the chat, the top banner will show the expense status and any next steps. By default, reports are automatically submitted for approval every Sunday. However, if it is ready for early submission, you can manually submit a report for approval. Once a report is submitted, your approver will be prompted to review your expense report. If changes are required, you will receive a notification to resolve any violations and resubmit. You will also be notified once your approver approves or denies your expenses. -- **Expenses submitted to a friend** are sent right to that individual via email or text. You can chat with them about the expense in Expensify Chat, and you can receive payments through your Expensify Wallet or outside of Expensify. +- Expenses submitted to an individual are instantly sent. +- The payer will receive an email or text prompting them to review and pay the expense. +- You can chat with the paying individual in Expensify. +- Make sure to [connect your personal bank account](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account) to receive payment. -{% include faq-begin.md %} -**Can I divide a payment between multiple people?** +# Next Steps for expense sent to a workspace -Yes, you can split an expense to share the cost between multiple people. +- Expenses submitted to a workspace are automatically added to a report and checked for violations or inconsistencies. +- You can view the details and status of the expense on the **Search** tab. +- Workspace settings determine the frequency of report submission. However, if the report is ready for early submission, you can manually submit a report for approval. +- Once a report is submitted, your approver will get an email or text to review and pay the expense. +- If changes are required, you will receive a notification to fix the expense and resubmit. +- You will also be notified once your approver approves or denies your expenses. +- Make sure to [connect your personal bank account](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account) to receive payment. -**Can I pay someone in another currency?** +{% include faq-begin.md %} +**Can I divide a payment between multiple people?** -While you can record your expenses in different currencies, Expensify wallets are only available for members who can add a U.S. personal bank account. +Yes, you can [split an expense](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Split-an-expense) in a group chat. **Can I change an expense once I’ve submitted it?** -Yes, you can edit an expense until it is paid. When an expense is submitted to a workspace, you, your approvers, and admins can edit the details on an expense except for the amount and date. +Yes, you can edit an expense until it is paid. When an expense is submitted, the details can be edited except for the amount and date. **What are expense reports?** -In Expensify, expenses are submitted on an expense report. When a draft report is open, all new expenses are added to the draft report. Once a report is submitted, it shows what stage of the approval process the expenses are in and any required next steps. +In Expensify, expense reports group expenses in a batch to be paid or reconciled. When a draft report is open, all new expenses are added to it. + +Once a report is submitted, you can track the status from the **Search** section. Click the **View** button for a specific expense or expense report. The status is displayed at the top of the expense or report. {% include faq-end.md %}
    diff --git a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md index 8fffec75e744..782e939e991e 100644 --- a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md +++ b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md @@ -12,7 +12,7 @@ When you upgrade the Expensify Cards to the new program, you'll have access to e - Unique naming for each virtual card for simplified expense categorization {% include info.html %} -The Expensify Card upgrade must be completed by November 1, 2024. +The Expensify Card upgrade must be completed by December 1, 2024. {% include end-info.html %} # Upgrade your company’s Expensify Card program diff --git a/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md index 6c7457641ce6..8915778962a0 100644 --- a/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md +++ b/docs/articles/new-expensify/expensify-card/Use-your-Expensify-Card.md @@ -4,13 +4,13 @@ description: Use your physical or virtual Expensify Card ---
    -As soon as you receive your physical Expensify Visa® Commercial Card, you can start using it right away by swiping it like you would with any other card, or you can link your card to your Apple or Google Pay mobile wallet to make in-person, contactless payments. You can also use your virtual Expensify Card for online and in-app purchases. +As soon as you receive your physical Expensify Visa® Commercial Card, you can start using it right away by swiping it like you would with any other card. You can also link your card to your Apple or Google Pay mobile wallet to make in-person, contactless payments. You can also use your virtual Expensify Card for online and in-app purchases. A virtual card is a digital card that can be used for online transactions. Virtual cards have the same details as physical cards, but they offer several additional benefits: -- **Flexibility**: Virtual cards can be created or deleted instantly. You can use them for individual transactions with predetermined amounts or recurring payments and subscriptions. +- **Flexibility:** Virtual cards can be created or deleted instantly. They can be used for individual transactions with predetermined amounts or recurring payments and subscriptions. - **Customizable limits**: You can set spending limits for each virtual card. -- **Security**: Admins have the option to issue virtual cards for a single-use (e.g. for one of expenses) or fixed-use (e.g. for recurring expenses). Since you have placed a limit on their usage, it makes them less susceptible to unauthorized transactions. -- **Insights**: You can easily track recurring spend for specific vendors when assigning a virtual card to a team, department, or vendor. +- **Security**: Admins have the option to issue virtual cards for a single-use (e.g., for one of the expenses) or fixed-use (e.g., for recurring expenses). Since you have placed a limit on their usage, it makes them less susceptible to unauthorized transactions. +- **Insights**: When assigning a virtual card to a team, department, or vendor, you can easily track recurring spending for specific vendors. # View your virtual card details @@ -34,7 +34,7 @@ A virtual card is a digital card that can be used for online transactions. Virtu {% include faq-begin.md %} -**Why did my transaction get declined?** +## Why did my transaction get declined? Here are some reasons why an Expensify Card transaction might be declined: @@ -43,7 +43,13 @@ Here are some reasons why an Expensify Card transaction might be declined: - **Incorrect card details**: Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines. There was suspicious activity - **Fraudulent or risky activity**: If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. This could happen due to irregular spending patterns, attempted purchases from risky vendors, or multiple rapid transactions. Check your Expensify Home page to approve unusual merchants and try again. If the spending looks suspicious, we may complete a manual due diligence check, and our team will do this as quickly as possible - your cards will all be locked while this happens. The merchant is located in a restricted country -**How do I report my Expensify Card expenses?** +## Where can I use my Expensify Card? + +Generally, the Expensify Card can be used anywhere Visa is accepted. However, the Expensify Card program is based in the US, so we are bound by US sanctions and other international limitations. + +Expensify Card purchases will be declined if a merchant is physically located in, or has its headquarters or billing address, in the following countries -- Belarus, Burundi, Cambodia, Central African Republic, Democratic Republic of the Congo, Cuba, Iran, Iraq, North Korea, Lebanon, Libya, Russia, Somalia, South Sudan, Syrian Arab Republic, Tanzania, Ukraine, Venezuela, Yemen, Zimbabwe + +## How do I report my Expensify Card expenses? You can report and submit Expensify Card expenses just like any other expenses, and you’ll want to submit them regularly to ensure you have a sufficient spending amount available on the card. As your expenses are approved, your Smart Limit updates accordingly. diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md index df77ed3b5b01..f2fd6970f5af 100644 --- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md +++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md @@ -30,7 +30,7 @@ To require workspace members to add tags and/or categories to their expenses, {% include end-selector.html %} -![In the Workspace > Categories setting, the right-hand panel is open and the toggle to require categories on expenses is highlighted.]({{site.url}}/assets/images/workspace_category_toggle.png){:width="100%"} +![In the Workspace, Categories setting, the right-hand panel is open and the toggle to require categories on expenses is highlighted.]({{site.url}}/assets/images/Workspace_category_toggle.png){:width="100%"} This will highlight the tag and/or category field as required on all expenses. diff --git a/docs/assets/images/ExpensifyHelp_OldDot_Karma_Group.png b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Group.png new file mode 100644 index 000000000000..e0d5d406ba2f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Group.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png new file mode 100644 index 000000000000..d3115469350f Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_Karma_Individual.png differ diff --git a/docs/assets/images/SageConfigureIntegrationConfigureButton.png b/docs/assets/images/SageConfigureIntegrationConfigureButton.png new file mode 100644 index 000000000000..e3ec52bacbb0 Binary files /dev/null and b/docs/assets/images/SageConfigureIntegrationConfigureButton.png differ diff --git a/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png b/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png new file mode 100644 index 000000000000..f126bb10dc51 Binary files /dev/null and b/docs/assets/images/SageConfigureUserDefinedDimensionsFilter.png differ diff --git a/docs/assets/images/SageConnectCreatingWorkspace.png b/docs/assets/images/SageConnectCreatingWorkspace.png new file mode 100644 index 000000000000..6084d0a8c7fb Binary files /dev/null and b/docs/assets/images/SageConnectCreatingWorkspace.png differ diff --git a/docs/assets/images/SageConnectEnableSage.png b/docs/assets/images/SageConnectEnableSage.png new file mode 100644 index 000000000000..25b43a510c15 Binary files /dev/null and b/docs/assets/images/SageConnectEnableSage.png differ diff --git a/docs/assets/images/SageConnectEnterCredentials.png b/docs/assets/images/SageConnectEnterCredentials.png new file mode 100644 index 000000000000..63772972290d Binary files /dev/null and b/docs/assets/images/SageConnectEnterCredentials.png differ diff --git a/docs/assets/images/SageConnectSettingUpWebServicesUser.png b/docs/assets/images/SageConnectSettingUpWebServicesUser.png new file mode 100644 index 000000000000..0fd3bb68c3d2 Binary files /dev/null and b/docs/assets/images/SageConnectSettingUpWebServicesUser.png differ diff --git a/docs/assets/images/SageConnectSubscriptionSettings.png b/docs/assets/images/SageConnectSubscriptionSettings.png new file mode 100644 index 000000000000..2e74d27c71e6 Binary files /dev/null and b/docs/assets/images/SageConnectSubscriptionSettings.png differ diff --git a/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png b/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png new file mode 100644 index 000000000000..8750c1ed596b Binary files /dev/null and b/docs/assets/images/SageConnectTimeandExpenseSequenceNumbers.png differ diff --git a/docs/assets/images/SageConnectWebServicesAuthorizations.png b/docs/assets/images/SageConnectWebServicesAuthorizations.png new file mode 100644 index 000000000000..d0b9a786d1cc Binary files /dev/null and b/docs/assets/images/SageConnectWebServicesAuthorizations.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index d3672618cfad..d9f18ebb0227 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -590,3 +590,11 @@ https://help.expensify.com/articles/expensify-classic/articles/expensify-classic https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Bulk-Upload-Multiple-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Add-Invoices-in-Bulk https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription +https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page +https://help.expensify.com/articles/expensify-classic/expenses/Add-expenses-in-bulk,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-mileage-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://help.expensify.com/articles/expensify-classic/expenses/Track-per-diem-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense +https://community.expensify.com/discussion/5116/faq-where-can-i-use-the-expensify-card,https://help.expensify.com/articles/new-expensify/expensify-card/Use-your-Expensify-Card#where-can-i-use-my-expensify-card +https://help.expensify.com/articles/other/Expensify-Lounge,https://help.expensify.com/Hidden/Expensify-Lounge +https://help.expensify.com/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Approve-expenses diff --git a/fastlane/Appfile b/fastlane/Appfile index 66955822aab7..42f887a827d1 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,5 +1,4 @@ -app_identifier("com.chat.expensify.chat") # The bundle identifier of your app -apple_id("ios@expensify.com") # Your Apple email address - -itc_team_id("152696") # App Store Connect Team ID -team_id("368M544MTT") # Developer Portal Team ID +# See https://docs.fastlane.tools/advanced/Appfile/ +apple_id("ios@expensify.com") +itc_team_id("152696") +team_id("368M544MTT") diff --git a/fastlane/Fastfile b/fastlane/Fastfile index eed84acdc916..e90fdbe50255 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,6 +68,23 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a production HybridApp AAB" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + gradle( + project_dir: '../Android', + task: "bundleRelease", + flags: "--refresh-dependencies", + properties: { + "android.injected.signing.store.file" => './upload-key.keystore', + "android.injected.signing.store.password" => ENV["ANDROID_UPLOAD_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["ANDROID_UPLOAD_KEYSTORE_ALIAS"], + "android.injected.signing.key.password" => ENV["ANDROID_UPLOAD_KEY_PASSWORD"], + } + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK" lane :build_local do ENV["ENVFILE"]=".env.production" @@ -80,6 +97,18 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a new local HybridApp APK" + lane :build_local_hybrid do + ENV["ENVFILE"]=".env.production" + gradle( + project_dir: '../Android', + task: 'assemble', + flavor: 'Production', + build_type: 'Release', + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK for e2e testing" lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" @@ -151,6 +180,38 @@ platform :android do ) 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 + ENV["SUPPLY_UPLOAD_MAX_RETRIES"]="5" + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + aab: ENV[KEY_GRADLE_AAB_PATH], + track: 'alpha', + rollout: '1.0' + ) + + # Update the internal testing group "beta" with the latest version + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + track: 'alpha', + track_promote_to: 'beta', + skip_upload_aab: true + ) + + # Update the internal testing group "Internal Testers" with the latest version + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + track: 'alpha', + track_promote_to: 'Internal Testers', + skip_upload_aab: true + ) + + end + desc "Deploy app to Google Play production" lane :upload_google_play_production do # Google is very unreliable, so we retry a few times @@ -228,6 +289,37 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp production build" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AppStore.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AppStore_Share_Extension.mobileprovision" + ) + + build_app( + workspace: "../iOS/Expensify.xcworkspace", + scheme: "Expensify", + output_name: "Expensify.ipa", + export_method: "app-store", + export_options: { + manageAppVersionAndBuildNumber: false, + provisioningProfiles: { + "com.expensify.expensifylite" => "(OldApp) AppStore", + "com.expensify.expensifylite.SmartScanExtension" => "(OldApp) AppStore: Share Extension" + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -238,6 +330,16 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an unsigned iOS HybridApp production build" + lane :build_unsigned_hybrid do + ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + build_app( + workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + scheme: "Expensify" + ) + setIOSBuildOutputsInEnv() + end + desc "Build AdHoc app for testing" lane :build_adhoc do ENV["ENVFILE"]=".env.adhoc" @@ -286,6 +388,7 @@ platform :ios do desc "Upload app to TestFlight" lane :upload_testflight do upload_to_testflight( + app_identifier: "com.chat.expensify.chat", api_key_path: "./ios/ios-fastlane-json-key.json", distribute_external: true, notify_external_testers: true, @@ -316,9 +419,46 @@ platform :ios do ) end - desc "Submit app to App Store Review" + desc "Upload HybridApp to TestFlight" + lane :upload_testflight_hybrid do + upload_to_testflight( + app_identifier: "com.expensify.expensifylite", + api_key_path: "./ios/ios-fastlane-json-key.json", + distribute_external: true, + notify_external_testers: true, + reject_build_waiting_for_review: true, + changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", + groups: ["Applause", "Beta Testers", "Expensify Employees"], + demo_account_required: true, + beta_app_review_info: { + contact_email: ENV["APPLE_CONTACT_EMAIL"], + contact_first_name: "Andrew", + contact_last_name: "Gable", + contact_phone: ENV["APPLE_CONTACT_PHONE"], + demo_account_name: ENV["APPLE_DEMO_EMAIL"], + demo_account_password: ENV["APPLE_DEMO_PASSWORD"], + notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me' + 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above + 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify' + 4. Open the email and copy the 6-digit sign-in code provided within + 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field" + } + ) + + puts "dsym path: #{ENV[KEY_DSYM_PATH]}" + upload_symbols_to_crashlytics( + app_id: "1:1008697809946:ios:3ffad71f664f2886", + dsym_path: ENV[KEY_DSYM_PATH], + gsp_path: "./ios/GoogleService-Info.plist", + # Assuming we are running this from the react-native submodule directory for HybridApp + binary_path: "../iOS/Pods/FirebaseCrashlytics/upload-symbols" + ) + end + + desc "Submit app for production App Store Review" lane :submit_for_review do deliver( + app_identifier: "com.chat.expensify.chat", api_key_path: "./ios/ios-fastlane-json-key.json", # Skip HTMl report verification diff --git a/help/GUIDELINES.md b/help/GUIDELINES.md new file mode 100644 index 000000000000..7fbf693e6830 --- /dev/null +++ b/help/GUIDELINES.md @@ -0,0 +1,150 @@ +# New Help Guidelines +This file outlines a series of specific rules. Whenever editing any file on this site, please verify your changes comply with these rules. + +## General Philosophy +In general, this help site is built around a few common principles: + +* **Consistency** - Every page of the site should follow a common pattern, as should every chapter on the page, and every section in the chapter +* **Focus** - Every section should focus as much as possible on a single self-contained subset of the page, with complex subsets being broken into section groups rather than large singular sections +* **Plain language** - All writing should target a high-school reading level, with very common language and simple phrasings. + + +## Structure Rules +To avoid ambiguity, let's establish the following terms: + +* **Site** - All of the pages combine to create a single help "site" providing comprehensive details on the Expensify Superapp, which is a collection of multiple products combined into a single app. + +* **Page** - Each help "page" is devoted to a single product within a tightly integrated suite. Accordingly, while each product page can refer to other products, each product page should only provide detailed definitions on a single product to avoid redundancy between product pages. Each product is split into multiple + +* **Chapter** - Each page is split into a standard set of "chapters", each of which contains multiple sections. + +* **Section** - Each chapter has three or more "sections", consisting of a header and body. +[Fr +* **Header** - Each section has a "header", which describes the contents of that section. + +* **Body** - Each section has a "body", which contains the contents of that section. + + +## Chapter Rules +Every page has exactly four "top level" chapters, which are given `##` (H2) headers: + +* **Introduction** - This chapter is devoted to very high level, jargon-free marketing language explaining the benefits of the product in clear and simple prose. The Introduction chapter has exactly three sections: + + * *Main uses* - This section has a definition list summarizing the key scenarios in which this product would be used. + + * *Core users* - This section has a definition list summarizing the key audiences that use this product. + + * *Key advantages* - This section has a definition list summarizing the major benefits of this product over the competition. + +* **Concepts** - This chapter is devoted to establishing a clear, unambiguous lexicon for discussing this product. It contains three or more definition list sections or section groups. It does not contain any how-to or FAQ sections, the Concepts section is entirely focused on establishing the concepts themselves, not explaining how to use them. + +* **Tutorials** - This chapter is devoted to providing detailed step-by-step instructions on how to accomplish certain goals. This chapter contains three or more how-to sections or section groups. Everything in the Tutorial should be consistent with the language established in the Concepts. + +* **FAQ** - This chapter provides focused answers to very specific questions that are easily misunderstood or otherwise don't fit perfectly in the above chapters. This chapter contains three or more FAQ-style sections or section groups. The FAQ does not define any new terms (only the Concepts section does that), and does not give any step-by-step instructions (only the Tutorials section does that). + +Anything outside of these four chapters should be moved within the relevant chapter, following the section guidelines for that chapter. + + +## Header Rules +There are two kinds of headers: + +* **Short headers** - These are titles that are limited to 1-3 short words, such that it will fit into the "left hand nav" containing the table of contents, without "wrapping" around. Short titles capitalize major words. For example, this would be a short title: + + ``` + # Platforms + ``` + +* **Long headers** - These are longer titles (4+ words), prefixed with a short title in square brackets. This allows for longer and more descriptive titles, while still providing a short title that fits into the left-hand nav comfortably. Long titles ask a complete question, and are capitalized and punctuated like a normal sentence. For example, this would be a long title: + + ``` + # [Platforms] Where can I use the Expensify App? + ``` + +* To avoid confusion, no two sections in the same chapter or section group should have the same short or long title. + +* Headers that contain questions should be asked from the customer's perspective (ie, "How do I X?" not "How do you do X?") + + +## Section Rules +There are three kinds of sections: + +### Definition List Sections +A "definition list" type section break a high level concept into smaller pieces, and consists of: + +* A "long header" describing the topic being deconstructed and defined, generally starting with "What", but never "How" or "Why". +* 1-2 introductory sentences, explaining the theme of the list +* An unnumbered bullet list, where each bullet consists of: + * A bolded term of 1-3 words + * A clear definition or description of the term, in 1-3 complete sentences. +* Nothing should exist in the section after the bullet list + +An example of a definition list section follows: + + ``` + # [Fruit] What are the best fruits? + It's well known that these are the best fruits: + + * **Apples** - The king of fruit. So crispy. + * **Oranges** - Often seen as diametrically opposed. But still delish. + * **Tomato** - Some people don't know this is a fruit. But it is. + ``` + +### How-to List Sections +A "how-to list" type section gives sequential steps to accomplish a goal, and consists of: + +* A "long header" describing the goal of the tutorial, starting with "How". +* 1-2 introductory sentences, explaining the goal of the tutorial +* A numbered list, where each step consists of a single sentence covering: + * A specific UI element to press or type into, if any, in bold + * An explanation of the benefit of doing this + * Each step describes exactly one user action; do not combine multiple actions into a single step +* Confirm the sum of the steps accomplishes the clearly stated goal +* Confirm every concept mentioned in the tutorial has a corresponding definition in the Concepts section +* Nothing should exist in the section after the numbered list + +An example of a how-to section follows: + + ``` + # [Email] How do I send an email? + Email is the easiest way to write someone. To send an email: + + 1. Press the **Email** app icon, to open the app. + 2. Press the **Compose** button, to start writing the email. + 3. Enter the address you want to send to into the **To** field, so it gets to the right person. + 4. Provide a subject of the email in the **Subject** field, to entice them to open the email. + 5. Write the email into the large blank body, to detail the message. + 6. Press the **Send** button, to deliver it to its addressed recipient. + ``` + +### Frequently Asked Question (FAQ) Sections +A "FAQ" type section gives a detailed answer to a single question, often to explain the non-obvious reasoning behind something, and consists of: + +* A "long header", asking a specific question, generally starting with "Why" + * Note: A FAQ cannot ask a "How do I...?" question -- move this to the Tutorials chapter and use a HowTo section +* 1 paragraph answering the question, in 2-4 comprehensive sentences. + * Note: A FAQ cannot have a bullet list -- move this to the Concepts chapter and use a definition list section + * Note: A FAQ cannot have a numbered list -- move this to the Tutorials chapter and use a HowTo section + + +## Section Group Rules +When the Concepts, Tutorials, or FAQ chapters have 6 or more sections, those sections can optionally be split into two or more "section groups". Each section group is given a "H3" header (`###`), and consists of: + +* A short header, named after the common theme of the sections of the section group +* 3-6 sections, of any type + + +## Cross Platform Rules +All instructions should be written in a fashion to work across all platforms (web, mobile, desktop, native, etc). Accordingly, the language should to the greatest degree possible be written in such a fashion that works across all platforms. Specifically: + +* Where possible, use a cross-platform verb. For example, do not say "click" or "tap", say "press" +* If there is no suitable cross-platform term, briefly explain how to do the equivalent action on both platforms. For example, "right-click or long-tap to open the context menu..." +* For anything that has no equivalent, clarify which platform the instruction refers to. For example: "If you have a mouse, hover over the chat to see the hover menu..." + +## General Language Rules +To ensure that the content always sounds consistent: + +* "You" always refers to the reader, who is a user and customer of Expensify +* "We" refers to the company Expensify, who is the author of the superapp this is documenting. +* Any use of "we" could be replaced with "Expensify" and would still work. +* The help documentation is in effect the product/company talking directly to the user, in the first person. + diff --git a/help/_config.yml b/help/_config.yml index 11091b1a8b7c..407dfe9fea91 100644 --- a/help/_config.yml +++ b/help/_config.yml @@ -8,3 +8,4 @@ github_username: expensify # Ignore what's only used for the Github repo exclude: - README.md + - GUIDELINES.md diff --git a/help/_layouts/default.html b/help/_layouts/default.html index cf8c7feeaea0..7fcd95c1b325 100644 --- a/help/_layouts/default.html +++ b/help/_layouts/default.html @@ -85,13 +85,27 @@ .toc-sidebar li { margin-left: 0; - padding-left: 10px; + padding-left: 0; + } + + .js-toc > ul > li > a { + font-weight: bold; + font-size: 18px; + } .js-toc > ul > li > ul > li { margin-top: 25px; } + .js-toc > ul > li > ul > li > a { + font-weight: bold; + } + + .js-toc > ul > li > ul > li > ul > li > ul > li { + padding-left: 10px; + } + .toc-sidebar a { word-wrap: break-word; display: block; @@ -110,20 +124,9 @@ .toc-sidebar .is-active-link { background-color: #eaf5ff; color: #0366d6; - font-weight: bold; border-radius: 6px; } - a:has(+ ul.is-collapsible)::after { - content: '∧'; /* Use the logical AND symbol */ - display: inline; /* Ensure the caret appears directly after the content */ - margin-left: 5px; /* Add some space between the text and the caret */ - transform: rotate(180deg); /* Rotate the caret 180 degrees */ - display: inline-block; /* Required to apply transform */ - position: relative; /* Enables positioning adjustments */ - top: 3px; /* Moves the caret down 3 pixels */ - } - /* Main content area */ main { margin-left: 300px; @@ -161,7 +164,7 @@ } .is-active-link { - font-weight: bold; + font-weight: normal; } .scroll-spacer { @@ -211,7 +214,7 @@ .footer-column { flex: 1; max-width: 300px; /* Set a max-width for each column */ - padding: 0 20px; /* Add padding for some space between the columns */ + padding: 0 20px; /* Add padding for some space between the columns */ } @@ -270,8 +273,8 @@

    Resources

  • Press Kit
  • Support
  • ExpensifyHelp
  • -
  • Community
  • -
  • Privacy
  • +
  • Terms of Service
  • +
  • Privacy
  • Expensify App
  • diff --git a/help/card.md b/help/card.md index c6a457629643..1ed51daf7713 100644 --- a/help/card.md +++ b/help/card.md @@ -5,14 +5,14 @@ title: Expensify Cards ## Introduction The Expensify Card is a corporate payment card that integrates seamlessly with Expensify Expense, allowing you to manage company spending in real-time. By enforcing your company’s expense policy at the point of sale, the Expensify Card eliminates the need for manual receipt tracking, reduces fraud, and ensures compliance with expense policies. Whether you’re looking for simplified expense management, real-time control, or cashback rewards, the Expensify Card is designed to meet your needs. -### [Main uses] When should I use the Expensify Card? +### Main uses The Expensify Card is ideal for any business looking to streamline its expense management and control employee spending. Key use cases include: * **Enforcing company policy** - Ensure that purchases are compliant with your company’s expense policy automatically at the point of sale. * **Automating expense tracking** - Eliminate manual receipt entry by capturing expenses automatically with every card transaction. * **Real-time spending control** - Gain immediate insight into employee spending, and control purchases with dynamic card limits. * **Earning cashback** - Get rewarded for company spending with up to 2% cashback on all purchases. -### [Core users] Who uses the Expensify Card? +### Core users The Expensify Card is a valuable tool for companies of all sizes, from startups to large enterprises. Some common users include: * **Small businesses** - Manage corporate spending efficiently without the complexity of traditional corporate cards. * **Enterprises** - Gain full visibility into employee spending and ensure compliance across all transactions. @@ -20,7 +20,7 @@ The Expensify Card is a valuable tool for companies of all sizes, from startups * **Nonprofits** - Track and control organizational spending while ensuring that all expenses align with donor guidelines. * **Accountants** - Streamline reimbursement and auditing processes by eliminating manual entry and simplifying receipt management. -### [Key advantages] Why should I use the Expensify Card? +### Key advantages The Expensify Card offers unique advantages for companies looking to optimize their expense management: * **Policy enforcement at the point of sale** - Automatically enforce your company’s expense policy when employees use the card, ensuring that only approved purchases go through. * **Real-time visibility** - See employee spending as it happens, with every transaction instantly visible in Expensify Expense. @@ -31,11 +31,10 @@ The Expensify Card offers unique advantages for companies looking to optimize th * **Fraud reduction** - Reduce fraud by limiting card use to specific categories or vendors, and by gaining full visibility into all transactions. ## Concepts -Expensify Cards introduce several concepts that redefine corporate spending management. ### [Policy enforcement] How does the Expensify Card enforce company policy? The Expensify Card is designed to automatically enforce your company’s expense policy: -* **Policy-based approvals** - Transactions are approved or denied based on predefined expense categories, amounts, or vendor types. For example, purchases outside of approved categories (e.g., entertainment, personal items) can be blocked in real-time. +* **Policy-based approvals** - Transactions are approved or denied based on predefined expense categories, amounts, or vendor types. * **Spending limits** - Set individual or department-level spending limits that the card will automatically enforce. * **Real-time monitoring** - Managers and admins can view all transactions as they happen, allowing them to flag or approve expenses in real-time. @@ -62,61 +61,232 @@ The Expensify Card includes several features that reduce the risk of fraud: * **Real-time visibility** - Track all card transactions as they happen, making it easy to identify and address suspicious activity. * **Dynamic limits** - Adjust spending limits in real-time, so if an employee is in a situation where they need additional funds, it can be handled securely. -## Platforms -The Expensify Card works seamlessly across all platforms, ensuring that your company’s expense management is fully integrated: -* **Web app** - Manage Expensify Cards and monitor transactions from the Expensify web app. -* **Mobile app** - Employees can use the Expensify mobile app to track expenses, view transactions, and manage their cards on the go. -* **Desktop app** - Full control of Expensify Cards is available through the Expensify desktop app for Mac and Windows, making it easy for admins to manage policies and review transactions. +### [Virtual Cards] What are the benefits of using virtual Expensify Cards? +Virtual cards are digital cards designed for online transactions with several benefits: +* **Flexibility** - Create or delete virtual cards instantly for transactions with predetermined amounts or recurring payments. +* **Customizable limits** - Set spending limits for each virtual card. +* **Security** - Issue virtual cards for single-use or recurring expenses to reduce the risk of unauthorized transactions. +* **Insights** - Track recurring spend for specific vendors by assigning a virtual card to a team, department, or vendor. ## Tutorials -### [Issue a card] How do I issue an Expensify Card to an employee? -1. Go to **Settings** > **Cards** in the Expensify app. -2. Press **Issue Card** and select the employee from the list. -3. Set an initial spending limit, and assign the card to the employee’s workspace. -4. The employee will receive an email with instructions to activate their card. +### Getting Started +#### [Enable Expensify Card] How do I enable the Expensify Card for my workspace? +To enable the Expensify Card for your workspace, you must be a Workspace Admin. Follow these steps: + +1. Press your profile image or icon in the menu. +2. Scroll and press **Workspaces** in the menu. +3. Select the workspace you want to enable Expensify Cards for. +4. Press **More features** in the menu. +5. Under the **Spend** section, enable the Expensify Card toggle. + +#### [Select a bank account] How do I select a bank account for the Expensify Card? +Before issuing Expensify Cards, connect them with a bank account. Here's how: + +1. Press **Expensify Card** in the menu. +2. Press **Issue new card**. +3. Select an existing bank account or follow the steps to add a new one. + +### Card Management +#### [Issue a card] How do I issue an Expensify Card to an employee? +To issue an Expensify Card to an employee, follow these steps: + +1. Press **Issue card**. +2. Select the employee you want to issue the card to. +3. Choose to issue a physical or virtual card. +4. Pick a smart, monthly, or fixed limit. +5. Enter the limit amount and add a card name. +6. Press **Issue card** to confirm and issue the card. + +#### [Adjust limits] How do I adjust spending limits on an Expensify Card? +To adjust spending limits on an Expensify Card, follow these steps: -### [Adjust limits] How do I adjust spending limits on an Expensify Card? 1. Go to **Settings** > **Cards**. 2. Select the employee’s card from the list. 3. Press **Edit Limits** and adjust the spending limit for the card. 4. Press **Save** to apply the new limit. -### [View transactions] How do I track Expensify Card transactions? +#### [Manage Expensify Cards] How do I manage my issued Expensify Cards? +To manage your issued Expensify Cards, you must be a Workspace Admin. Follow these steps: + +1. Press your profile image or icon in the bottom left menu. +2. Scroll down and press **Workspaces** in the left menu. +3. Select the workspace containing the desired Expensify Cards. +4. Press **Expensify Card** in the left menu to see a list of all issued cards. +5. Press a card row to view details or adjust the card limit, limit type, name, or deactivate it. +6. Press **Settings** in the top right to adjust the settlement account or change the settlement frequency. + +### Transactions and Tracking +#### [View transactions] How do I track Expensify Card transactions? +To track Expensify Card transactions, follow these steps: + 1. Navigate to the **Expenses** section in the Expensify app. 2. Filter by **Expensify Card** to view all transactions made using the card. 3. Select any transaction to view the details, including receipts and categorization. -### [Manage policies] How do I enforce a company policy using Expensify Cards? -1. Go to **Settings** > **Policies**. -2. Select the policy to apply to your Expensify Cards. -3. Under **Spending Rules**, set category and spending restrictions. -4. Press **Save** to ensure all Expensify Card transactions follow these rules. +#### [Dispute a transaction] How do I dispute an Expensify Card transaction? +If you encounter a transaction error, you can dispute it by following these steps: -## FAQ +1. Contact the merchant to try and resolve the issue directly. +2. If unresolved, contact Expensify by opening a chat with Expensify Concierge or emailing concierge@expensify.com with details of the disputed charge and supporting documentation. +3. If you suspect fraud, immediately deactivate your card by pressing your profile image, selecting **Wallet**, pressing your Expensify Card, and then **Report card fraud**. Follow the prompts to deactivate and request a new card. +4. Enable [Two-Factor Authentication (2FA)](https://help.expensify.com/articles/new-expensify/settings/Enable-Two-Factor-Authentication) for added security. -### How do I set up the Expensify Card for my company? -To set up the Expensify Card: -1. Go to **Settings** > **Cards**. -2. Follow the prompts to enable the Expensify Card for your company. -3. Issue cards to employees, set spending limits, and define company policies for card usage. +### Digital Wallet and Notifications +#### [Add to Wallet] How do I add the Expensify Card to my digital wallet? +To use your Expensify Card for contactless payments, add it to your Apple or Google Pay digital wallet: + +**Apple Pay** + +1. Open the **Wallet** app on your device. +2. Press the **+** button to add a new card. +3. Select **Debit or Credit Card**. +4. Press **Continue** and follow the instructions to add your virtual Expensify Card. + +**Google Pay** + +1. Open the **Google Pay** app on your device. +2. Press **Add to Wallet**. +3. Select **Payment Card** and then **Add new debit or credit card**. +4. Enter your virtual Expensify Card details to complete the process. + +#### [Enable Notifications] How do I enable notifications for my Expensify Card? +To receive real-time notifications for spending activity on your Expensify Card, follow these steps: + +1. From your Expensify Chat inbox, press the dropdown on the logo or avatar in the top left corner. +2. Select the workspace you want to update the notification settings for. +3. Press the workspace chat in your inbox (the chat with your workspace’s name as the title). +4. Press the header at the top of the chat. +5. Press **Settings**. +6. Press **Notify me about new messages** and select **Immediately**. + +Then, enable notifications on your device: + +**iPhone** + +1. Go to your device settings. +2. Find and tap **New Expensify**. +3. Tap **Notifications** and enable notifications. +4. Customize your alerts. Depending on your phone model, you may have extra options to customize the types of notifications you receive. + +**Android** + +1. Go to your device settings. +2. Tap **Notifications** and select **Apps notifications**. +3. Find and tap **New Expensify**. +4. Enable notifications. +5. Customize your alerts. Depending on your phone model, you may have extra options to customize the types of notifications you receive. + +You will now receive real-time spend notifications to your mobile device. -### How does the Expensify Card enforce my company’s expense policy? -The Expensify Card automatically enforces your company’s expense policy by: -* Blocking purchases outside of approved categories. -* Enforcing spending limits in real-time. -* Providing real-time visibility into employee spending for managers. +### Card Details and Limits +#### [Update Mailing Address] How do I update my Expensify Card mailing address? +To update your mailing address for your Expensify Card, follow these steps: -### How do employees submit expenses with the Expensify Card? +1. Hover over **Settings** and press **Account**. +2. Press the **Credit Card Import** tab. +3. Press **Request a New Card** on your physical card pending activation. +4. Select **I lost my card**. If you’re updating your address to receive your new Expensify Visa® Commercial Card, select this option even though you have not lost a card. +5. Confirm your details and press **Continue**. +6. Update your address and press **Continue**. If the new card has already been shipped to an incorrect address, proceed to the next step to resend the card to the newly updated address. +7. Proceed with the card replacement. Your new card will arrive in 2-3 business days. + +#### [Check Card Limit] How do I check my Expensify Card limit? +The Smart Limit of your Expensify Card updates automatically after each purchase. To check your available Smart Limit, follow these steps: + +1. Press your profile image or icon in the menu. +2. Press **Wallet**. +3. Press your Expensify Card to see the available Smart Limit. + +### Upgrading and Virtual Card Details +#### [Upgrade Cards] How do I upgrade to the new Expensify Visa® Commercial Card? +To upgrade your company’s Expensify Cards to the new Expensify Visa® Commercial Card, follow these steps: + +1. On the **Home** page, press the task titled "Upgrade to the new and improved Expensify Card." +2. Review and agree to the **Terms of Service**. +3. Press **Get the new card** to automatically mail new physical cards to existing cardholders with limits greater than $0 and issue virtual cards for immediate use. +4. If Positive Pay is enabled, contact your bank to whitelist the new ACH ID: 2270239450. +5. Remind employees to update payment information for recurring charges to their virtual card information. + +Existing cards remain active until deactivated by a Domain Admin or the cardholder. Cards won't be issued to employees who don't currently have them; you'll need to [issue a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa%C2%AE-Commercial-Card-for-your-Company) for them. + +#### [View Virtual Card Details] How do I view my virtual Expensify Card details? +To view your virtual card details in Expensify, follow these steps: + +1. Press your profile image or icon in the menu. +2. Press **Wallet**. +3. Press your Expensify Card. +4. Press **Reveal Details** to view your virtual Expensify Card number, expiration date, CVV, and address. + +## FAQs + +### Usage and Setup +#### Why should I use Expensify Expense for my business? +The Expensify Card is a corporate payment card that integrates seamlessly with Expensify Expense, providing simplified expense management, real-time control, and cashback rewards. + +#### How do I set up the Expensify Card for my company? +To set up the Expensify Card, go to **Settings** > **Cards**, follow the prompts to enable the Expensify Card for your company, issue cards to employees, set spending limits, and define company policies for card usage. + +#### How does the Expensify Card enforce my company’s expense policy? +The Expensify Card automatically enforces your company’s expense policy by blocking purchases outside of approved categories, enforcing spending limits in real-time, and providing real-time visibility into employee spending for managers. + +### Expense Submission and Tracking +#### How do employees submit expenses with the Expensify Card? Employees don’t need to manually submit expenses with the Expensify Card. Each transaction is automatically recorded, categorized, and attached to an expense report. Receipts are automatically captured and matched with transactions, eliminating the need for manual entry. -### Can I track transactions in real-time? +#### Can I track transactions in real-time? Yes, the Expensify Card provides real-time visibility into all transactions. Admins and managers can monitor employee spending as it happens, ensuring full control and oversight. -### What rewards do I earn with the Expensify Card? +### Rewards and Benefits +#### What rewards do I earn with the Expensify Card? The Expensify Card offers up to 2% cashback on all purchases. Cashback can be applied directly to reduce your monthly Expensify bill, or used to offset other company expenses. -### How do I control where employees can use their Expensify Cards? +#### How do I control where employees can use their Expensify Cards? You can control employee card usage by setting vendor and category restrictions. For example, you can restrict cards to be used only for travel-related purchases, or limit spending to certain vendors. These restrictions are enforced at the point of sale. +### Transaction Issues +#### Why did my transaction get declined? +Here are some reasons why an Expensify Card transaction might be declined: + - **Insufficient card limit**: If a transaction exceeds your Expensify Card’s available limit, the transaction will be declined. Submitting expenses and getting them approved will free up your limit for more spending. + - **Inactive card**: Your card isn’t active yet or it was disabled by your Domain Admin. + - **Incorrect card details**: Your card information was entered incorrectly with the merchant. Entering incorrect card information, such as the CVC, ZIP, or expiration date, will also lead to declines. + - **Fraudulent or risky activity**: If Expensify detects unusual or suspicious activity, we may block transactions as a security measure. + +### Expense Reporting +#### How do I report my Expensify Card expenses? +You can report and submit Expensify Card expenses just like any other expenses, and you’ll want to submit them regularly to ensure you have a sufficient spending amount available on the card. As your expenses are approved, your Smart Limit updates accordingly. + +SmartScanned receipts should automatically attach to the related Expensify Card expense. Expensify also automatically generates an IRS-compliant eReceipt for every transaction as long as the expense isn’t lodging-related. If your organization doesn’t require itemized receipts, you can rely on eReceipts instead. + +### Fraud Protection +#### How am I protected from fraud using the Expensify Card? +Expensify uses sophisticated algorithms to detect and block unusual card activity. You can also enable real-time notifications to receive alerts each time your card is charged. + +#### How long does the dispute process take? +The dispute process can take up to 90 days. + +#### Can I cancel a dispute? +You can cancel a filed dispute by using your Expensify Chat thread with Concierge or by emailing concierge@expensify.com. + +### Account and Usage Requirements +#### Do I need a specific type of bank account to use the Expensify Card? +The Expensify Card requires a US business bank account opened in the name of a business incorporated in the US. + +#### Can I use the Expensify Card across multiple workspaces? +You can use the Expensify Card on every workspace you create. However, a settlement account can only be used with the Expensify Card on one workspace. + +#### Can I issue multiple cards to the same employee? +You can issue an unlimited number of both physical and virtual cards to employees, supporting a variety of use cases. + +### Upgrading and Reconciliation +#### Why don’t I see the task to agree to new terms on my Home page? +There are several reasons why the task to accept new terms might not appear: + - You may not be a Domain Admin. + - Another domain admin has already accepted the terms. + - The task might be hidden. Scroll to the bottom of the Home page and press **Show Hidden Tasks** to view all tasks. + +#### Will upgrading affect the continuous reconciliation process? +The upgrade process won't affect continuous reconciliation. During the transition, you may have employees with both old and new cards, resulting in two separate debits for your settlement account per period. Once all spending transitions to the new cards, you'll only see one settlement. +#### Do I have to upgrade to the new Expensify Visa® Commercial Card? +Yes, an upgrade to the new Expensify Visa® Commercial Card is necessary. A deadline will be provided soon, but you'll have ample time to complete the upgrade. \ No newline at end of file diff --git a/help/chat.md b/help/chat.md index c3b684874974..b46d1bec1066 100644 --- a/help/chat.md +++ b/help/chat.md @@ -2,136 +2,246 @@ layout: product title: Expensify Chat --- + ## Introduction -Expensify Chat is a full-featured business chat tool, seamlessly integrated into the Expensify Superapp. It enables real-time collaboration with your team, clients, vendors, and friends, offering a powerful, Slack-style chat experience. Expensify Chat provides all the features you expect from a modern chat tool, including chat rooms, direct messages, file sharing, image attachments, emoji reactions, and threaded conversations. - -### [Main uses] When should I use Expensify Chat? -Expensify Chat is designed for teams and businesses of all sizes to facilitate communication and collaboration. Use Expensify Chat to: -* **Collaborate with teammates** - Create chat rooms and direct messages to discuss projects, share updates, and work together in real-time. -* **Support clients** - Manage client conversations with ease, keeping all discussions, invoices, and approvals in one place. -* **Engage with vendors** - Communicate with your vendors to negotiate, place orders, and track payments. -* **Coordinate with friends** - Keep in touch with friends and colleagues using direct messages or group chats. - -### [Core users] Who uses Expensify Chat? -Expensify Chat is for everyone who needs to stay connected and collaborate, including: -* **Teams** - Coordinate work, share updates, and resolve issues quickly with real-time chat rooms and message threads. -* **Remote workers** - Stay connected with your team from anywhere, with desktop and mobile chat apps that support real-time communication. -* **Clients** - Provide seamless client communication, allowing you to manage projects and billing through the same platform. -* **Vendors** - Manage vendor communication, ensuring that orders, invoices, and payments are all handled in one platform. -* **Friends and family** - Expensify Chat is also great for personal conversations, making it easy to chat and share files with anyone who has an email address. - -### [Key advantages] Why should I use Expensify Chat? -Expensify Chat offers unique benefits that set it apart from other business chat tools: -* **Integrated with Expensify** - Unlike standalone chat apps, Expensify Chat is fully integrated with the Expensify Superapp, giving you access to all your expenses, invoices, payments, and chats in one platform. -* **Real-time communication** - Instantly message anyone with an email address or phone number, whether they are part of your organization or an external client or vendor. -* **Threads and reactions** - Organize conversations with threaded replies and react to messages with emojis to keep discussions focused and fun. -* **File sharing and attachments** - Share files, images, and links directly within your chats for easy collaboration. -* **Searchable history** - Expensify Chat allows you to search through all conversations, so you never lose track of important discussions or files. -* **Cross-device functionality** - Stay connected with your team from anywhere, with support for both desktop and mobile apps. +Expensify Chat is a tool for real-time collaboration with a Slack-style experience. + +### Main Uses +Key scenarios for using Expensify Chat: + +* **Team collaboration** - Discuss projects and share updates in real-time. +* **Client support** - Keep all client communications and approvals in one place. +* **Vendor engagement** - Communicate with vendors for negotiations and orders. +* **Friend coordination** - Stay in touch with friends through direct messages or group chats. + +### Core Users +The main audiences for Expensify Chat: + +* **Teams** - Coordinate work and resolve issues quickly. +* **Remote workers** - Stay connected via desktop and mobile apps. +* **Clients** - Manage projects and billing seamlessly. +* **Vendors** - Handle orders and payments efficiently. +* **Friends and family** - Easy personal conversations with file sharing. + +### Key Advantages +Benefits of using Expensify Chat: + +* **Integration with Expensify** - Access expenses, invoices, and chats in one place. +* **Instant communication** - Message anyone with an email or phone number. +* **Organized discussions** - Threaded replies and emoji reactions. +* **Easy file sharing** - Directly share files and images within chats. +* **Searchable history** - Find past conversations and files effortlessly. +* **Cross-device support** - Stay connected on desktop and mobile. ## Concepts -Expensify Chat introduces several key features that make it a powerful communication tool. - -### [Chat rooms] How do chat rooms work in Expensify Chat? -Chat rooms are the core feature of Expensify Chat, allowing groups of people to collaborate in real-time: -* **Create rooms** - You can create public or private rooms for your team, clients, or vendors. Public rooms are open to anyone in your workspace, while private rooms require an invitation. -* **Invite members** - Invite anyone with an email address or SMS number to join a chat room, even if they aren’t on Expensify yet. -* **Threads** - Keep conversations organized by replying to specific messages in a thread. This is useful for discussing multiple topics in a single room. - -### [Direct messages] What are direct messages? -Direct messages are private, one-on-one conversations between two users: -* **One-to-one messaging** - Use direct messages for private conversations with teammates, clients, or friends. -* **Send files** - Attach images, documents, and links directly in your one-on-one conversations. -* **Searchable** - All direct messages are fully searchable, so you can easily find past conversations or files. - -### [File sharing] How do I share files in Expensify Chat? -Expensify Chat makes it easy to share files and attachments: -* **Upload files** - You can upload images, documents, PDFs, and other files directly into any chat room or direct message. -* **Preview files** - View shared files directly in the chat without having to download them. -* **Download files** - All shared files can be downloaded for offline use or further collaboration. - -### [Emoji reactions] How do emoji reactions work in Expensify Chat? -Emoji reactions add a fun and efficient way to respond to messages: -* **React to messages** - Simply click the emoji icon under any message to react with an emoji. Reactions are visible to everyone in the conversation. -* **Multiple reactions** - You can add multiple reactions to the same message, and others can join in by adding their own reactions. - -### [Threads] How do threaded conversations work? -Threads allow you to keep conversations organized within chat rooms: -* **Reply to a specific message** - Instead of creating a new message, you can reply directly to a previous message to start a thread. -* **View threaded replies** - Threads are nested under the original message, making it easy to follow the conversation. -* **Keep discussions organized** - Threads prevent clutter in busy chat rooms by grouping related messages together. - -### [Search] How does search work in Expensify Chat? -Expensify Chat includes a powerful search feature to help you find messages, files, and conversations: -* **Search messages** - Search across all your chat rooms and direct messages to find specific keywords, phrases, or conversations. -* **Search files** - Quickly locate any files shared in chat rooms or direct messages by searching for file names or types. -* **Filter by chat room** - Narrow your search results by limiting them to a specific chat room or direct message. - -## Platforms -Expensify Chat works across multiple platforms, ensuring you can stay connected with your team wherever you are: -* **Web app** - Access Expensify Chat through your browser, with full support for chat rooms, file sharing, and emoji reactions. -* **Mobile app** - Stay connected on the go with the Expensify mobile app, which supports all chat features, including image attachments and notifications. -* **Desktop app** - Use the Expensify desktop app for a more immersive experience, with full support for notifications, file sharing, and threaded conversations. + +### Chat Types +Expensify Chat supports several types of communication: + +* **Private chats** - One-on-one communication. +* **Group chats** - Private conversations with multiple participants. +* **Chat rooms** - Public or private discussions available to workspace members. + +### Special Chat Rooms +Expensify Chat includes special chat rooms for specific purposes: + +#### Admin and Announce +Special rooms in a workspace: + +* **#admins** - Only accessible to Workspace Admins to manage settings and collaborate with other admins. This room includes your Expensify Setup Specialist and, if applicable, your Account Manager. You can also: + - Chat with your dedicated Expensify Setup Specialist. + - Chat with your Account Manager (if you have a subscription with 10 or more members). + - Review changes made to your Workspace settings. + +* **#announce** - For company-wide announcements. By default, all Workspace Members can send messages, but permissions can be updated to allow only admins to post. + +### Update Messaging Permissions in #announce +To allow only admins to post in an #announce room: + +1. Open the #announce room chat in your inbox. +2. Press the room header. +3. Select **Settings**. +4. Choose **Who can post** and select **Admins only**. + +### Reorder Chat Inbox + +Customize the order of chat messages in your inbox by pinning them or changing your message priority: + +* **Pin**: Moves a specific chat to the top of your inbox list. +* **Message priority**: Determines the order of message display: + - **Most Recent**: Shows all chats by the most recent, with pinned chats at the top. + - **#focus**: Displays only unread and pinned chats, sorted alphabetically. + +#### Pin a Message + +To pin a message: + +1. Press and hold (or right-click) a chat in your inbox. +2. Select **Pin**. The chat will be pinned to the top of your inbox. +3. To unpin, repeat this process and select the pin icon again. + +#### Change Message Priority + +To change message priority: + +1. Press your profile image or icon. +2. Select the **Preferences** tab. +3. Choose **Priority Mode** and select either #focus or Most Recent. + +### Leave a Chat Room + +To leave a chat room: + +1. Open the chat room. +2. Press the header or the 3 dot menu icon in the top right. +3. Select **Leave**. After leaving, the chat room will no longer appear in your inbox, and you won't receive notifications from it. + +### Flag Chat Messages + +Flagging a message as offensive (including unwanted behavior or offensive messages or attachments) escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified of the flag anonymously, and the moderation team will decide what further action is needed. + +Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed. In extreme cases, the sender of the message may be temporarily or permanently blocked from posting. + +Messages sent in public chat rooms are automatically reviewed for offensive content by an automated system. If offensive content is found, the message is sent to Expensify’s internal moderation team for further review. + +To flag a message: + +1. Open the chat in your inbox. +2. Press and hold (or hover over on desktop) the message and select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. ## Tutorials -### [Create a chat room] How do I create a chat room in Expensify Chat? -1. Navigate to the **Chat** section of the Expensify app. -2. Press **Create Room**. -3. Enter a name for the room and choose whether to make it public or private. -4. Invite members by entering their email addresses or phone numbers. -5. Press **Create** to finalize the room. - -### [Send a direct message] How do I send a direct message? -1. Press **New Message** from the chat screen. -2. Enter the email address or phone number of the person you want to message. -3. Type your message and press **Send**. -4. Optionally, attach files or images by pressing the attachment icon. - -### [React to a message] How do I react to a message with an emoji? -1. Hover over the message you want to react to. -2. Press the **emoji** icon that appears below the message. -3. Choose an emoji from the list, and it will be added to the message. -4. To add more reactions, simply repeat the process. - -### [Start a thread] How do I reply to a message in a thread? -1. Hover over the message you want to reply to. -2. Press the **Reply in thread** button. -3. Type your reply and press **Send**. Your reply will appear nested under the original message. - -### [Search for a message] How do I search for messages or files? -1. Press the **Search** bar at the top of the chat screen. -2. Enter the keyword, phrase, or file name you are looking for. -3. Filter results by chat room or direct message (optional). -4. Press **Search** to view the results. +### [Create Room] How do I create a chat room? +To create a chat room: + +1. Press the **+** button and select **Start Chat**. +2. Choose the **#Room** tab. +3. Enter a name for the room (ensure it's unique within the workspace). +4. Optionally, add a description. +5. Select **Workspace** to assign the room to a workspace. +6. Choose **Who can post** to set posting permissions (all members or only admins). +7. Set **Visibility** to determine room accessibility: + - **Public**: Viewable by anyone (ideal for conferences). + - **Private**: Only invited individuals can find it. + - **Workspace**: Accessible by all workspace members. +8. Press **Create room** to finalize the setup. + +*Note: Anyone, including those outside the workspace, can be invited to private or restricted rooms.* + +### [Invite Members] How do I invite members to a chat group or room? +Invite members using one of the following methods: + +- **Mentioning**: + 1. Open the chat group or room. + 2. In the message field, type @ and the person’s name or email address. Repeat for all participants. + 3. Enter a message if desired and press Send. + +- **Members Pane**: + 1. Open the chat group or room. + 2. Press the room or group header, then **Members**. + 3. Press **Invite member**, select contacts, and press **Invite**. + +- **Sharing Link or QR Code**: + 1. Open the chat group or room. + 2. Press the room or group header, then **Share**. + 3. Copy the link or present the QR code for others to scan. + +*Note*: These options are only for groups or rooms, not for private 1-on-1 chats. + +### [Start a Private Chat] How do I start a private 1-on-1 chat? +To start a private 1-on-1 chat: + +1. Press the **+** button and select **Start Chat**. +2. Enter the name, email, or phone number of the person you want to chat with. +3. Select their name to start a new chat with them. + +*Note: You cannot add more people to a private chat. To include additional participants, create a group chat.* + +### [Start a Group Chat] How do I start a group chat? +To start a group chat: + +1. Press the **+** button and select **Start Chat**. +2. Enter the names, emails, or phone numbers of the participants and select **Add to group** for each. +3. Press **Next** and update the group image or name if desired. + - **Name**: Select **Group Name**, enter the new name, and save. + - **Image**: Select the profile image, upload a new image, and adjust as needed. +4. Press **Start group** to create the chat. + +### [Direct Message] How do I send a direct message? +To send a direct message: + +1. Press **New Message**. +2. Enter the recipient's email or phone. +3. Type and send your message. +4. Attach files using the attachment icon. + +### [Send and Format Messages] How do I send and format chat messages? +To send and format chat messages: + +1. Open any chat in your inbox. +2. Use the message bar at the bottom to enter your message, add attachments, and insert emojis. + - **To add a message**: Press the field labeled "Write something" and type your message. + - **To add an attachment**: Press the plus icon and select **Add attachment**. Choose the attachment from your files. + - **To add an emoji**: Press the emoji icon to the right of the message field. +3. Press the Send icon to send the message. + +You can format the text using markdown: + +- _Italicize_: Add underscores _ on both sides of the text. +- **Bold**: Add two asterisks ** on both sides of the text. +- ~~Strikethrough~~: Add two tildes ~~ on both sides of the text. +- Heading: Add a number sign # in front of the text. +- Inline image: Add `![Alt text](image URL)` with the URL and alt text. +- Tag another member: Add an @ symbol followed by the member's name, username, or email. +- Mention a room: Add a # followed by the room name. +- > Blockquote: Add an angled bracket > in front of the text. +- `Code block for a small amount of text`: Add a backtick ` on both sides of the text. +- Code block for the entire message: Add three backticks ``` at the beginning and end of the message. + +### [Start a Conversation Thread] How do I start a conversation thread? +To start a conversation thread within a chat: + +1. Open the chat in your inbox. +2. Press on the message you want to reply to and select **Reply in thread**. +3. Enter and submit your reply in the new chat thread. + +To return to the main conversation, use the link at the top of the thread. + +### [React Message] How do I react to a message? +To react to a message: + +1. Hover over the message (desktop only). +2. Press the **emoji** icon. +3. Select an emoji to add to the message. + +### [Edit or Delete Messages] How do I edit or delete messages? +To edit or delete your own messages: + +1. Open a chat in your inbox. +2. Press on the message you want to edit or delete. +3. Select **Edit comment** to modify the message. Once edited, an "edited" label will appear next to it. +4. Select **Delete comment** to remove the message or image for all viewers. Note that deleting a message cannot be undone. ## FAQ ### How do I get started with Expensify Chat? -To start using Expensify Chat: -1. Log in to your Expensify account and navigate to the **Chat** section. -2. Create new chat rooms or direct messages and start chatting with your team, clients, or vendors. -3. You can also join existing chat rooms if you've been invited. - -### Can I invite external users to Expensify Chat? -Yes, you can invite anyone to Expensify Chat by entering their email address or phone number. They will receive an invitation to join and can participate in chat rooms or direct messages. - -### Can I search through past conversations in Expensify Chat? -Yes, Expensify Chat allows you to search through all your past conversations, including chat rooms and direct messages. Simply use the search bar at the top of the screen to find specific messages or files. +Log in to your account and go to the **Chat** section to create or join chat rooms and start messaging. -### How do I send files and attachments in Expensify Chat? -To send files: -1. Open a chat room or direct message. -2. Press the **attachment** icon. -3. Select the file from your device and press **Send**. +### Can I invite external users? +Yes, invite anyone via email or phone to join chat rooms or direct messages. -### What types of files can I share in Expensify Chat? -You can share images, documents, PDFs, and other common file types in Expensify Chat. +### Can I search past conversations? +Yes, use the search bar to find specific messages or files in past conversations. -### Can I create private chat rooms? -Yes, when creating a new chat room, you can choose to make it private. Private rooms require an invitation to join, and only invited members can see the room or participate in the conversation. +### What's the difference between a private 1-on-1 chat and a group chat with only 2 people? +With a group chat, you can add additional people to the chat at any time. However, you cannot add more participants to a private 1-on-1 chat. -### How do I manage notifications in Expensify Chat? -You can manage your notifications from the **Settings** section of the Expensify app. Here, you can customize notification preferences for chat messages, mentions, and other activity. +### How do I remove someone from a chat group or room? +Currently, members have to remove themselves from a chat. +### Why is someone I don't recognize in my #admins room? +Your #admins room includes your dedicated Expensify Setup Specialist who assists with onboarding and answers your questions. If you have a subscription with 10 or more members, your dedicated Account Manager is also part of the #admins room for ongoing support. +### Additional Permissions +Some chat rooms may have permissions that restrict who can send messages. If you do not have the required permission level, you will not be able to send messages in those rooms. \ No newline at end of file diff --git a/help/expense.md b/help/expense.md index 0d0012c95fbb..a6335b8e3549 100644 --- a/help/expense.md +++ b/help/expense.md @@ -2,110 +2,881 @@ layout: product title: Expensify Expense --- + ## Introduction Expensify Expense is the core of the Expensify Superapp, offering world-class expense management capabilities for individuals and businesses alike. Whether you're tracking personal expenses for budgeting, submitting receipts for reimbursement, or overseeing company-wide spending, Expensify Expense simplifies the process with its user-friendly design and powerful automation features. -### [Main uses] When should I use Expensify Expense? -Expensify Expense is designed for a wide range of expense management needs, including: -* **Reimburse employee receipts** - Manage business expenses by capturing and submitting receipts for approval. -* **Track personal expenses** - Keep tabs on your own expenses for tax deductions, budgeting, or general financial tracking. -* **Split bills** - Easily divide the cost of shared expenses like meals or group activities and send or receive payments. -* **Automate receipt capture** - Use SmartScan to automatically capture receipt details and categorize them instantly. -* **Submit and approve expense reports** - Create detailed reports for approval, with multi-level workflows if needed. -* **Stay on top of company spending** - With corporate cards and real-time tracking, managers can ensure compliance and stay within budget. - -### [Core users] Who uses Expensify Expense? -Expensify Expense is versatile enough for personal, business, and enterprise use. Some key user groups include: +### [Main uses] What are the main uses of Expensify Expense? +Expensify Expense is designed for a wide range of expense management needs: +* **Reimburse Employee Receipts** - Manage business expenses by capturing and submitting receipts for approval. +* **Track Personal Expenses** - Keep tabs on your expenses for tax deductions, budgeting, or general financial tracking. +* **Split Bills** - Easily divide the cost of shared expenses like meals or group activities and send or receive payments. +* **Automate Receipt Capture** - Use SmartScan to automatically capture receipt details and categorize them instantly. +* **Submit and Approve Expense Reports** - Create detailed reports for approval, with multi-level workflows if needed. +* **Stay on Top of Company Spending** - With corporate cards and real-time tracking, managers can ensure compliance and stay within budget. + +### [Core users] Who are the core users of Expensify Expense? +Expensify Expense is versatile enough for personal, business, and enterprise use. Key user groups include: * **Individuals** - Track personal spending and maximize tax deductions with easy categorization of expenses. * **Freelancers** - Manage client billable expenses and reimbursements. * **Employees** - Submit expense reports with attached receipts, whether you're in the office or traveling. * **Managers** - Approve expenses, oversee spending, and ensure compliance with company policies. * **Accountants** - Streamline financial reporting by integrating with accounting platforms and processing reimbursements. -* **Corporate teams** - Manage large-scale company expenses with corporate cards and centralized approval workflows. +* **Corporate Teams** - Manage large-scale company expenses with corporate cards and centralized approval workflows. -### [Key advantages] Why should I use Expensify Expense? -Expensify Expense offers a variety of advantages for both personal and corporate users: -* **Automated receipt capture** - Eliminate manual data entry with SmartScan, which reads and categorizes receipts automatically. -* **Integrated corporate cards** - Link company cards to track purchases in real-time and avoid the need for reimbursements. -* **Real-time expense tracking** - Keep an eye on budgets and expenses as they happen, reducing the risk of overspending. -* **Custom approval workflows** - Create multi-level approval processes to streamline and secure the expense submission process. -* **Seamless integration** - Sync your expenses with accounting systems like QuickBooks, Xero, NetSuite, and others. -* **Worldwide compatibility** - Expensify supports every currency, making it ideal for international travel and business. +### [Key advantages] What are the key advantages of using Expensify Expense? +Expensify Expense offers a variety of advantages: +* **Automated Receipt Capture** - Eliminate manual data entry with SmartScan. +* **Integrated Corporate Cards** - Link company cards to track purchases in real-time. +* **Real-Time Expense Tracking** - Monitor budgets and expenses as they happen. +* **Custom Approval Workflows** - Create multi-level approval processes. +* **Seamless Integration** - Sync your expenses with accounting systems like QuickBooks, Xero, NetSuite, and others. +* **Worldwide Compatibility** - Expensify supports every currency, ideal for international business. ## Concepts -Expensify Expense is built on a set of core concepts that make expense tracking easy and efficient: -### [Receipt capture] How does Expensify Expense capture receipts? -Expensify simplifies receipt management with SmartScan: -* **SmartScan** - Automatically scans and extracts important details from your receipts (date, amount, merchant, etc.) and categorizes the expense. +### [Receipt capture] What is receipt capture in Expensify Expense? +Receipt capture simplifies receipt management with SmartScan: +* **SmartScan** - Automatically scans and extracts details from your receipts and categorizes the expense. * **E-receipts** - Automatically generate IRS-compliant electronic receipts for purchases made with the Expensify Card. -* **Manual upload** - Take a photo of your receipt or upload it manually from your phone or desktop. - -### [Expense reports] How do I create and submit an expense report? -Expensify streamlines expense report creation: -1. **Add expenses** - Attach receipts or manually enter expenses into a report. -2. **Categorize expenses** - Use custom categories and tags to organize your expenses. -3. **Submit for approval** - Send your report to the relevant approver(s) with just one click. -4. **Track status** - Get notified when your report is approved and reimbursed. - -### [Approvals] What is the approval process? -Managers can review and approve expenses through a customizable workflow: -* **Single or multi-level approvals** - Set up multiple approvers based on the amount or department. -* **Automatic reminders** - Send automatic reminders to approvers to ensure timely processing. -* **Real-time visibility** - Approvers can see the full expense report with attached receipts and can approve or reject it with a single click. - -### [Corporate cards] How do Expensify Cards work? -Expensify Cards integrate directly with Expensify Expense to automate expense tracking: -* **Automatic receipt capture** - Transactions made with Expensify Cards automatically generate e-receipts. -* **Spend limits and controls** - Managers can set individual spending limits, track real-time spend, and lock cards if needed. -* **Rewards** - Earn up to 2% cashback on Expensify Card purchases. - -### [Integrations] Which accounting systems does Expensify Expense support? -Expensify integrates with all major accounting systems: -* **QuickBooks** - Sync expenses and receipts with your QuickBooks account for easy reconciliation. -* **Xero** - Automate the transfer of expense data to your Xero account. -* **NetSuite** - Link expenses to your NetSuite ERP system for complete financial management. -* **More integrations** - Expensify also integrates with Sage Intacct, Oracle, and others. - -### [Reimbursement] How do I get reimbursed for my expenses? -Expensify makes reimbursement quick and easy: -* **Direct deposit** - Get reimbursed directly to your bank account after your report is approved. -* **International payments** - Expensify supports reimbursement in multiple currencies, perfect for global teams. - -## Platforms -Expensify Expense is available on all platforms, ensuring you can track expenses wherever you are: -* **Web app** - Access Expensify Expense from your browser at any time. -* **Mobile app** - Track expenses on the go using the Expensify mobile app for iOS and Android. -* **Desktop app** - Use the Expensify desktop app for Windows or Mac to manage expenses and reports. +* **Manual Upload** - Take a photo or upload your receipt manually from your device. + +### [Expense categories] What are expense categories? +Expense categories help code expenses for accounting and financial reporting. Categories can be manually created or imported from connected platforms like QuickBooks, Xero, and NetSuite. Over time, Expensify learns how you categorize specific merchants and applies them automatically. + +### [Track taxes] What is tax management in Expensify? +Expensify allows you to configure and manage tax rates within your workspace, applicable on Collect and Control plans. This applies the correct tax rates to expenses based on currency and workspace settings. + +#### Enabling and Managing Taxes +Expensify allows you to enable and manage tax rates in your workspace: + +* **Enable Taxes** - Taxes can be enabled on any workspace where the default currency is not USD. If there's a direct accounting integration, tax rates will be managed through the integration. + +* **Managing Tax Rates** - You can manually add, edit, or delete tax rates. Additionally, you can set default tax rates for both workspace currency and foreign currencies. + +### [Corporate cards] What is the role of corporate cards in Expensify Expense? +Corporate cards integrate with Expensify Expense for automated expense tracking: +* **Automatic Receipt Capture** - Transactions automatically generate e-receipts. +* **Spend Limits and Controls** - Managers can set limits, track spending, and lock cards. +* **Rewards** - Earn cashback on Expensify Card purchases. + +### [Integrations] What accounting systems does Expensify support? +Expensify integrates with all major accounting systems, including QuickBooks Online, Xero, NetSuite, and Sage Intacct. + +### [Distance Rates] What are distance rates in Expensify? +Distance rates are configured for mileage expenses, allowing employees to select predefined rates when logging distance-based expenses. + +### [NetSuite Integration] What is the NetSuite integration in Expensify? +NetSuite integration allows for seamless data transfer between Expensify and NetSuite: +* **Expense Categories** - Automatically imported from NetSuite into Expensify for consistency. +* **Tags and Report Fields** - Import departments, classes, and locations as tags or report fields for detailed categorization. +* **Custom Segments/Records** - Import custom segments and records for more specific data mapping. +* **Auto-Sync** - Synchronize data changes between Expensify and NetSuite daily. + +### [Duplicate Detection] What is duplicate detection in Expensify? +Duplicate Detection helps prevent duplicate expense requests by flagging expenses with the same date and amount in the same member's account: +* **Flagging** - A red dot appears in the left menu or the expense’s chat room, putting the expense on “hold.” +* **Eligibility** - Available exclusively for Collect & Control plans. + +### [Bank Account Connection] What does connecting a personal bank account to Expensify mean? +Connecting a personal bank account allows direct receipt of payments and reimbursements: +* **Secure Verification** - We use Plaid, an encrypted third-party platform, to verify your banking information securely. +* **Direct Deposits** - Once connected, all payments and reimbursements go directly into your designated bank account. + +### [Expensify Wallet] What is the Expensify Wallet? +The Expensify Wallet enables peer-to-peer payments by connecting a personal bank account: +* **Setup** - Connect your bank account via Plaid to enable the wallet. +* **Verification** - Verify your identity through Onfido by uploading identification. +* **Payments** - Once the wallet is enabled, you can send and receive payments seamlessly. + +### [Business Bank Account Validation] What is validating a business bank account in Expensify? +Validating a business bank account is essential to ensure that your account is ready for use in Expensify: +* **Test Deposits** - After the bank account connection is approved, Expensify sends three test transactions to your account for validation. +* **Verification Status** - Check your bank account status in the **Bank accounts** section under workspace settings. The status will either be **Verifying** or **Pending**. +* **Input Transaction Amounts** - Once you receive the test deposits, input the transaction amounts as prompted in Expensify to complete the validation process. + +### [Expense Tags] What are tags in Expensify Expense? +Tags in Expensify refer to line-item details like classes, projects, locations, and customers that help code expenses for accounting and reporting. Tags can be manually created or imported from a connected accounting system. Expensify learns and applies tags automatically over time. + +### [Workflows] What are workflows in Expensify Expense? +Workflows in Expensify Expense allow you to manage expense approvals and submissions: +* **Add Approvals** - Requires additional approval for an expense before payment can be authorized. The default approver is the workspace owner, but it can be changed to another workspace admin. +* **Delay Submissions** - Determines when expenses without issues are automatically submitted. You can set a delay frequency for automatic submissions. + +### [Report Fields] What are report fields in Expensify Expense? +Report fields allow you to add additional details to your reports: +* **Enable Report Fields** - Report fields can be enabled in Workspaces on the Control plan. They provide a way to specify header-level details like project names or locations. +* **Create Report Fields** - Once enabled, report fields can be created for free-text input, date selection, or a list of options. +* **Edit/Delete Report Fields** - Existing report fields can be modified or removed as needed to keep your workspace organized. ## Tutorials -### [Create report] How do I create an expense report? -1. Navigate to **Create** > **Expense Report**. -2. Add your receipts and manually log expenses. -3. Categorize your expenses. -4. Submit the report for approval. +### Expense Reports +#### [Create report] How do I create an expense report? +To create an expense report: +1. Press **Create** > **Expense Report** to start a new report. +2. Add your receipts and manually log expenses for tracking. +3. Categorize your expenses for organization. +4. Submit the report for approval to the relevant supervisor. + +#### [Submit Expenses] What happens after I submit an expense? +After submitting an expense, the next steps depend on whether it was sent to a workspace or an individual: +- **Workspace submissions**: Automatically added to a report, checked for violations, and a chat is created. Reports are submitted for approval every Sunday, but can be manually submitted if ready. +- **Individual submissions**: Sent via email or text, with chat option in Expensify Chat for discussions. + +#### [Approve expenses] How do I approve expense reports? +To approve expense reports: +1. Go to your Inbox and select the report needing approval. +2. Review the receipts and expense details for accuracy. +3. Press **Approve** or **Reject** based on your assessment. + +### Manage Workflows +#### [Enable workflows] How do I enable workflows in Expensify? +To enable workflows: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the desired workspace. +4. Press **More features** in the left menu. +5. Under the Spend section, toggle **Workflows** to enable approval settings. + +#### [Select workflows] How do I select workflow settings? +To select workflow settings: +1. Press **Workflows** in the left menu. +2. Toggle the desired settings: + - **Add Approvals**: Select an approver for expenses requiring additional approval. + - **Delay Submissions**: Choose a frequency for automatic submission of expenses. + +#### [Add approvals] How do I add approvals to a workspace? +To enable Add approvals on a workspace: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to add approvals. +4. Press **Workflows** in the left menu. +5. Toggle **Add approvals**. + +Enabling **Add approvals** reveals the option to set a default approval workflow. + +#### [Configure approval workflows] How do I configure approval workflows in a workspace? +To configure the default approval workflow: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to set the approval workflow. +4. Press **Workflows** in the left menu. +5. Under **Expenses from Everyone**, press **First approver**. +6. Select the workspace member as the first approver. +7. Under **Additional approver**, continue selecting members. +8. Press **Save**. + +To set a custom approval workflow for specific members: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to add approvals. +4. Press **Workflows** in the left menu. +5. Under **Add approvals**, press **Add approval workflow**. +6. Choose the specific member for the custom workflow. +7. Press **Next**. +8. Select the first approver. +9. Press **Next**. +10. Press **Additional approver** to select more members. +11. Press **Add workflow** to save. + +#### [Edit or delete approval workflows] How do I manage approval workflows in Expensify? +To edit an approval workflow: +1. On the **Workflows** page, press the workflow to edit. +2. Press the Approver field for the desired level. +3. Select or deselect members as approvers. +4. Press **Save**. + +To delete an approval workflow: +1. On the **Workflows** page, press the workflow to delete. +2. Press **Delete**. +3. In the confirmation window, press **Delete** again. + +### Set Up Payment Account +#### [Set up payment account] How do I set up a business bank account for workspace payments? +To set up a business bank account for payments: +1. Press **Workflows**. +2. Enable the **Payments** toggle. +3. Press **Connect Bank Account** and follow the prompts to connect your company bank account. +4. Select an authorized expense payer, who is a workspace admin with access to the business bank account. + +### Expense Capture +#### [SmartScan] How do I use SmartScan to capture receipts? +To use SmartScan, follow these steps: +1. Press the **+** icon and select **Submit Expense**. +2. Press **Scan**. +3. Take a photo of a receipt or upload it from your device. SmartScan will auto-populate details like merchant, date, and amount. +4. Use the search field to find the desired workspace or person's name, email, or phone number. +5. Add a description, category, tags, or tax as needed. +6. (Optional) Enable the expense as billable if it should be billed to a client. +7. Press **Submit**. + +#### [Manually add expense] How do I manually add an expense? +To add an expense manually, follow these steps: +1. Press the **+** icon and select **Submit Expense**. +2. Press **Manual**. +3. Enter the amount and press **Next**. Choose a currency if necessary. +4. Use the search field to find the desired workspace or person's name, email, or phone number. +5. (Optional) Add a description. +6. Add a merchant. +7. Press **Show more** to add additional fields like category if needed. +8. Press **Submit**. + +### Manage Expense Tags +#### [Create and manage tags] How do I create and manage expense tags? +To create and manage expense tags in your workspace: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the desired workspace. +4. Press **More features** and enable the **Tags** toggle in the Organize section. +5. Press **Tags**. +6. To add a tag, press **Add Tag**, enter a name, and press **Save**. +7. To delete a tag, press the tag, press the three-dot menu, and select **Delete tag**. + +#### [Enable or disable tags] How do I enable or disable tags for expenses? +To enable or disable tags: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the desired workspace. +4. Press **Tags**. +5. Press the tag and use the toggle to enable or disable it. + +#### [Add or edit a GL code] How do I add or edit a GL code for a tag? +If your workspace is on the Control plan, you can add or edit a GL code for a tag: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace. +4. Press **Tags**. +5. Press the tag to open tag-settings. +6. Click the GL code field, make changes, and press **Save**. + +### Manage Expense Categories +#### [Create categories] How do I create expense categories? +To create expense categories: +1. Press your profile image or icon in the bottom menu. +2. Press **Workspaces**. +3. Select the workspace you want to add categories to. +4. Press **Categories**. +5. Press **Add Category** and enter a name. +6. Press **Save**. + +#### [Delete categories] How do I delete expense categories? +To delete an expense category: +1. Press the category in the **Categories** page. +2. Press the three-dot menu in the top right. +3. Press **Delete category** to permanently delete it. + +#### [Enable or disable categories] How do I enable or disable expense categories? +To enable or disable categories: +1. Press your profile image or icon in the bottom menu. +2. Press **Workspaces**. +3. Select a workspace. +4. Press **Categories**. +5. Press a category and use the toggle to enable or disable it. + +### Require Tags and Categories +#### [Require tags and categories] How do I require tags and categories for expenses? +To require workspace members to add tags and/or categories to their expenses: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select a workspace. +4. Press **Tags** or **Categories** in the left menu. +5. Press **Settings** at the top right of the page. +6. Enable the “Members must tag/categorize all expenses" toggle. +7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). + +This will highlight the tag and/or category field as required on all expenses. Note that expenses can still be submitted without a tag and/or category, but the submitter and approver will see an orange dot on the expense details to alert them that the tag/category is missing. + +### Distance Expenses +#### [Create distance expense] How do I create a distance expense? +To create a distance expense: +1. Press the **+** icon and select **Submit Expense**. +2. Press **Distance**. +3. Enter starting and ending locations. +4. (Optional) Add stops by pressing **Add stop**. +5. Press **Next**. +6. Use the search field to find the desired workspace or person's name, email, or phone number. +7. (Optional) Add a description. +8. Press **Submit**. + +#### [Create and send a distance request] How do I create and send a distance request for reimbursement? +To create and send a distance request for mileage reimbursement: +1. Press the green **+** button and select **Request Money**. +2. Press **Distance** on the Request Money screen. +3. Enter the **Start** and **Finish** addresses and press **Next**. If you have multiple stops, add them before proceeding. +4. Choose the recipient by selecting your organization's workspace from the recent workspaces list. +5. On the confirmation page, review the amount, date, and distance. Optionally, add a description or category. Press **Request**. +6. Your request will be sent to a workspace admin for approval and reimbursement through Expensify or other means. + +### Manage Distance Rates +#### [Enable distance rates] How do I enable distance rates in a workspace? +To enable distance rates in a workspace you manage: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to enable distance rates. +4. Press **More features** in the left menu. +5. Toggle **Distance rates** to enable the feature. + +Once enabled, a new **Distance rates** option will appear in the left menu. + +#### [Add or manage distance rates] How do I add, edit, or delete distance rates? +To manage distance rates: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to manage distance rates. +4. Press **Distance rates** in the left menu. + +To add a rate: +1. Press **Add rate** in the top right. +2. Enter a value and press **Save**. + +To edit or delete a rate: +1. Press the desired distance rate. +2. To enable or disable, use the toggle next to **Enable rate** and press **Save**. +3. To edit, enter the new value and press **Save**. +4. To delete, press **Delete**. + +For bulk actions: +1. Use the checkboxes next to distance rates. +2. Press "x selected" at the top right. +3. Choose **Enable rates**, **Disable rates**, or **Delete rates** as needed. + +### Manage Tax Rates +#### [Enable Taxes] How do I enable taxes on a workspace? +To enable taxes on your workspace: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to enable tax codes. +4. Press **More features** in the left menu. +5. Toggle **Taxes** to enable the feature. + +After enabling taxes, a new **Taxes** option will appear in the left menu. + +#### [Add or manage tax rates] How do I add, edit, or delete tax rates? +To manage tax rates: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace where you want to manage tax rates. +4. Press **Taxes** in the left menu. + +To add a rate: +1. Press **Add rate** in the top right. +2. Enter a name, value, and tax code, then press **Save**. + +To edit or delete a rate: +1. Press the desired tax rate. +2. To enable or disable, use the toggle next to **Enable rate** and press **Save**. +3. To edit, enter the new value and press **Save**. +4. To delete, press **Delete**. + +For bulk actions: +1. Use the checkboxes next to tax rates. +2. Press "x selected" at the top right. +3. Choose **Enable rates**, **Disable rates**, or **Delete rates** as needed. + +#### [Change Default Tax Rates] How do I change the default tax rates in a workspace? +To change the default tax rates: +1. On the **Taxes** settings page, press **Settings** in the top right. +2. Press **Workspace currency default** or **Foreign currency default** and select the desired tax rate. + +### Bank Account Management +#### [Connect Business Bank Account] How do I connect a business bank account in Expensify? +To connect a business bank account: +1. Enable the Make or Track Payments Workflow by navigating to **Workspaces** > **More Features** > **Enable Workflows**, then press **Workflows** and enable **Make or Track Payments**. +2. Press **Connect Bank Account** and select either **Connect Online with Plaid** or **Connect Manually**. +3. Enter your bank details. +4. Upload a photo of your ID and take a selfie video for verification. +5. Enter your company information, including business name, address, tax ID, and website. +6. Provide additional information on beneficial owners if applicable. +7. Verify all details are accurate and accept the agreement terms. + +#### [Validate Business Bank Account] How do I validate a business bank account in Expensify? +To validate your business bank account: +1. Navigate to **Settings > Workspaces > _Workspace Name_ > Bank account** to check the status. +2. If the status is **Verifying**, check your email for further instructions. If **Pending**, proceed to the next step. +3. Wait 1-2 business days for Expensify to send three test transactions to your bank account. +4. In the **Bank accounts** section of your workspace settings, input the transaction amounts as prompted. + +Once completed, your business bank account is validated and ready for use in Expensify. + +#### [Unlock Business Bank Account] How do I unlock a business bank account? +If your business bank account is locked due to a rejected withdrawal request, follow these steps to unlock it: +1. Go to **Settings > Workspaces > _Workspace Name_ > Bank account** and press **Fix**. This sends a request to our support team to review the reason for the lock. They will provide you with the necessary next steps. +2. Be patient, as unlocking the account can take several business days due to ACH processing times and clawback periods. + +If you need to enable direct debits from your verified bank account, provide your bank with the following details: +- **For Expensify**: + - ACH CompanyIDs: 1270239450, 4270239450, 2270239450 + - ACH Originator Name: Expensify +- **For Bill Payments with Stripe**: + - ACH CompanyIDs: 1800948598, 4270465600 + - ACH Originator Name: expensify.com +- **For International Reimbursements with CorPay**: + - ACH CompanyIDs: 1522304924, 2522304924 + - ACH Originator Name: Cambridge Global Payments + +#### [Connect Personal Bank Account] How do I connect a personal bank account to Expensify? +To connect a personal bank account for receiving payments and reimbursements: +1. Press your profile image or icon in the bottom left menu. +2. Press **Wallet**. +3. Press **Add Bank Account** to initiate the process. +4. Press **Continue** to redirect to Plaid for secure bank account verification. +5. Follow the prompts to enter your bank account details via Plaid. +6. Once done, return to Expensify to complete the linking process. +7. Choose the account you wish to connect and press **Save & continue**. + +Once connected, payments and reimbursements will be automatically deposited into the linked bank account. + +### Invoice Management +#### [Enable Invoicing] How do I enable invoicing on a workspace? +To enable invoicing: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** and select the workspace for which you want to enable invoicing. +3. Press **More features** in the left menu. +4. Under the Earn section, enable the **Invoice** toggle. + +#### [Send Invoice] How do I send an invoice using Expensify? +To send an invoice: +1. Press the **+** icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and press **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including description, date, category, tag, and/or tax. +5. Press **Send**. + +#### [Receive Invoice Payment] How do I receive invoice payments? +If you have not connected a business bank account to receive invoice payments, you will see an **Invoice balance** in your Wallet. Expensify will automatically transfer these invoice payments once a business bank account is connected. + +#### [Pay an Invoice] How do I pay an invoice in Expensify? +To pay an invoice in Expensify, follow these steps: + +1. Press the link in the email or text notification you receive from Expensify. +2. Press **Pay**. +3. Choose to **Pay as an individual** or **Pay as a business**. +4. Press **Add Bank Account** or **Add debit or credit card** to issue payment. + +You can also view all unpaid invoices by searching for the sender’s email or phone number on the left-hand side of the app. The invoices waiting for your payment will have a green dot. + +### Expense Management +#### [Approve and Pay Expenses] How do I approve and pay expenses in Expensify? +To manage expenses effectively, follow these steps: + +1. **Manually Approve an Expense**: + - Open the Expensify Chat thread for the expense. + - Press the expense or group of expenses. + - Review the details, ensuring receipt, amount, and description accuracy. + - Determine the next step: Approve, hold, or request changes. + +2. **Approve Expenses**: + - Open the Expensify Chat thread for the expense. + - Press the expense or group of expenses. + - Review the expense details for correctness. + - Decide the next steps: + - **Approve**: When satisfied, press **Approve**. + - **Handle Holds**: Choose to approve non-held expenses or the full amount, including held ones. + - **Request Changes**: Add a comment in the chat thread to request any changes. + +3. **Hold an Expense**: + - Open the Expensify Chat thread for the expense. + - Press the expense or group of expenses. + - Press the three-dot menu and select **Hold**. + - Enter a reason for the hold. + - Review the hold overview and press **Got It**. + - When ready, remove the hold or approve the expense. + +4. **Unapprove an Expense**: + - Press the workspace logo in the top left. + - Select the workspace with the expense report. + - Search for the approved report. + - Press the dropdown arrow for report actions. + - Press **Unapprove**. + +5. **Pay Expenses**: + - Open the Expensify Chat thread for the expense. + - Press the expense or group of expenses. + - Select a payment option: + - Press **Pay** to pay the full amount within Expensify. + - Press **Pay Elsewhere** if payment is made outside Expensify. + +#### [Review & Resolve Duplicates] How do I handle duplicate expense requests? +To review and resolve duplicate expenses: +1. Press the red dot in the left menu or open the expense’s chat room to view the flagged request. +2. Press the green **Review duplicates** button at the top of the request. +3. Review the list of potential duplicates. +4. To resolve a duplicate, press either **Keep all** or **Keep this one**. + - **Keep all**: Retains all expenses as separate charges and removes the hold. + - **Keep this one**: Retains this expense and discards its other related duplicates. +5. If discrepancies exist between the duplicates (e.g., category, tags), choose which details to keep. +6. Confirm your selection to merge the requests or keep all. + +The expenses are removed from the duplicates list and the hold is removed. -### [SmartScan] How do I use SmartScan to capture receipts? -1. Snap a photo of your receipt or upload it to Expensify. -2. Let SmartScan automatically detect and categorize the expense. +#### [Track Expenses] How do I track expenses in Expensify? +To create, store, or share non-reimbursable expenses using the Track Expenses feature: +1. Press the **+** icon in the bottom menu and select **Track Expense**. +2. Create the expense manually, scan the receipt, or add a distance expense. +3. Choose the next steps for the expense: + - **Submit it to someone**: Select this option to request payment from a contact or other members of your Expensify workspace. + - **Categorize it**: Select this option to choose a category and additional details to code the expense for a specific workspace. The expense will then be placed on a report and can be submitted to the workspace for approval. + - **Share it with my accountant**: Select this option to share the expense with your accountant. The expense will then be placed on a report under the workspace for your accountant to review. + - **Nothing for now**: Select this option to store the expense. Expensify will keep the expense until you are ready to take action on it—it won’t expire. When you’re ready, you can then select one of the above options for the expense at a later time. -### [Approve expenses] How do I approve expense reports? -1. Go to your Inbox and select the report requiring approval. -2. Review the receipts and expense details. -3. Click **Approve** or **Reject**. +#### [Split an Expense] How do I split an expense with others? +Splitting an expense allows the person who paid the bill to request money from multiple people who will split the cost with them. To split an expense: +1. Press the **+** icon and select **Split Expense**. +2. Upload a photo of your receipt or manually enter the total bill amount. +3. Press **Next**. +4. Enter the names, email addresses, or phone numbers for the people you want to request money from. Note: You can select multiple people. +5. Press **Next**. +6. (Optional) Enter a reason for the request in the Description field. +7. (Optional) If you manually entered the bill amount, add the merchant and date of purchase. +8. Press **Split**. + +Each person will receive an email or text with the details of the request. You can also chat with them about the expense in Expensify Chat, and you can receive payments through your Expensify Wallet or outside of Expensify. + +### Manage Report Fields +#### [Enable Report Fields] How do I enable report fields on a workspace? +To enable report fields on a workspace: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace you want to enable report fields for. +4. Press **More features** and toggle **Report Fields** to enable them. + +#### [Create Report Fields] How do I create new report fields? +To create new report fields: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace you want to create report fields on. +4. Press **Report Fields** in the left menu. +5. Press **Add Field** in the top right corner to create a new field. +6. Enter a name for your report field and select the field type (Text, Date, or List). +7. Press **Save** to finalize the new field. + +#### [Edit or Delete Report Fields] How do I edit or delete existing report fields? +To edit or delete existing report fields: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** in the left menu. +3. Select the workspace with the report fields you want to edit or delete. +4. Press **Report Fields** in the left menu. +5. Select the report field you wish to edit or delete. +6. Make the required edits in the right-hand panel, or press **Delete**. + +### Accounting Integrations +#### [Connect to QuickBooks Online] How do I connect Expensify to QuickBooks Online? +To integrate with QuickBooks Online: +1. Press your profile image or icon in the bottom left menu to access settings. +2. Press **Workspaces** and select the workspace you want to connect to QuickBooks Online. +3. Press **More features** and enable the Accounting toggle. +4. Press **Accounting** and then **Set up** next to QuickBooks Online. +5. Enter your Intuit login details to import your settings. + +#### [Configure QuickBooks Online] How do I configure QuickBooks Online settings in Expensify? +Configuring QuickBooks Online involves setting import, export, and advanced settings for seamless integration with Expensify. + +1. **Import Settings**: + - Under Accounting, select **Import** under QuickBooks Online. + - Review settings for Chart of Accounts, Classes, Customers/Projects, Locations, and Taxes. + +2. **Export Settings**: + - Under Accounting, select **Export** for QuickBooks Online. + - Review settings for Preferred Exporter, Export Out-of-Pocket Expenses, and Invoices. + +3. **Advanced Settings**: + - Select **Advanced** under QuickBooks Online. + - Set options for Auto-sync, Invite Employees, Automatically Create Entities, and Sync Reimbursed Reports. + +#### [Connect to Xero] How do I connect Expensify to Xero? +To integrate with Xero: +1. Press your profile image or icon in the bottom left menu to access settings. +2. Press **Workspaces** and select your desired workspace. +3. Press **More features** and enable the Accounting toggle. +4. Press **Accounting** and then **Set up** next to Xero. +5. Enter your Xero login details to import your settings. + +#### [Configure Xero] How do I configure Xero settings in Expensify? +To configure Xero settings: +1. Under the Accounting settings for your workspace, press **Import** under the Xero connection. +2. Select options for settings like Xero organization, Chart of Accounts, Tracking Categories, Re-bill Customers, and Taxes. +3. Under the Accounting settings, press **Export** for Xero connection configuration. +4. Review export settings like Preferred Exporter, Export Out-of-Pocket Expenses, and Xero Bank Account. +5. Press **Advanced** under Xero connection to set Auto-sync, Set Purchase Bill Status, Sync Reimbursed Reports, and other advanced settings. + +#### [Connect to Sage Intacct] How do I connect Expensify to Sage Intacct? +To integrate with Sage Intacct: +1. In Expensify, go to **Settings > Workspaces > [Workspace Name] > Accounting**. +2. Press **Set up** next to Sage Intacct and enter your credentials. +3. Press **Confirm** to finalize the setup. + +#### [Configure Sage Intacct] How do I configure Sage Intacct settings in Expensify? +To configure Sage Intacct: +1. Navigate to **Accounting settings** and select **Entity** under Sage Intacct to choose the entity. +2. Press **Import** to set preferences for categories, expenses, and dimensions. +3. Press **Export** to choose exporter and methods for expenses. +4. Press **Advanced** to enable features like auto-sync. + +#### [Connect to NetSuite] How do I connect Expensify to NetSuite? +To integrate with NetSuite: +1. Log into Expensify as a workspace admin and press your profile image or icon in the bottom left menu. +2. Scroll down and press **Workspaces** and select the workspace you want to connect to NetSuite. +3. Press **More features** and enable the Accounting toggle. +4. Press **Accounting** and then **Set up** next to NetSuite. +5. Enter your NetSuite Account ID, Token ID, and Token Secret. These can be found in NetSuite under **Setup > Integration > Web Services Preferences**. +6. Press **Confirm** to complete the setup. + +#### [Configure NetSuite] How do I configure NetSuite settings in Expensify? +To configure NetSuite settings: +1. Ensure the Expensify Bundle is installed in NetSuite by going to **Customization > SuiteBundler > Search & Install Bundles**. +2. Enable Token-Based Authentication in NetSuite under **Setup > Company > Enable Features > SuiteCloud > Manage Authentication**. +3. Add the Expensify Integration Role to a user in NetSuite under **Lists > Employees** and manage access. +4. Create Access Tokens in NetSuite by entering "page: tokens" in the Global Search and selecting **New Access Token**. +5. Confirm Expense Categories and Reports are enabled in NetSuite under **Setup > Accounting** and **Employees**. +6. Follow the detailed steps for ensuring transaction forms are properly configured in NetSuite for Expense Reports, Journal Entries, Vendor Bills, and Credits. + +### Exporting Data +#### [Export Expenses] How do I export expenses to a CSV file? +To export your expense data to a CSV file: +1. Press the **Search** tab in the bottom left menu to view your expenses. +2. Select the checkbox next to the expenses or reports you wish to export. +3. Press **# selected** at the top-right and select **Download** to export. + + The CSV download will save locally to your device with the file naming prefix "Expensify." This file includes data such as Date, Merchant, Description, From, To, Category, Tag, Tax, Amount, Currency, Type, and Receipt URL. + +#### [Exporting Reports to Xero] How do I export reports to Xero manually? +If an error occurs during an automatic export to Xero: +1. Check your email or the related Workspace Chat for error notifications. +2. Resolve the issue by opening the expense and making necessary changes. +3. Ensure the report is in the Approved, Closed, or Reimbursed state. +4. An admin must press the heading at the top of the expense, select **Export**, and then choose **Xero**. + +#### [Exporting Reports to QuickBooks Online] How do I manually export reports to QuickBooks Online? +If an error occurs during an automatic export to QuickBooks Online: +1. Check your email or the related Workspace Chat for error notifications. +2. Open the expense and make necessary changes. +3. Ensure the report is in the Approved, Closed, or Reimbursed state. +4. An admin must press the heading at the top of the expense, select **Export**, and then choose **QuickBooks Online**. + +#### [QuickBooks Online Manual Export Troubleshooting] Why can't I manually export a report to QuickBooks Online? +To export a report to QuickBooks Online, the report must be in the Approved, Closed, or Reimbursed state. If the report is in the Open state, pressing **Export** will lead to an empty page. Ensure the report is submitted or approved if it's in the Processing state. Once these changes are made, an admin can manually export the report to QuickBooks Online. ## FAQ -### Why should I use Expensify Expense for my business? -Expensify Expense automates time-consuming processes like receipt capture, approval workflows, and reimbursement, saving you time and improving accuracy. +### General Inquiries +#### Why should I use Expensify Expense for my business? +Expensify Expense automates processes like receipt capture, workflows, and reimbursement, saving time and improving accuracy. + +#### How do SmartScan limits work? +SmartScan allows you to scan a set number of receipts each month for free, with more available under paid plans. + +#### Can I use Expensify Expense for free? +Yes, Expensify Expense offers a free plan with basic features, with advanced plans for larger business needs. + +#### How does Expensify support multi-currency expenses? +Expensify converts expenses to your preferred currency and supports global reimbursement. + +### Workflow Management +#### [Delayed Submission and Approvals] If I have delayed submission and an approver, what should I expect with a report? +When Add Approver is enabled with Delay Submission, expense reports go from Open > Processing > Approved. If delayed submission is disabled, expense reports go from Processing > Approved. + +#### Can an employee have more than one approval workflow? +No, each employee can have only one approval workflow. + +### Integration and Export +#### [Disconnect from Xero] How do I disconnect Xero from Expensify? +To disconnect Xero: +1. Press your profile image or icon in the bottom left menu. +2. Press **Workspaces** and select your workspace. +3. Press **Accounting**. +4. Press the three-dot menu next to Xero and select **Disconnect**. +5. Press **Disconnect** again to confirm. + +You will no longer see the imported options from Xero. + +#### [Xero Export Confirmation] How do I know if a report successfully exported to Xero? +When a report exports successfully, a message is posted in the related Expensify Chat room. + +#### [Duplicate Report Handling] What happens if I manually export a report that has already been exported? +When an admin manually exports a report, Expensify will warn them if the report has already been exported. If the admin chooses to export it again, it will create a duplicate report in Xero. You will need to delete the duplicate entries from within Xero. + +#### [Auto Sync Impact] What happens to existing reports that have already been approved and reimbursed if I enable Auto Sync? +- If Auto Sync was disabled when your Workspace was linked to Xero, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in Xero during the next sync. +- If a report has been exported and marked as paid in Xero, it will be automatically marked as reimbursed in Expensify during the next sync. +- If a report has not yet been exported to Xero, it won’t be automatically exported. + +#### [Report Export to Sage Intacct] Why wasn't my report automatically exported to Sage Intacct? +There are a number of factors that can cause auto-export to fail. If this happens, you will find the specific export error in the report comments for the report that failed to export. Once you’ve resolved any errors, you can manually export the report to Sage Intacct. + +#### [Negative Expenses to Sage Intacct] Can I export negative expenses to Sage Intacct? +Yes, you can export negative expenses to Sage Intacct. If you are exporting out-of-pocket expenses as expense reports, then the total of each exported report cannot be negative. + +#### [NetSuite Plan Requirement] What type of Expensify plan is required to connect to NetSuite? +You need a Control workspace to integrate with NetSuite. If you have a Collect workspace, you will need to upgrade to Control. + +#### [NetSuite Page Size] What should I set my page size to in NetSuite for importing customers and vendors? +Make sure your page size is set to 1000 in NetSuite for importing your customers and vendors. Go to **Setup > Integration > Web Services Preferences** and search **Page Size** to determine your page size. + +#### [NetSuite Export Options] What are the export options for NetSuite? +You can export out-of-pocket expenses and company card expenses as Expense Reports, Vendor Bills, or Journal Entries in NetSuite. For invoices, select an Accounts Receivable account. Export settings can be configured to choose the date for records, export foreign currency amounts, and export to the next open period if a period is closed. + +#### [QuickBooks Online Error Resolution] Why do I see a red dot next to my QuickBooks Online connection? +If there is an error with your connection, you’ll see a red dot next to Accounting in the left menu. When you press Accounting, you’ll also see a red dot displayed next to the QuickBooks Online connection card. This may occur if you incorrectly enter your QuickBooks Online login information when trying to establish the connection. To resubmit your login details: +1. Press the three-dot menu to the right of the QuickBooks Online connection. +2. Press **Enter credentials**. +3. Enter your Intuit login details to establish the connection. + +#### [QuickBooks Online Export Confirmation] How do I know if a report is successfully exported to QuickBooks Online? +When a report exports successfully, a message is posted in the expense’s related chat room. + +#### [Duplicate Report Handling in QuickBooks Online] What happens if I manually export a report that has already been exported? +When an admin manually exports a report, Expensify will notify them if the report has already been exported. Exporting the data again will create a duplicate report in QuickBooks Online. + +#### [Auto Sync Impact for QuickBooks Online] What happens to existing approved and reimbursed reports if I enable Auto Sync? +- If Auto Sync was disabled when your Workspace was linked to QuickBooks Online, enabling it won’t impact existing reports that haven’t been exported. +- If a report has been exported and reimbursed via ACH, it will be automatically marked as paid in QuickBooks Online during the next sync. +- If a report has been exported and marked as paid in QuickBooks Online, it will be automatically marked as reimbursed in Expensify during the next sync. + +Reports that have yet to be exported to QuickBooks Online won’t be automatically exported. + +#### [Report Exporting to Xero Troubleshooting] Why can't I manually export a report to Xero? +To export a report to Xero, it must be in the Approved, Closed, or Reimbursed state. If it is in the Open state, pressing **Export** will lead to a notification that the data is not yet available for export. Make sure the report is submitted or approved if it's in the Processing state. Once these changes are made, an admin can manually export the report to Xero. + +### Exporting and Downloading Options +#### [CSV Export Options] Can I export in a different format, like PDF or XLS? +No, currently Expensify supports CSV export only. + +#### [CSV Customization] Can I add columns to the CSV download to capture additional data points? +No, the CSV template cannot be customized. + +#### [Bulk Selection] Can I select expenses or reports in bulk for exporting? +Yes, you can select expenses or reports in bulk by using the **Select multiple** or **Select all** option. To display these options on the mobile app, simply long press an item. + +### Invoicing and Payment +#### [Workspace Requirement] Why do I need to create a workspace to send an invoice? +A workspace is a configuration of settings related to your business. Since invoicing is considered a business feature, you must have a workspace to configure and use invoicing. + +#### [Invoice Communication] How do I communicate with the sender/recipient about the invoice? +Expensify will automatically notify the invoice recipient about the new invoice via email, SMS, and a mobile app notification, along with instructions on how to pay it. Daily reminders will be sent until the invoice is paid. Additionally, an invoice chat room will be automatically created in Expensify between the invoice sender, their workspace admins, and the payer. You can use this chat to discuss anything related to the invoice. + +#### [Invoice Export] Can you export invoices between an accounting integration? +Yes, you can export invoices between Expensify and your connected accounting integration. + +#### [Invoice Permissions] Who can send and pay an invoice? +All workspace admins will be able to send and pay invoices. Invoices can also be paid by anyone, including recipients without an Expensify account. + +#### [Disable Invoicing] What happens if I disable invoicing in the future? +When invoicing is disabled, all previously created invoice rooms and historical invoices will remain unaffected and continue to exist. However, all workspace admins will no longer have the option to send an invoice. + +#### [Business Bank Account Error] Why am I getting an error after I enter my website when connecting a business bank account? +We can only accept a private domain website to ensure the security of your business. If you receive an error when entering your website, it is likely because the domain is not recognized as private. Make sure you are using a business email with a private domain. If you continue to experience issues, contact our support team at concierge@expensify.com for further assistance. + +### Duplicate Handling +#### [Duplicate Expense Handling] What should I do if an expense is flagged as a duplicate? +If an expense is flagged as a duplicate, you can review and resolve it by selecting to keep all duplicates or only one. Adjust and confirm any discrepancies before finalizing your choice. + +#### [Duplicate Detection Criteria] When are expenses flagged as duplicates? +Expenses are flagged as duplicates if they have the same date and amount unless: +- They were split from a single expense. +- They were imported from a credit card. +- Matching email receipts were received with different timestamps. + +#### [Concierge Duplicate Alert] What should I do if Concierge flags a receipt as a duplicate? +If Concierge flags a receipt as a duplicate, scanning the receipt again will trigger the same alert. You can review these in the deleted filter on Expensify Classic. + +#### [Edit Duplicate Requests] Can I edit a duplicate request once resolved? +Yes, you can edit a duplicate request after it has been resolved, but ensure the hold is first removed. + +#### [Review Discarded Duplicates] Can I review a discarded duplicate later? +Yes, approvers can review discarded duplicates to ensure accuracy and prevent fraud. + +### Invoice Payment Options +#### [Invoice Payment Options] What are the payment options for invoices in Expensify? +When paying an invoice, you can choose to pay as an individual or as a business. You can add a bank account or use a debit or credit card to issue payment. Only the person who received the invoice will see the option to pay it. If you want to pay an invoice outside of Expensify, you will need to coordinate with the vendor to discuss alternative payment options. + +#### [Adding Payment Methods] Can I add additional payment methods for paying invoices? +Yes, you can add additional payment methods to your Expensify Wallet. To do this, go to Account Settings > Wallet, then press Add Bank Account. This allows you to choose a payment method when paying future invoices. + +#### [Invoice Sending Limitations] Can anyone send an invoice through Expensify? +Only Expensify customers can send an invoice. This feature is designed to ensure that businesses using Expensify can manage their invoice billing processes efficiently while providing flexibility for their customers to make payments. + +#### [Invoice Visibility] Can someone other than the recipient pay an invoice? +No, only the person who received the invoice will see the option to pay it. This ensures secure and accurate processing of invoice payments. + +#### [Unpaid Invoices] How can I view unpaid invoices? +To view all unpaid invoices, search for the sender’s email or phone number on the left-hand side of the app. Invoices awaiting your payment will have a green dot. + +#### [Invoice Chat Communication] How can I communicate about an invoice? +You can chat directly with your vendor at expensify.com in the designated invoice room to discuss invoice-related matters. + +### Bank Account Requirements +#### [Business Bank Account Requirements] What are the general requirements for adding a business bank account? +To add a business bank account to issue reimbursements via ACH (US) or to issue Expensify Cards: +- Enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We cannot accept a PO Box or MailDrop location. +- We are required by law to verify your identity. Part of this process requires you to verify a US-issued photo ID. Your ID must be issued by the United States to use features related to US ACH. You and any Beneficial Owner (if one exists) must also have a US address. + +#### [Beneficial Owner Definition] What is a Beneficial Owner? +A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. + +#### [Beneficial Owner Details] What do I do if the Beneficial Owner section only asks for personal details, but my organization is owned by another company? +Please indicate you have a Beneficial Owner only if it is an individual who owns 25% or more of the business. + +#### [Address and ID Verification] Why can’t I input my address or upload my ID? +When adding a verified business bank account in Expensify, the individual adding the account and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account and then share access with you once it is verified. + +#### [Document Requests] Why am I asked for documents when adding my bank account? +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. + +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. + +If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. + +### Bank Account Validation +#### [Microtransaction Validation] I don’t see all three microtransactions I need to validate my bank account. What should I do? +Wait until the end of the second business day. If you still don’t see them, please contact your bank and ask them to whitelist our ACH IDs **1270239450**, **4270239450**, and **2270239450**. Expensify’s ACH Originator Name is "Expensify." + +Once that's all set, make sure to contact your account manager or concierge, and our team will be able to re-trigger those three test transactions! + +#### [Test Deposits for Validation] How many test deposits will I receive when validating my business bank account? +You will receive two withdrawals and one deposit in your business bank account to complete the validation process. + +#### [Missing Test Deposits] What should I do if I don't see the test deposits in my business bank account after two business days? +If the test deposits are not visible after two business days, it may be due to direct debits not being enabled on your bank account. In such cases, provide your bank with the following details: +- ACH CompanyIDs: 1270239450, 4270239450, 2270239450 +- ACH Originator Name: Expensify + +If the issue persists, please contact Expensify Support for further assistance. + +### Sage Intacct Integration +#### [Configure Sage Intacct] How do I set up and configure Sage Intacct in Expensify? +To connect and configure Sage Intacct: +1. **Connect to Sage Intacct**: Go to **Settings > Workspaces > [Workspace Name] > Accounting** and press **Set up** next to Sage Intacct, then enter your credentials to complete the setup. +2. **Select Entity**: Choose the Sage Intacct entity to connect each Expensify workspace to, especially for multi-entity setups. +3. **Import Settings**: Navigate to Accounting settings, press **Import** under Sage Intacct, and set preferences for expense types, categories, dimensions, customers/projects, and tax. +4. **Export Settings**: Access export options under **Export**, choosing your preferred exporter and export methods for out-of-pocket and company card expenses. +5. **Advanced Settings**: Enable auto-sync, invite employees, and configure reimbursement sync under **Advanced** settings to ensure seamless integration. -### How do SmartScan limits work? -SmartScan allows you to scan a set number of receipts each month for free, with additional scans available under paid plans. +#### [Frequently Asked Questions] What are common questions about using Sage Intacct with Expensify? +Some common concerns include: +- **Auto-sync**: Only newly approved reports will be auto-exported to Sage Intacct. Existing approved reports must be manually exported. +- **Negative Expenses**: Negative expenses can be exported, but out-of-pocket expense reports cannot be entirely negative. +- **Export Errors**: If auto-export fails, check report comments for specific errors and resolve them before attempting manual export. -### Can I use Expensify Expense for free? -Yes, Expensify Expense offers a free plan with basic features, and advanced plans are available for businesses with larger needs. +### Sage Intacct Tutorials +#### [Configure Import Settings] How do I configure import settings for Sage Intacct? +To configure import settings: +1. Under Accounting settings, press **Import** for Sage Intacct. +2. Choose how to import categories, dimensions, customers/projects, and tax. +3. Configure expense types and chart of accounts based on how you plan to export expenses. +4. Set up mapping for billable expenses by enabling necessary permissions in Sage Intacct. -### How does Expensify support multi-currency expenses? -Expensify automatically converts expenses to your preferred currency and supports global reimbursement. +#### [Configure Export Settings] How do I configure export settings for Sage Intacct? +To configure export settings: +1. In Accounting settings, press **Export** under Sage Intacct. +2. Choose the preferred exporter and export date options. +3. Decide whether to export out-of-pocket expenses as expense reports or vendor bills. +4. Set export preferences for company card expenses, selecting between credit card charges or vendor bills. +#### [Manage Advanced Settings] How do I manage advanced settings for Sage Intacct? +To manage advanced settings: +1. Navigate to **Settings > Workspaces > [Workspace name] > Accounting** and press **Advanced** under Sage Intacct. +2. Enable auto-sync for daily updates and automatic export of expenses. +3. Use the invite employees feature to add Sage Intacct users to Expensify. +4. Configure reimbursement sync to reflect accurate status between Expensify and Sage Intacct. \ No newline at end of file diff --git a/help/index.md b/help/index.md index dde0e97da851..45366fecae11 100644 --- a/help/index.md +++ b/help/index.md @@ -3,558 +3,503 @@ layout: product title: Expensify --- ## Introduction -The Expensify Superapp packs the full power of 6 world-class business, finance, and collaboration products into a single app that works identically on desktop and mobile, efficiently with your colleagues, and seamlessly with your customers, vendors, family, and friends. +The Expensify Superapp combines 6 world-class business, finance, and collaboration products into one app. It works identically on desktop and mobile, with colleagues and customers, and for personal use. -### [Main uses] When should I use Expensify? +### Main uses Expensify can do a lot. You should check us out whenever you need to: -* **Track and manage expenses** - Whether you are reimbursing employee receipts, deducting personal expenses, or just splitting the bill, Expensify Expense is for you. -* **Issue corporate cards** - Skip the reimbursement and capture receipts electronically in real-time by issuing the Expensify Card to yourself and your employees. -* **Book and manage travel** - If you are booking your own business trip, arranging a trip for a colleague, or managing the travel of your whole company, Expensify Travel has got you covered. -* **Chat with friends and coworkers** - Whether it's collaborating with your team, supporting your client, negotiating with your vendor, or just saying Hi to a friend, Expensify Chat connects you with anyone with an email address or SMS number. -* **Collect invoice payments online** - Expensify Invoice allows you to collect online payments from consumers and businesses alike – anyone with an email address or SMS number. -* **Approve and pay bills online** - Scan, process, and approve bills online using Expensify Billpay, then we'll pay them electronically or via check, whatever they prefer. - -If you send, receive, or spend money – or even just talk to literally anyone, about literally anything – Expensify is the tool for you. - -### [Core users] Who uses Expensify? -Expensify offers something for everyone. Some people who commonly use us include: -* **Individuals** - Millions of individuals use Expensify to track personal expenses to maximize their tax deductions, stay within personal budgets, or just see where their money is going. -* **Friends** - Expensify is a great way to split bills with friends, whether it's monthly rent and household expenses, a big-ticket bachelorette party, or just grabbing drinks with friends. -* **Employees** - Road warriors and desk jockeys alike count on Expensify to reimburse expense reports they create in international airports, swanky hotels, imposing conference centers, quaint coffee shops, and boring office supply stores around the world. -* **Managers** - Bosses manage corporate spend with Expensify to empower their best (and keep tabs on their… not-so-best), staying ahead of schedule and under budget. -* **Accountants** - Internal accountants, fractional CFOs, CAS practices – you name it, they use Expensify to Invoice customers, process vendor bills, capture eReceipts, manage corporate spend: the whole shebang. If you're an accountant, we're already best friends. -* **Travel managers** - Anyone looking to manage employee travel has come to the right place. - -If you are a person online who does basically anything, you can probably do it with Expensify. - -### [Key advantages] Why should I use Expensify? -Though we do a lot, you've got a lot of options for everything we do. But you should use us because we are: -* **Simple enough for individuals** - We've worked extremely hard to make a product that strips out all the complex jargon and enterprise baggage, and gives you a simple tool that doesn't overwhelm you with functionality and language you don't understand. -* **Powerful enough for enterprises** - We've worked extremely hard to make a product that "scales up" to reveal increasingly sophisticated features, but only to those who need it, and only when they need it. Expensify is used by public companies, multinational companies, companies with tens of thousands of employees, non-profits, investment firms, accounting firms, manufacturers, and basically every industry in every currency and in every country around the world. If you are a company, we can support your needs, no matter how big or small. -* **6 products for the price of 1** - Do you pay for an expense management system? A corporate card? A travel management platform? An enterprise chat tool? An invoicing tool? A billpay tool? Now you don't need to. Expensify's superapp design allows us to offer ALL these features on a single platform, at probably less than what you pay for any of them individually. -* **Supports everyone everywhere** - Expensify works on iPhones and Androids, desktops and browsers. We support every currency and can reimburse to almost any country. You don't need to be an IT wizard – if you can type in their email address or SMS number, you can do basically everything with them. -* **You get paid to use it** - Do you spend money? Spend it on the Expensify Card and we pay you up to 2% cashback. It's your money after all. -* **Revenue share for accountants** - Do you manage the books for a bunch of clients? Become an Expensify Approved Accountant and take home 0.5% revenue share. Or share it with your clients as a discount, up to you! - -You are in the driver's seat; we're here to earn your business. But we're going to work harder for you than the other guys, and you won't be disappointed. +* **Track and manage expenses** - Whether reimbursing employee receipts, deducting personal expenses, or splitting a bill, Expensify Expense is for you. +* **Issue corporate cards** - Skip reimbursement and capture receipts in real-time by issuing the Expensify Card to yourself and employees. +* **Book and manage travel** - If booking your own trip, arranging for a colleague, or managing company travel, Expensify Travel has you covered. +* **Chat with friends and coworkers** - Collaborate with your team, support clients, negotiate with vendors, or just say Hi with Expensify Chat. +* **Collect invoice payments online** - Expensify Invoice lets you collect online payments from anyone with an email or SMS number. +* **Approve and pay bills online** - Scan, process, and approve bills online with Expensify Billpay, and we'll pay them electronically or via check. + +If you send, receive, or spend money – or talk to anyone about anything – Expensify is for you. + +### Core users +Expensify offers something for everyone. Common users include: +* **Individuals** - Millions track personal expenses to maximize deductions, stay within budgets, or see where money goes. +* **Friends** - Split bills with friends for rent, parties, or drinks. +* **Employees** - Reimburse expense reports from airports, hotels, conference centers, or coffee shops. +* **Managers** - Manage corporate spend, empowering the best and keeping tabs on the rest, staying on schedule and budget. +* **Accountants** - Internal accountants, CFOs, CAS practices use Expensify to invoice customers, process vendor bills, and manage spend. +* **Travel managers** - Manage employee travel easily with Expensify. + +If you're online doing anything, Expensify can probably help. + +### Key advantages +You've got options, but use Expensify because it is: +* **Simple enough for individuals** - A simple tool without overwhelming functionality or language. +* **Powerful enough for enterprises** - It scales up to reveal sophisticated features only when needed. Used by public companies, multinationals, and more. +* **6 products for the price of 1** - Offers expense management, corporate cards, travel management, chat, invoicing, and billpay in one platform. +* **Supports everyone everywhere** - Works on iPhones, Androids, desktops, and browsers, supporting every currency and reimbursing to almost any country. +* **You get paid to use it** - Spend on the Expensify Card and earn up to 2% cashback. +* **Revenue share for accountants** - Manage client books and earn or share a 0.5% revenue share. + +You are in the driver's seat, and we're here to earn your business. ## Concepts -The Expensify Superapp has a lot of moving pieces, so let's break them down one by one. - -### [Superapp] What makes Expensify a superapp? -A "superapp" is a single app that combines multiple products into one seamlessly interconnected experience. Expensify isn't a "suite" of separate products linked through a single account – Expensify is a single app with a single core design that can perform multiple product functions. The secret to making such a seamless experience is that we build all product functions atop the same common core: -* **App** - The basis of the superapp experience is the actual app itself, which runs on your mobile phone or desktop computer. -* **Chats** - Even if you don't plan on using Expensify Chat for enterprise-grade workspace collaboration, chat is infused through the entire product. -* **Expense** - Even if you aren't actively managing your expenses, you've still got them. Every product that deals with money is ultimately dealing with expenses of some kind. -* **Workspace** - Though Expensify works great for our millions of individual members, every product really shines when used between groups of members sharing a "workspace." -* **Domain** - To support more advanced security features, many products provide extra functionality to members who are on the same email "domain." - -These are the foundational concepts you'll see again and again that underpin the superapp as a whole. - -### [App screens] What is the Expensify app? -Just like your eyes are a window to your soul, the Expensify App is the doorway through which you experience the entire global world of interconnected chat-centric collaborative data that comprises the Expensify network. The main tools of this app consist of: -* **Inbox** - The main screen of the app is the Inbox, which highlights exactly what you should do next, consolidated across all products. -* **Search** - The next major screen is Search, which as you'd expect, lets you search everything across all products, from one convenient and powerful place. -* **Settings** - Settings wraps up all your personal, workspace, and domain configuration options, all in one helpful space. -* **Create** - Finally, the big green plus button is the Create button, which lets you create pretty much anything, across all the products. - -It's a deceptively simple app, with a few very familiar-looking screens and buttons that unlock an incredible range of sophisticated multi-product power. - -### [Platforms] Where can I use the Expensify app? -The Expensify app comes in three flavors: -* **Expensify web app** - The Expensify web app is what you would access at new.expensify.com. You can access the web app via a mobile web browser or a desktop web browser – it's optimized to work on both. -* **Expensify mobile app** - The Expensify mobile app works more or less identically to the Expensify web app (when opened in a mobile browser), but is more reliable, higher performance, has better support for notifications. -* **Expensify desktop app** - The Expensify desktop app works more or less identically to the Expensify web app (when opened in a desktop browser), but is more reliable, higher performance, and has better support for notifications. - -Whatever computer or phone you use, Expensify will work on it. - -### [Workspace] What is a workspace? -A workspace groups members together to enable secure sharing and real-time collaboration. Every product adds features to the workspace, but all share the same common baseline: -* **Name** - You can name your workspace anything. Names are not globally unique, but even if every other Alice has their own "Alice's Apples" workspace, yours is definitely the most special. -* **Profile photo** - Give your workspace a great headshot (or logo), or just stick with the beautiful one it is randomly assigned. -* **Description** - Help your members out by giving a good description to your workspace containing copious links and details. -* **Currency** - Though every workspace can support expenses in every currency, for convenience they are all converted into a single currency of your choosing. -* **Headquarters** - Workspaces work great for virtual teams, but some products deal with the physical world and need to know where you are headquartered. -* **Members** - Though there are many situations in which you might want a workspace just for personal use, in general, workspaces work best when they have many members. -* **Admins** - All members have some common elements of access, but "admin" members have enhanced privileges to manage the workspace overall. -* **Rooms** - Every workspace has a series of chat rooms, some of which are built in automatically and some of which are created manually. -* **Plan** - Workspaces come in two flavors, depending on the functionality you need: - * **Collect** - The Collect workspace is optimized for businesses with simpler requirements looking for basic expense management, Expensify Card, invoice collections, and bill pay functionality. - * **Control** - The Control workspace is built for more advanced companies with more powerful needs, such as multi-level approval, advanced domain control, enterprise accounting integrations, and so on. - -Workspaces make up the backbone of Expensify's collaboration features. - -### [Domain] What is a domain? -A domain is a secondary way of grouping users, generally for more advanced security purposes. Unlike a workspace, which can contain anybody with any email address or SMS number, you join a domain by validating your email address and then optionally "claiming" it as your own. -* **Name** - Each domain corresponds to the "domain name" of your email address (eg, cathy@croissants.com would have the domain of `croissants.com`). Unlike a workspace, you can't rename your domain. -* **Members** - A domain is similar to a workspace in that it represents a group of users. Unlike a workspace, however, domain members are generally limited to those who have validated email contact methods on this domain. -* **Group** - Every member of the domain is a member of exactly one group on the domain. This domain group sets various security rules for that member, such as setting their "preferred workspace." - -Domains allow for more advanced management and top-down control of Expensify members. - -### [Inbox] What does the Inbox do? -Given Expensify's chat-centric design, that makes Expensify in effect a superpowered chat app – and in any chat app, the most important page is the Inbox. The Inbox does a real-time search across all products to highlight exactly what you should do *right now*. A few key features of the Inbox include: -* **Green dot** - Whenever someone is waiting on you to do something – such as an expense you need to reimburse or a booking you need to approve – that thing's chat will be put to the top of the list with a little green dot next to it. -* **Red dot** - Anything you need to finish to accomplish something you started – such as fixing a violation before an expense can be submitted – will also be put to the top of the list with a little red dot next to it. -* **Pinned** - Anything you want to pay special attention to can be manually "pinned" to the top of the Inbox so it stays top of mind. -* **Priority mode** - Though everyone's work style is unique to them, the Expensify app is organized around two modes of prioritization: - * **Most recent mode** - The default mode for new users is to sort the Inbox to put whatever chat was most recently modified at the top. This works particularly well for those engaged in rapid-fire collaboration who want to "go where the action's at." - * **Focus mode** - When the Inbox gets over 30 rows, it automatically switches to "focus mode," which alphabetically organizes the chats and only shows those that are "unread" (ie, have comments you haven't read yet), have a green or red dot, or are pinned. This works well for those engaged in many large group conversations that you might want to monitor, but not necessarily engage with immediately. - -The Inbox is the most powerful page in the app, and where you will spend the bulk of your time. - -### [Search] What does Search do? -By and large, pretty much anything. Expensify has a "universal search" design that brings all data objects into a single place, and then lets you search all those objects using an incredibly flexible and powerful search engine. Search consists of the following main pieces: -* **Query** - At the top of the search page is the "query," which formally describes what you are searching for. -* **Datatype selector** - By default, we will search all datatypes simultaneously, but you can narrow the results to a single type. -* **Filters** - Similarly, each datatype has its own properties (eg, an expense has an amount, a trip has a destination), and you can filter on each. -* **Saved searches** - If you dial in a search you intend to do again and again, you can save it for future reuse. - -The Inbox's job is to push information in your direction, but the Search page exists to help you find anything you're looking for. - -### Settings -Every product will generally have its own distinct settings, but all settings are conveniently grouped into three main categories: -* **Account** - Every user has an "account" that stores all data owned by that user. Each individual person has a single user account, though that account can be associated with many contact methods (ie, email addresses and SMS numbers). -* **Workspace** - Group functionality across all products is organized into "workspaces," which allow secure sharing of data and settings between multiple members. -* **Domain** - Many users sign up with an email address, and the end of that address (ie, @company.com) corresponds to the "domain" that user is a member of. Domains are another way to group accounts and securely share data between the domain members. - -Every product adds its own layers of sophistication and power onto the common foundation of this shared superapp core. - -#### [Account] What are my account settings? -Your account contains the sum total of all data you own or shared with you, across all products. But all products rely upon the same common set of account properties: -* **Profile** - Your profile allows you to introduce and uniquely identify yourself to everyone else. -* **Wallet** - Your wallet organizes the various financial payment tools (such as the Expensify Card) and bank accounts associated with your account. -* **Preferences** - Your preferences configure high-level settings on how you are notified and how data is presented to you. - -Your personal account contains all the details that make you, you. - -#### [Profile] What are my profile settings? -Your "profile" is how you identify yourself, both publicly and privately: -* **Your public details** - As the name implies, your public details can be seen by other users. These include: - * **Profile photo** - Your profile photo is the image that is shown next to your name wherever you appear. You can customize this however you please, or a random "avatar" image will be picked for you. - * **Display name** - Your display name is the name that is generally shown next to your photo. If you don't have a display name, then your primary contact method will be shown instead. - * **Contact methods** - Your contact methods are all the email addresses and SMS numbers associated with your account. All contact methods allow you to sign in and associate any email receipts with your account. - * **Primary contact method** - This is the contact method that is highlighted on your profile, and to which all communications are sent. If you are an employee of a business, your primary contact method will typically be your company email address. - * **Secondary contact method** - You can add any number of "secondary" contact methods. These are not shown on your profile, but do allow you to sign into your account. It's helpful to have multiple secondary contact methods (such as a personal email address and personal phone number) to ensure you can access your account if you lose access to your primary contact method (such as your work address). - * **Status** - Your status is an expiring optional icon and message you can set that appears next to your name, such as to hint that you are on vacation or in a meeting, etc. - * **Pronouns** - Your pronouns are an optional tool for allowing you to indicate how you would like to be addressed by others. - * **Timezone** - Your timezone reflects the timezone in which you are currently located. This will generally be set automatically as you travel around the world, but can be manually set as well. - -* **Your private details** - Also as the name implies, your private details are not shown to others but might be required to enable certain functionality: - * **Legal name** - Your legal name is what appears on your government ID, which might differ from how you like to be addressed on a daily basis (ie, your display name). By default, your legal name is assumed to be your display name, but if that is not the case, you can easily correct this. - * **Date of birth** - Your date of birth is the birthday listed on your government ID. - * **Address** - Your address reflects where you would like us to contact you via mail, in the event we ever need to do so (such as to ship you an Expensify Card). - -The combination of your public and private profile gives you the tools to introduce yourself to the world and to us. - -#### [Wallet] What are my wallet settings? -Your wallet is your one-stop shop for all things banking and payment card-related. The major items in your wallet include: -* **Cash** - Just like a regular wallet that has a mix of cash and cards, your Expensify wallet is also able to hold electronic cash you receive from others. -* **Cards** - This contains a central list of every card associated with your Expensify account: - * **Expensify Cards** - Your employer can assign you an Expensify Card that gives you access to company credit for business purchases. - * **Imported cards** - You can import the transactions from your personal or corporate card into Expensify to submit to your company for approval or reimbursement or just to manage for your own needs. - * **Payment** - You can link a credit card to your account for paying your Expensify subscription or to fund your wallet's cash balance. -* **Bank accounts** - This contains a link of bank accounts associated with your Expensify account: - * **Personal bank account** - You can like a personal bank account to either receive company reimbursements or fund your wallet's cash balance. - * **Business bank account** - You can connect your business's bank account to reimburse expenses, issue Expensify Cards, collect online invoice payments, pay bills, and more! - -Just like your normal wallet, a lot can be stuffed into your Expensify wallet, and all of it is priceless. - -#### [Preferences] What are my preferences? -Your preferences are personal settings that affect how we display information to you: -* **Training and marketing** - We, in general, like to occasionally reach out with new information about features, changes, or offers to help – but only if you like. -* **App sounds** - We've worked hard to come up with some subtle audio cues that hint when certain actions happen in the app, but they are entirely optional. -* **Priority mode** - This is how you specify which Inbox priority mode you prefer. -* **Language** - Everybody in the world can use Expensify, and we are supporting an increasing number of languages natively. -* **Theme** - Give into the dark side or stay in the light, we won't judge! -* **Two-factor authentication** - We strongly recommend everyone enable two-factor authentication to secure access to your account. - -Everybody likes things their own way, and preferences are how you make the Expensify app your own. - -#### [Subscription] What are my subscription settings? -Most of Expensify is completely free to use, and millions of members use Expensify without paying anything at all. To unlock our more powerful functionality, create a workspace and pick which products you need – each can be adopted independently, but all are included in the base price (though some products have slightly different nuances: Expensify Card cashback deducts from the bill, Expensify Travel booking fees add to the bill, etc). Regardless of which products you enable, all are billed together via the same subscription. Your subscription consists of the following: -* **Billing card** - Pick a credit or debit card from your wallet to pay your subscription. -* **Subscription length** - Expensify has options for everyone depending on your specific needs, allowing you to balance cost versus commitment: - * **Pay-per-use** - By default, your Expensify account starts with zero-risk, zero-commitment: just use Expensify to your heart's content, and you will be billed for as much or as little as you use the next month. - * **Annual plan** - Once you know how much Expensify you need, lock in a 50% annual plan discount by committing to a certain number of seats for 12 months. The annual plan is configured as follows: - * **Subscription size** - This is the number of seats you commit to purchasing for the next 12 months (billed monthly), at a 50% discounted rate. Any active seats billed at the end of the month in excess of the subscription size are billed at the pay-per-use rate (ie, without the 50% discount). - * **Auto-renew** - Whether to automatically renew this subscription at the end of 12 months, or revert back to pay-per-use (giving up the 50% discount). - * **Auto-increase annual seats** - Whether to automatically increase the number of annual seats you commit to based on the number of seats used. This avoids being accidentally billed for any pay-per-use seats. - -Pick the plan that works for you, and feel free to change as you need. - -#### [Price] What is the price of Expensify? -For most users, Expensify is completely free. For business users, the price of Expensify depends on which features are enabled – and with Expensify Card cashback, you can actually be paid to use Expensify! The major variables going into the price of Expensify for your specific needs include the following: -* **Personal use** - Most users enjoy Expensify free of charge, as there is a huge range of free features designed for use by yourself and with your friends. -* **Active seats** - Our paid functionality is largely contained within workspaces and billed on an "active seat" basis. This means at the end of the month, we look over the activity of each workspace member to determine if they used any paid functionality or merely free features: - * **Paid seat** - A workspace member who uses any paid functionality (ie, submitting, approving, or paying expenses) requires a "paid seat." - * **Free seat** - A workspace member who only used free functionality (ie, viewing expenses, chatting outside of an expense report) only requires a "free seat." -* **Paid seat price** - Once we determine how many paid seats you require in a given month, we initially set the price per paid seat at $20/seat/mo for Collect workspaces and $36/seat/mo for Control. -* **Expensify Card discount** - The first modification to the base seat price is to assess how much total spend was approved on the workspace, versus how much of it was spent on the Expensify Card. This will generate a sliding discount ranging from 0% (if you aren't using the card at all) to 50% (if you have used the Expensify Card for at least 50% of your company's spend). The Expensify Card discount is applied to the seat price, which can reduce it down to $10/seat/mo for Collect workspaces or $18/seat/mo for Control. For example: - * If your company spends 0% of the total approved spend on the Expensify Card, you receive no discount. - * If your company spends 25% of the total approved spend on the Expensify Card, you receive a 25% discount off each seat. - * If your company spends 75% of the total approved spend on the Expensify card, you receive a 50% discount off each seat. -* **Annual plan discount** - Next, we determine how many seats you have committed to in your annual plan subscription size and apply an additional 50% discount to those seats – bringing the price down to $5/seat/mo for Collect workspaces, or $9/seat/mo for Control. -* **Expensify Card cashback** - Finally, we calculate how much cashback you earned from spending on the Expensify Card and apply that to the bill, reducing the price further. In many cases, the cashback is larger than the Expensify bill itself, meaning our so-called "paid" features could not only be free, *you can actually be paid to use them.* - -Long story short, depending upon which features you use, you might pay us, it might be free, or we might even pay you. There are a lot of variables involved, so please check out our savings calculator to understand how this will shake out for you. - -#### [Save the world] What the heck is "Save the world"? -Expensify.org’s mission is to empower individuals and communities to eliminate injustice around the world by making giving and volunteering more convenient, meaningful, and collaborative. We simplify full transparency for all, allowing our donors and volunteers to connect and make positive permanent changes. The foundation of Expensify.org was built on applying our expertise in expense management to increase the transparency of how funds are used, the convenience of how donations are gathered, and — most importantly — the human connection between donors, volunteers, and recipients. - -Please note that our funding model is not in the form of a grant given to a nonprofit organization. Instead, we're looking to help amplify the work of individuals who are directly absorbing the costs. - -### [Global create] What does the big green Create button do? -Saving the best for last is the big green "global create" button. As the name suggests, this allows you to create basically anything your account is allowed to create. The exact options will depend on which products are configured in your workspace, but it can be any of the following: -* **Start chat** - Begins a new chat with one or more users. -* **Track expense** - Tracks an expense for personal use. -* **Submit expense** - Submits an expense to another user for payment. -* **Split expense** - Splits an expense with one or more other users for shared payments. -* **Pay someone** - Sends money to another user from your Expensify wallet balance. -* **Send invoice** - Sends an invoice from a workspace to a customer for online payment. -* **Assign task** - Creates a new task and assigns it to yourself or another user for completion. -* **Book travel** - Books a flight, hotel reservation, or car rental. -* **Quick action** - Repeats the last action you took, most commonly to scan a receipt and submit it via a particular workspace all in a single button. - -As you can see, there's a lot packed into that big button – press it and see what happens! -## Tutorials -The Expensify superapp has a lot of moving parts, what specifically are you trying to do? Let's point you in the right direction with some step-by-step guides. - -### Role - -#### [Individual] How do I use Expensify as an individual? -Expensify is designed to be flexible for a wide range of individual use cases. As an individual, you can: -1. Track personal expenses -2. Split bills with friends -3. Collect receipts and categorize them -4. Use Expensify Card for cashback and simplified reimbursement - -Simply log in, navigate to the expense section, and use the Create button to start organizing your expenses. - -#### [Friends] How do I use Expensify with my friends? -You can use Expensify to settle shared expenses between friends, such as splitting the bill at a restaurant. Here's how: -1. Create an expense and enter the total amount. -2. Choose **Split Expense** and add your friends by entering their email addresses. -3. Expensify will calculate each person's share, and you can easily send a request to them to settle the balance. - -#### [Employee] How do I use Expensify as an employee? -As an employee, Expensify can help you: -1. Submit expense reports for approval. -2. Use the Expensify Card for company expenses. -3. Book travel and manage expenses during work trips. -4. Communicate with colleagues through integrated chat features. - -After logging in, create an expense report, attach receipts, and submit it for approval through your workspace. - -#### [Manager] How do I use Expensify as a manager? -Managers can use Expensify to: -1. Approve or reject expense reports from their team. -2. Monitor corporate spending in real-time. -3. Issue Expensify Cards to employees. -4. Set up advanced approval workflows for multi-level reviews. - -Use the Inbox and Workspace features to manage team expenses and approvals efficiently. - -#### [Accountant] How do I use Expensify as an accountant? -Accountants can: -1. Manage multiple clients’ expense workflows through different workspaces. -2. Create invoices and collect payments. -3. Export data directly to accounting software for tax purposes. -4. Benefit from revenue-sharing programs by becoming an Expensify Approved Accountant. - -You can use the Invoice and Bill Pay tools to manage clients' billing, and track expenses for tax reporting. - -#### [Travel manager] How do I use Expensify as a travel manager? -Travel managers can: -1. Book and manage employee travel. -2. Track expenses related to flights, hotels, and car rentals. -3. Issue Expensify Cards for travel-related spending. -4. Approve travel expenses before they are reimbursed. - -Simply navigate to the Travel section, where you can manage travel bookings and expense submissions in one place. - -### Platforms - -#### [Web] How do I access Expensify on the web? -To visit the Expensify website: -1. Go to www.expensify.com, either on a desktop or mobile browser. - -#### [Mobile] How do I install the Expensify mobile app? -To install the Expensify mobile app: -1. Visit the Expensify for iOS or Expensify for Android app stores. -2. Press **Install**. -3. Follow the prompts to install. -4. Press the Expensify icon in your phone's app list to start. - -#### [Desktop] How do I install the Expensify desktop app? -To install the Expensify desktop app on MacOS: -1. Download the Expensify for MacOS or Expensify for Windows installer. -2. Double-click on the installer to open it. -3. Click the Expensify icon on the taskbar to start. - -#### [Sign in] How do I sign up or sign in to my Expensify account? -Signing up for a new account works the same as signing into an existing account, as follows: -1. Install or access Expensify on any platform: - * Access Expensify on the web. - * Install the Expensify mobile app. - * Install the Expensify desktop app. -2. Choose how you want to connect and press Next: - * Press **Email** and enter your email address, or - * Press **Phone Number** and enter your SMS-compatible phone number. - * Press **Google** and sign in to your Google account, or - * Press **Apple** and sign into your Apple account. -3. If asked to validate your email address, check your email inbox for a magic link and press it. -4. If asked to join, this means that this is the first time you are signing in with this email address or phone number; press **Join** to confirm you entered it correctly. - -#### [Magic link] How do I use a magic link? -Magic links are used for secure login without passwords. When prompted: -1. Check your email for the Expensify Magic Link. -2. Click the link in your email, and it will log you in to Expensify without needing to enter a password. - -#### [Sign out] How do I sign out? -To sign out of Expensify: -1. Press **Settings** in the Expensify app. -2. Scroll to the bottom and press **Sign Out**. - -#### [Two factor] How do I secure my account with two-factor authentication? -To enable two-factor authentication: -1. Press **Settings**. -2. Press **Security**. -3. Press **Two-factor authentication**. -4. Follow the steps to link your mobile phone for 2FA. - -#### [Close account] How do I close my account? -To close your account: -1. Press **Settings**. -2. Press **Security**. -3. Press **Close account**. -4. Confirm by following the prompts to complete the process. +### Superapp Fundamentals +#### [Superapp Introduction] What is a superapp? +A superapp is a single app combining multiple products into one interconnected experience. Expensify isn't a "suite" of separate products but a single app performing multiple functions. Built on a common core: +* **App** - The superapp experience runs on your mobile phone or desktop computer. +* **Chats** - Chat is infused through the entire product, even if not used for enterprise-grade collaboration. +* **Expense** - All products dealing with money ultimately deal with expenses. + +#### [Domain Introduction] What is a domain? +A domain groups users for advanced security. Join by validating your email: +* **Name** - Corresponds to the "domain name" of your email address. +* **Members** - Represents users with validated email contact methods. +* **Group** - Each member belongs to one group, setting security rules. + +### Expensify Tools +#### [Tools Introduction] What are the main tools in the Expensify App? +The Expensify App is your window to the connected world of Expensify: +* **Inbox** - Highlights what you should do next, across all products. +* **Search** - Lets you search everything across all products from one place. +* **Settings** - Wraps up personal, workspace, and domain configuration options. +* **Create** - The big green plus button to create anything across all products. + +#### [Workspace Introduction] What is a workspace? +A workspace groups members for secure sharing and collaboration. Features include: +* **Name** - Name your workspace anything, it's not globally unique. +* **Profile photo** - Use a headshot or logo, or the assigned one. +* **Description** - Provide details for members with links and information. +* **Currency** - Supports expenses in every currency, converted to one of your choice. +* **Headquarters** - Some products need to know your physical location. +* **Members** - Workspaces work best with many members. +* **Admins** - Admins have enhanced privileges to manage the workspace. +* **Rooms** - Contains chat rooms built in automatically or created manually. +* **Plan** - Workspaces come in two flavors: + * **Collect** - Optimized for businesses with simpler requirements. + * **Control** - Built for companies with more powerful needs. + +### App Platforms and Search +#### [Platforms Introduction] Where can I use the Expensify App? +The Expensify app is available in three forms: +* **Expensify web app** - Accessed at new.expensify.com via mobile or desktop web browser. +* **Expensify mobile app** - Works like the web app but is more reliable and supports notifications. +* **Expensify desktop app** - Similar to the web app but optimized for desktops and supports notifications. + +Expensify works on any computer or phone. + +#### [Search Introduction] What does Expensify's "universal search" do? +Expensify's "universal search" brings all data into one place. Search components include: +* **Query** - Describes what you are searching for. +* **Datatype selector** - Narrow results to a single type. +* **Filters** - Filter by datatype properties. +* **Saved searches** - Save searches for future use. + +The Search page helps you find anything you're looking for. + +### Inbox and Communication +#### [Inbox Introduction] What makes Expensify's Inbox powerful? +Expensify's chat-centric design makes it a superpowered chat app. The Inbox highlights what you should do now: +* **Green dot** - Indicates someone is waiting on you. +* **Red dot** - Shows what you need to finish. +* **Pinned** - Manually pin important items. +* **Priority mode** - Organized by two modes: + * **Most recent mode** - Sorts Inbox by recent activity. + * **Focus mode** - Shows unread, green/red dot, or pinned chats. + +The Inbox is the most powerful page, where you'll spend most of your time. + +### Security and Data Protection +#### [Security Overview] What security measures does Expensify implement? +Expensify takes security seriously, aligning its measures with those used by banks to protect sensitive financial data. Regular testing and updates ensure security stays ahead of potential threats. Expensify also undergoes daily checks by McAfee for added protection against hackers. Users can verify Expensify's security at the McAfee SECURE site. + +#### [Security Standards] What are Expensify's security standards? +Expensify adheres to the Payment Card Industry Data Security Standard (PCI-DSS), a high security standard used by major companies like PayPal and Visa to protect online credit card information. Additionally, Expensify is compliant with SSAE 16 and undergoes an annual SSAE-18 SOC 1 Type 2 audit by independent third-party auditors. + +#### [Data Encryption] How does Expensify encrypt data and passwords? +Expensify employs data encryption to protect information. Upon submission, data is transformed into a secret code to ensure security during transit between your device and Expensify's servers, as well as within the server network. Expensify uses HTTPS+TLS for all web connections, ensuring data is encrypted at every stage. + +#### [GDPR Compliance] How does Expensify comply with GDPR? +Expensify is committed to the General Data Protection Regulation (GDPR), which strengthens data protection for EU individuals. Key compliance measures include: +- Participation in the EU-US and Swiss-US Privacy Shield Frameworks. +- Annual SSAE-18 SOC 1 Type 2 audits. +- Maintaining PCI-DSS compliance. +- Annual penetration tests by third-party experts. +- Background checks and security training for employees and contractors. +- Appointing a dedicated Data Protection Officer reachable at [privacy@expensify.com](mailto:privacy@expensify.com). +- Signing Data Processing Addendums with vendors. +- Transparency about sub-processors on the website. +- User tools for data export, preference management, and account closure. + +**Disclaimer**: This information is not legal advice. Consult legal counsel for specific GDPR applicability. -### Profile +## Tutorials -#### [Photo] How do I set my profile photo? -To set your profile photo: -1. Press **Settings**. -2. Press the **pencil icon** next to your existing profile photo. -3. Press **Upload** photo. -4. Follow the prompts on your platform to select your photo from local storage. +### Getting Started +#### [Web Access Guide] How do I access Expensify on the web? +Visit the Expensify website: +1. Go to www.expensify.com on a browser. -#### [Display name] How do I change my display name? -To change your display name: -1. Press **Settings**. +#### [Mobile App Installation] How do I install the Expensify mobile app? +Install the Expensify mobile app: +1. Visit iOS or Android app stores. +2. Press **Install**. +3. Follow prompts to install. +4. Press the Expensify icon to start. + +#### [Desktop App Installation] How do I install the Expensify desktop app? +Install the Expensify desktop app: +1. Download the MacOS or Windows installer. +2. Double-click the installer. +3. Click the Expensify icon to start. + +#### [Join a Workspace] How do I join my company's workspace? +Welcome to Expensify! If you received an invitation to join your company's Expensify workspace, follow these steps: + +1. Download the Expensify mobile app to upload expenses and check reports from your phone. +2. Press your profile image or icon in the bottom menu, then press the **pencil icon** next to your photo to upload an image from your saved files. +3. Press **Profile** to edit details like Display Name, Contact Method, Status, Pronouns, and Timezone. +4. Meet **Concierge**, your personal assistant, to get reminders and alerts. +5. Learn to add an expense by SmartScanning a receipt or entering it manually. +6. Secure your account by enabling two-factor authentication through the **Security** settings. + +#### [Create a Company Workspace] How do I create a workspace for my company? +Creating a workspace in Expensify is your first step to organizing your company's expenses. Here's how to do it: + +1. Press your **profile photo** or icon in the bottom menu to open the settings. +2. Scroll and press **Workspaces**. +3. Press **New workspace** to start creating your workspace. +4. Press the **Edit pencil icon** next to your workspace image to upload a custom image. +5. Press **Name** to set the workspace's name. +6. Press **Default Currency** to choose the currency for all expenses. + +Invite team members to collaborate efficiently. + +#### [Manage a Copilot] How do I add, remove, or act as a Copilot? +Manage your Copilot settings: + +1. To add a Copilot: + 1. Press your **profile icon** in the bottom left corner to open **Settings**. + 2. Press **Security**. + 3. Under Copilot: Delegated Access, press **Add Copilot**. + 4. Search for the user you'd like to add using their name or email address. + 5. Select **Full** or **Limited** access and press **Add Copilot**. + +2. To remove a Copilot: + 1. Press your **profile icon** in the bottom left corner to open **Settings**. + 2. Press **Security**. + 3. Under Copilot: Delegated Access, press the three vertical dots next to the Copilot and press **Remove Copilot**. + +3. To act as a Copilot: + 1. Press your **profile icon** in the bottom left corner to open **Settings**. + 2. Press the up-down arrow next to your profile name in the top left corner to access the account switcher. + 3. Select the account and level of access. + +#### [Name Update Process] How do I update my display or legal name? +Update your display or legal name: +1. Press your **profile icon** to open **Settings**. 2. Press **Profile**. -3. Press **Display name**. -4. Enter your first and last name. -5. Press **Save**. +3. Edit your name: + - **Display name**: Press **Display Name**, enter your first name (or nickname) and last name, then press **Save**. + - **Legal name**: Scroll to the Private Details section, press **Legal Name**, enter your legal first and last name, and press **Save**. -#### [Secondary contact] How do I add a secondary contact method? -To add a secondary contact method: -1. Press **Settings**. -2. Press **Profile**. -3. Scroll to **Contact Methods**. -4. Press **Add Secondary Contact** and enter your additional email or phone number. +#### [Update Notification Preferences] How do I update my notification preferences? +Customize how you receive email and in-app notifications from Expensify: -#### [Primary contact] How do I change my primary contact method? -To change your primary contact method: -1. Add a new secondary contact method. -2. Press **Make primary** to make it the new primary contact method. +1. Press your profile image or icon in the bottom menu. +2. Press **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. -#### [Remove contact] How do I remove a contact method? -To remove a contact method: -1. Press **Settings**. -2. Go to **Profile** and navigate to **Contact Methods**. -3. Select the contact method to remove and press **Remove**. +#### [Email Address Management] How do I change or add an email address on my Expensify account? +To change or add an email address on your Expensify account: -#### [Pronouns] How do I set my pronouns? -To change your pronouns: -1. Press **Settings**. +1. Press your profile image or icon. 2. Press **Profile**. -3. Press **Pronouns**. -4. Start typing your preferred pronouns. -5. Choose your preferred set from the list. - -#### [Timezone] How do I change my timezone? -By default, your timezone will be set automatically to match your system settings. To instead set it manually: -1. Press **Settings**. +3. Press **Contact Method**. +4. Press **New Contact Method**. +5. Enter the email address or phone number you want to use. +6. Press **Add**. +7. A verification code will be sent to your email. Enter it in Expensify and press **Verify**. + +You can press any email address in your list to set it as the default, remove it, or verify it. + +#### [Switch Theme] How do I switch between light and dark mode in Expensify? +Change the appearance of Expensify by selecting a theme: + +1. Press your **profile image or icon** in the bottom menu. +2. Press **Preferences**. +3. Press the **Theme** option and select the desired theme: + - **Dark mode**: The app will appear with a dark background. + - **Light mode**: The app will appear with a light background. + - **Use Device settings**: Expensify will automatically use your device’s default theme. + +#### [Switch Language to Spanish] How do I switch my account language to Spanish? +Change your account language to Spanish: + +1. Press your **profile image or icon** in the bottom menu. +2. Press **Preferences**. +3. Press the **Language** option and select **Spanish**. + +#### [Timezone Adjustment] How do I change my timezone? +Change your timezone: +1. Press your profile image or icon in the bottom menu. 2. Press **Profile**. -3. Press **Timezone**. -4. Disable **Automatically determine your location**. -5. Press **Timezone**. -6. Choose your preferred timezone from the list. +3. Press **Timezone** to select your timezone. -#### [Status] How do I set my status? -To set your status: -1. Press **Settings**. -2. Press **Profile**. -3. Press **Status**. -4. Enter your custom status message and choose an emoji (optional). -5. Press **Save**. +#### [Pronouns Update] How do I update my pronouns? +Update your pronouns to display them on your account: -#### [Legal name] How do I change my legal name? -To change your legal name: -1. Press **Settings**. +1. Press your profile image or icon. 2. Press **Profile**. -3. Press **Legal Name**. -4. Enter your updated legal name. -5. Press **Save**. - -#### [Date of birth] How do I change my date of birth? -To change your date of birth: +3. Press **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. + +### Troubleshooting +#### [Feature Issues] What should I do if I'm facing issues with a specific feature? +If you're experiencing problems with a specific feature, refer to the respective section of the help docs for common errors and troubleshooting steps. If the issue persists, reach out to Concierge via in-product chat or by emailing concierge@expensify.com. + +#### [Local Issues] How do I troubleshoot local issues with my webpage? +If your webpage isn't loading properly, try these steps: +1. Press [here](https://www.expensify.com/signout.php?clean=true) to force a clean sign-out from the site, which can help remove stale data causing issues. +2. Clear cookies and cache on your browser. +3. Use an Incognito or Private browsing window. +4. Try accessing the site on a different browser. + +#### [JavaScript Console Access] How do I access the JavaScript console on my browser or application? +A developer console logs backend operations of sites and applications, providing information that can help developers solve your issues. To provide a screenshot of your developer console, follow the instructions for your browser or application: + +- **Chrome**: Press Cmd + Option + J on Mac, or Ctrl + Shift + J on Windows; or navigate through View > Developer > JavaScript Console. +- **Firefox**: Press Cmd + Option + K on Mac, or Ctrl + Shift + J on Windows; or go through Menu Bar > More Tools > Web Developer Tools > Console tab. +- **Safari**: Enable the console in Safari by selecting "Show features for web developers" in Safari Menu > Settings > Advanced. Then, press Cmd + Option + C or use the Develop Menu > Show JavaScript Console. +- **Microsoft Edge**: Press Cmd + Option + J on Mac, or Ctrl + Shift + J on Windows; or right-click a webpage and select Inspect > Console. + +### Account Management +#### [Sign In Process] How do I sign up or sign in? +Sign up or sign in: +1. Install or access Expensify on any platform. +2. Choose connection method and press Next. +3. Validate your email address and press **Join** if first time signing in. + +#### [Magic Link Login] How do I use a magic link for secure login? +Use a magic link for secure login: +1. Check email for Expensify Magic Link. +2. Click link to log in without a password. + +#### [Sign Out Process] How do I sign out of Expensify? +Sign out of Expensify: 1. Press **Settings**. -2. Press **Profile**. -3. Press **Date of Birth**. -4. Update your birth date and press **Save**. +2. Scroll and press **Sign Out**. -#### [Address] How do I change my address? -To change your address: +#### [Close Account Process] How do I close my account? +Close your account: +1. Press your profile image or icon in the bottom menu. +2. Press **Security**. +3. Press **Close account**. +4. Provide answers to the questions and confirm closure by pressing **Close Account**. + +### Subscription Management +#### [Manage Subscription] How do I manage my subscription? +To manage your subscription in New Expensify: +1. Open the app on your device. +2. Press your profile icon in the bottom-left corner. +3. Navigate to the **Workspaces** section. +4. Press **Subscription** under Workspaces to view your subscription details. + +#### [Add Payment Card] How do I add a payment card for billing? +To add a payment card for billing: +1. Locate the **Add Payment Card** option within your subscription settings. +2. Enter your payment card details securely to maintain uninterrupted service. + +#### [Understand Subscription Details] What subscription details can I view? +Within your subscription overview, you can view: +- **Plan details**: See the number of seats, billing information, and renewal date. +- **Auto-renew settings**: Check when your subscription will renew automatically. +- **Auto-increase seats**: Discover potential savings by automatically increasing seats for team members exceeding the subscription size. + +#### [Request Early Cancellation] How can I request an early cancellation of my subscription? +To request an early cancellation: +1. Access the **Request Early Cancellation** option in the Subscriptions section. +Note: Early cancellation might not be available for all customers. + +#### [Pricing Information] Where can I find more details on pricing plans? +For detailed pricing plans, visit the billing page [coming soon]. + +### Security and Customization +#### [Enable 2FA Security] How do I secure my account with two-factor authentication? +Secure your account with two-factor authentication: 1. Press **Settings**. -2. Press **Profile**. -3. Press **Address**. -4. Enter your new address and press **Save**. +2. Press **Security**. +3. Press **Two-factor authentication**. +4. Follow steps to link your phone. -### Workspace +#### [Additional Security with 2FA] How do I add an extra layer of security with 2FA? +Adding an extra layer of security can help protect your financial data. To enable two-factor authentication (2FA): -#### [Create] How do I create a workspace? -To create a workspace: +1. Press your profile image or icon in the bottom menu. +2. Press **Security**. +3. Under Security Options, press **Two Factor Authentication**. +4. Save a copy of your backup codes. This is critical to avoid losing access if you cannot use your authenticator app. + - Press **Download** to save the backup codes to your device. + - Press **Copy** to paste the codes into a secure location. +5. Press **Next**. +6. Download or open your preferred authenticator app and connect it to Expensify by scanning the QR code or entering the code manually. +7. Enter the 6-digit code from your authenticator app into Expensify and press **Verify**. + +When you log in to Expensify in the future, you'll need to use a magic code from your email and a 6-digit code from your authenticator app. If you lose access to your authenticator app, use your recovery codes as you would the authenticator code. + +### Profile and Contact Methods +#### [Profile Photo Setup] How do I set my profile photo? +Set your profile photo: +1. Press your profile image or icon in the bottom menu. +2. Press the **pencil icon** next to your photo. +3. Press **Upload Image** to select a photo from your saved files. + +#### [Display Name Modification] How do I change my display name? +Change your display name: 1. Press **Settings**. -2. Press **Workspaces**. -3. Press **Create Workspace**. -4. Follow the steps to name and configure your new workspace. +2. Press **Profile**. +3. Press **Display name**. +4. Enter your name and press **Save**. -#### [Rename] How do I rename my workspace? -To rename your workspace: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select your workspace and press **Edit**. -4. Change the name and press **Save**. +#### [Status Update] How do I set my status? +Set your status: +1. Press your profile image or icon in the bottom menu. +2. Press **Profile**. +3. Press **Status**. +4. (Optional) Press the **emoji icon** to add an emoji. +5. Enter a status message, such as "out of office" or "in a meeting." +6. Press **Clear After** to select when the status should expire. +7. Press **Save**. -#### [Photo] How do I change the profile photo of my workspace? -To change your workspace's profile photo: +#### [Add Secondary Contact] How do I add a secondary contact method? +Add a secondary contact method: 1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Profile Photo**. -4. Upload a new photo and press **Save**. +2. Press **Profile**. +3. Scroll to **Contact Methods**. +4. Press **Add Secondary Contact** and enter details. -#### [Description] How do I change the description of my workspace? -To update your workspace description: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Description**. -4. Update the text and press **Save**. +#### [Change Primary Contact] How do I change my primary contact method? +Change your primary contact method: +1. Add a new secondary contact method. +2. Press **Make primary** to set it as primary. -#### [Currency] How do I change the currency of my workspace? -To change your workspace currency: +### Private Details and Regional Settings +#### [Remove Contact Method] How do I remove a contact method? +Remove a contact method: 1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Currency**. -4. Choose a new default currency and press **Save**. +2. Go to **Profile** and **Contact Methods**. +3. Select and press **Remove**. -#### [Headquarters] How do I change the headquarters of my workspace? -To change your workspace's headquarters location: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Headquarters**. -4. Update the address and press **Save**. +#### [Set Pronouns] How do I set my pronouns? +Set your pronouns: +1. Press your profile image or icon in the bottom menu. +2. Press **Profile**. +3. Press **Pronouns**. +4. Type any letter to see a list of available pronouns and select your preferred set. -#### [Invite member] How do I add or invite someone to my workspace? -To invite a new member: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Members**. -4. Press **Add Member** and enter the email addresses of the new members. -5. Press **Invite**. +#### [Timezone Adjustment] How do I change my timezone? +Change your timezone: +1. Press your profile image or icon in the bottom menu. +2. Press **Profile**. +3. Press **Timezone** to select your timezone. -#### [Remove member] How do I remove someone from my workspace? -To remove a member from your workspace: +#### [Legal Name Update] How do I change my legal name? +Change your legal name: 1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Members**. -4. Choose the member to remove and press **Remove Member**. +2. Press **Profile**. +3. Scroll to the Private Details section and press **Legal Name**. +4. Enter updated name and press **Save**. -#### [Add admin] How do I make someone an admin of my workspace? -To promote a member to an admin: +#### [Date of Birth Adjustment] How do I change my date of birth? +Change your date of birth: 1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Members**. -4. Select the member and press **Make Admin**. +2. Press **Profile**. +3. Scroll to the Private Details section and press **Date of Birth**. +4. Update birth date and press **Save**. -#### [Remove admin] How do I remove an admin from my workspace? -To remove admin privileges: +### Address and Workspace Management +#### [Address Update] How do I change my address? +Change your address: 1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Members**. -4. Choose the admin and press **Remove Admin**. +2. Press **Profile**. +3. Scroll to the Private Details section and press **Address**. +4. Enter new address and press **Save**. -#### [More features] How do I enable features on my workspace? -To enable features: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Features**. -4. Toggle on the desired features and press **Save**. +### Theme Preferences +#### [Theme Preference Setup] How do I set my theme preference in New Expensify? +Customize your theme preference in New Expensify to enhance your experience: -#### [Upgrade plan] How do I upgrade my workspace? -To upgrade a workspace to the Control plan: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select your workspace and press **Upgrade to Control**. -4. Follow the steps to finalize the upgrade. +1. Press your **profile image or icon** in the bottom menu. +2. Press **Preferences**. +3. Tap on **Theme**. +4. Choose your preferred theme: + - **Dark mode**: Provides a dark background for a sleek look. + - **Light mode**: Offers a bright background for a classic appearance. + - **Use Device settings**: Aligns with your device's theme settings, adjusting automatically as your device changes. -#### [Delete] How do I delete my workspace? -To delete a workspace: -1. Press **Settings**. -2. Press **Workspaces**. -3. Select the workspace and press **Delete Workspace**. -4. Confirm the deletion. +The default setting is **Use Device Settings**, which matches your device's theme transitions. Your selected theme will sync across all Expensify platforms you use. ## FAQ You've got questions? We've got answers! -### App +### App and Messaging +#### [Passwordless Authentication] Why don't I set a password? +Expensify uses a "passwordless" design, sending a "magic link" to your contact method for secure authentication. Once signed in, you remain signed in until you sign out. + +#### [Infinite Sessions] Why am I never asked to sign in? +Expensify uses "infinite sessions," keeping you signed in indefinitely until you sign out. -#### Why don't I set a password? -Expensify uses a "passwordless" design, where each time you sign in, we send a "magic link" to your contact method. This securely authenticates you based on your ability to receive the magic link to the contact method associated with your account. Once signed into a device, you remain signed into that account until you ask to sign out. +#### [Messaging Closed Accounts] Why can others message me even if my account is closed? +Expensify is a communications platform allowing messaging with valid email or SMS numbers, even if you don't use Expensify. -#### Why am I never asked to sign in? -Expensify uses an "infinite sessions" design, where after you sign in on a particular device, you remain signed in indefinitely, until you explicitly sign out. +#### [Messaging User Blocking] Why can't I block users from messaging me using Expensify? +Like Gmail or iMessage, Expensify allows messaging with valid emails or SMS numbers, without blocking all users. -#### Why can others message me even if my account is closed? -Like Gmail or iMessage, Expensify is a communications platform designed to let you message anyone with a valid email address or SMS number – whether or not they also use Gmail or iMessage. Accordingly, even if you don't use Expensify (or if you did use it but have since closed your account), other users can still message you using Expensify. +### Profile and Legal Information +#### [Display and Legal Names] Why do I have both a display name and legal name? +Your display name shows how you'd like to be identified. Your legal name is used for documentation like billing or tax-related matters. -#### Why can't I block users from messaging me using Expensify? -Similar to how you can't ask Gmail to stop all Gmail users from emailing you, or ask iMessage to stop all iMessage users from texting you, you can't ask Expensify to stop all Expensify users from emailing you. Gmail, iMessage, and Expensify are all tools designed to enable the user to email and SMS other users. +#### [Need for Legal Name] Why do you need my legal name? +Your legal name is for identity verification when issuing payment cards and processing reimbursements. -### Profile +#### [Birth Date Requirement] Why do you need my date of birth? +Your birth date verifies identity for financial products, ensuring compliance with regulations. -#### Why do I have both a display name and legal name? -You have a display name to show how you'd like to be publicly identified. Your legal name is used for documentation purposes, such as for billing or tax-related matters, which require your formal identification. +#### [Home Address Requirement] Why do you need my home address? +We need your address for shipping items and identity verification when processing payments. -#### Why do you need my legal name? -Your legal name is necessary for identity verification when issuing payment cards, processing reimbursements, and fulfilling regulatory requirements. +### Workspace and Copilot +#### [Workspace Address Requirement] Why do you need the address of my workspace's headquarters? +We need the address to process transactions, apply local taxes, and comply with regional laws. -#### Why do you need my date of birth? -Your date of birth is used for verifying your identity when issuing financial products like the Expensify Card. It helps ensure compliance with regulatory requirements. +#### [Copilot Permissions] As a Copilot, can I add or remove other Copilots? +No. Copilots are restricted from adding or removing Copilots from other accounts. Only the account owner can add or remove Copilots from their own account. The only exception is that Copilots can remove themselves from another user's account. -#### Why do you need my home address? -We need your home address for shipping physical items like the Expensify Card and for identity verification when processing reimbursements or payments. +#### [Copilot Action Identification] How can I tell which actions were taken by a Copilot? +Any action taken by a Copilot will be displayed as being taken by the Copilot on behalf of the account owner. -### Workspace +#### [Multiple Copilots] Can I have more than one Copilot? +You can assign as many Copilots as you need—there is no limit. However, you can only add one Copilot per minute. -#### Why do you need the address of my workspace's headquarters? -We need the headquarters' address to correctly process transactions, apply any local taxes, and ensure compliance with regional laws. +### Account Closure and Pricing +#### [Account Closure Issues] Why can't I close my account? +There are several reasons you might be unable to close your account. If your account has an outstanding balance or if you have been assigned a role under a company’s Expensify workspace, you may encounter an error message during the account closure process, or the Close Account button may not be available. Here are the steps to follow for each scenario: +- **Account Under a Validated Domain**: A Domain Admin must remove your account from the domain. Then you will be able to successfully close your account. +- **Sole Domain Admin**: If you are the only Domain Admin for a company’s domain, you must assign a new Domain Admin before you can close your account. +- **Workspace Billing Owner with an annual subscription**: You must downgrade from the annual subscription before closing the account. Alternatively, you can have another user take over billing for your workspaces. +- **Company Workspace Owner**: You must assign a new workspace owner before you can close your account. +- **Account has an outstanding balance**: You must make a payment to resolve the outstanding balance before you can close your account. +- **Preferred Exporter for a workspace integration**: You must assign a new Preferred Exporter before closing your account. +- **Verified Business Account that is locked**: You must unlock the account. +- **Verified Business Account that has an outstanding balance**: You must make a payment to settle any outstanding balances before the account can be closed. +- **Unverified account**: You must first verify your account before it can be closed. -### Pricing +#### [Workspace Member Pricing] Which active workspace members require paid seats? +Workspace members billed for a paid seat if they submit, approve, pay, export, or chat on expenses. -#### Which active workspace members require paid seats? -If a workspace member takes any of the following actions inside of a workspace, you will be billed at the end of the month for a paid seat: -* Submit an expense -* Approve an expense -* Pay an expense -* Export an expense -* Chat on an expense report -* and so on +#### [Member Double Billing] Why do some workspace members using paid features not require paid seats? +Members using paid functionality on multiple workspaces in a month aren't billed twice – no "double dipping." -In general, any action that modifies financial data or participates in a financial workflow is billable activity. +### Billing and Subscriptions +#### [Billing Page] What is the status of the billing page? +The billing page is currently under development and will be available soon. Stay tuned for updates on how to access and use the new billing features. -#### Why do some workspace members using paid features not require paid seats? -In general, any workspace member that uses paid functionality will require a paid seat. However, if you own two or more workspaces with the same member, and the member uses paid functionality on multiple workspaces in a given month (ie, an admin approving expense reports on two different workspaces), you will not be billed twice for the same member – there is no "double dipping." \ No newline at end of file +#### [Recovery Codes Usage] How do I use my recovery codes if I lose access to my authenticator app? +Your recovery codes work the same way as your authenticator codes. Just enter a recovery code as you would the authenticator code. \ No newline at end of file diff --git a/help/map.md b/help/map.md new file mode 100644 index 000000000000..eb218e67dcc0 --- /dev/null +++ b/help/map.md @@ -0,0 +1,371 @@ +--- +layout: product +title: Application Map +--- + +## Application Map +Lost in the app? Let this map guide you! + +* Inbox + * Workspace selector + * Chat selector + * Special chats: + * Concierge + * Workspace chat + * #announce + * #admins + * Personal chat + * Chat modifiers: + * Pin + * Green dot + * Red dot + * Unread + * Current chat + * Chat header + * Profile image + * Name + * Description + * Pin / Unpin + * Share shortcut + * Members + * Invite member + * Bulk actions + * Find a member + * Member list + * Select all + * Member row + * Profile image + * Name + * Remove from chat + * Profile link + * Profile image + * Message + * Email + * Preferred pronouns + * Local time + * Settings + * Notify me about new messages + * Who can post + * Visibility + * Private notes + * Leave + * Message list + * Message actions + * Add reaction + * Reply in thread + * Mark as unread + * Join thread + * Copy link + * Flag as offensive + * Download + * Message composer + * Attach + * Split expense + * Assign task + * Add attachment + * Write something + * Emoji + * Send +* Search + * Type selector + * State selector + * Filters + * Search results table + * Select all + * Search results row +* Settings + * Status shortcut + * Profile + * Public + * Display name + * Contact method + * Status + * Emoji + * Message + * Clear after + * Pronouns + * Timezone + * Automatically determine your location + * Timezone + * Share + * QR Code + * Copy URL + * Get $250 + * Private + * Legal name + * Date of birth + * Phone number + * Address + * Wallet + * Bank accounts + * Assigned cards + * Send and receive money with friends + * Preferences + * Notifications + * Receive relevant feature updates and Expensify news + * Mute all sounds from Expensify + * Priority mode + * Language + * Theme + * Security + * Two-factor authentication + * Close account + * Workspaces + * Profile + * Profile image + * Name + * Description + * Default currency + * Company address + * Share + * Delete + * Members + * Bulk actions + * Remove members + * Make member + * Make admin + * Make auditor + * Profile image + * Name + * Remove from workspace + * Role + * Profile shortcut + * Invoices + * Invoice balance + * Bank accounts + * Bank account + * Make default payment method + * Delete + * Add bank account + * Invoicing details + * Company name + * Company website + * Invoicing details + * Distance rates + * Add rate + * Bulk actions + * Settings + * Rate table + * Rate column + * Status column + * Rate row + * Enable rate + * Rate + * Delete + * Expensify Card + * Issue new card + * Workflows + * Delay submissions + * Submission frequency + * Add approvals + * Approvals + * Expenses from + * Approver + * Add approval workflow + * Make or track payments + * Connect bank account + * Connect online with Plaid + * Connect manually + * Rules + * Expenses + * Receipt required amount + * Max expense amount + * Max expense age + * Billable default + * eReceipts + * Expense reports + * Custom report names + * Prevent self-approvals + * Auto-approve compliant reports + * Auto-pay approved reports + * Categories + * Add category + * Bulk actions + * Delete categories + * Enable categories + * Settings + * Members must categorize all expenses -- Why not in Rules? + * Default spend categories + * Three dots menu + * Import spreadsheet + * Download CSV + * Category table + * Name column + * Status column + * Category row + * Enable category + * Name + * GL code + * Payroll code + * Category rules: + * Require description + * Default tax rate + * Flag amounts over + * Require receipts over + * Delete + * Tags + * Add tag + * Bulk actions + * Delete tag + * Disable tag + * Settings + * Custom tag name + * Members must tag all expenses + * Track billable expenses + * Three dots menu + * Import spreadsheet + * Download CSV + * Tag table + * Name column + * Status column + * Tag row + * Enable tag + * Name + * GL code + * Delete + * Taxes + * Add rate + * Bulk actions + * Delete rate + * Disable rate + * Settings + * Custom tax name + * Workspace currency default + * Foreign currency default + * Tax table + * Name + * Status + * Tax row + * Enable rate + * Name + * Value + * Tax code + * Report fields + * Add field + * Bulk actions + * Delete field + * Field table + * Name column + * Type column + * Field row + * Name + * Type + * Initial value + * Delete + * Accounting + * Connections list + * Quickbooks Online Connect + * Quickbooks Desktop Connect + * Xero + * NetSuite + * Sage Intacct + * More features + * Spend + * Distance rates + * Expensify Card + * Manage + * Workflows + * Rules + * Earn + * Invoices + * Organize + * Categories + * Tags + * Taxes + * Report fields + * Integrate + * Accounting + * Subscription + * Payment + * View payment history + * Request refund + * Your plan + * Subscription details + * Annual subscription + * Pay-per-user + * Three dot menu + * Request tax exempt status + * Domains + * Help + * Switch to Expensify Classic + * About + * App download links + * View keyboard shortcuts + * View the code + * View open jobs + * Report a bug + * Troubleshoot + * Client side logging + * Mask fragile user data wile exporting Onyx state + * Import Onyx state + * Export Onyx state + * Clear cache and restart + * Testing preferences + * Debug mode + * Use Staging server + * Force offline + * Simulate failing network requests + * Authentication status + * Device credentials + * Save the world + * Teachers Unite + * I know a teacher + * I am a teacher + * Sign out +* Search router + * Search for something + * Recent searches + * Recent chats +* Global Create + * Start chat + * Chat + * Name, email, or phone number + * Recents + * Contacts + * Add to group + * Room + * Room name + * Room description + * Workspace + * Visibility + * Track expense + 1. Choose type: + * Manual + * Amount + * Currency + * Scan + * Choose file + * Camera + * Distance + * Start + * Stop + 2. Code the expense + * Amount + * Description + * Show more + * Merchant + * Date + * Submit expense + 1. Choose type: + * Manual + * Amount + * Currency + * Scan + * Choose file + * Camera + * Distance + * Start + * Stop + 2. Choose who to submit to + * Name, email, or phone number + * Recents + * Contacts + 3. Code the expense + * Amount + * Description + * Show more + * Merchant + * Date + * Book travel + * Quick Action Button +* Magic link page diff --git a/help/travel.md b/help/travel.md index 43e082896ce4..351f83f90ba4 100644 --- a/help/travel.md +++ b/help/travel.md @@ -7,7 +7,7 @@ Expensify Travel is a comprehensive travel management platform integrated direct ### [Main uses] When should I use Expensify Travel? Expensify Travel is perfect for any situation involving corporate or personal travel, including: -* **Booking business travel** - Book flights, hotels, and car rentals in a few clicks, all within the Expensify platform. +* **Booking business travel** - Book flights, hotels, car rentals, and train travel in a few presses, all within the Expensify platform. * **Tracking travel expenses** - Automatically capture travel-related expenses such as airfare, lodging, and meals, ensuring everything is logged without manual input. * **Managing employee travel** - Empower managers to oversee travel bookings, set travel policies, and approve expenses in real time. * **Ensuring compliance with travel policies** - Use travel policies to enforce company rules around budgets, preferred vendors, and travel categories. @@ -38,59 +38,104 @@ Expensify Travel integrates a seamless booking experience directly into the app: * **Flights** - Search for and book flights, choosing from corporate-approved airlines or vendors. * **Hotels** - Book hotels using preferred vendor rates or select your own accommodations, with policy checks to ensure compliance. * **Car rentals** - Rent vehicles from top providers, with automatic receipt tracking and expense capture. -* **All in one place** - View and manage your full itinerary (flights, hotels, cars) from a single interface. +* **Trains** - Book train travel alongside other modes of transportation. +* **All in one place** - View and manage your full itinerary (flights, hotels, cars, trains) from a single interface. -### [Travel policies] How do I enforce company travel policies? +### Travel Policies Corporate travel policies can be configured in Expensify Travel to ensure compliance: * **Budgets** - Set maximum budgets for flights, hotels, and other travel-related expenses. * **Preferred vendors** - Require employees to book through specific airlines, hotel chains, or rental agencies to take advantage of corporate rates. * **Approval workflows** - Ensure all travel plans are reviewed and approved by the appropriate managers before booking. * **Expense categories** - Automatically categorize travel expenses in line with company accounting policies. -### [Approvals] How does the travel approval process work? -Travel approvals in Expensify are designed to ensure compliance before any bookings are confirmed: -1. **Request travel** - Employees submit travel requests, including flights, hotels, and car rentals, directly in the app. -2. **Automatic policy checks** - Expensify automatically flags any out-of-policy bookings or expenses for manager review. -3. **Manager approval** - Managers can approve or reject travel requests with one click, ensuring compliance before the trip is booked. -4. **Track approval status** - Both employees and managers can monitor the status of a travel request in real time. +### Approval Methods +Expensify Travel offers three approval methods to accommodate different organizational needs: Soft Approval, Hard Approval, and Passive Approval. -### [Expense integration] How does Expensify Travel integrate with Expensify Expense? -Expensify Travel works seamlessly with Expensify Expense to automate the handling of travel expenses: -* **Automatic expense capture** - Travel-related expenses (flights, hotels, meals) are automatically imported into Expensify Expense for easy tracking and reimbursement. -* **Real-time tracking** - Travel expenses appear in your expense report as soon as they are incurred, providing real-time visibility into costs. -* **One-click submission** - Employees can submit all travel expenses in a single report, and managers can approve them in bulk. +- **Soft Approval**: Bookings are automatically approved unless a manager declines them within 24 hours. If not declined, the arrangements proceed even if they are out of policy. +- **Hard Approval**: Bookings are automatically canceled if not approved within 24 hours. +- **Passive Approval**: Managers are notified of out-of-policy travel, but no action is required. -### [Corporate cards] Can I use Expensify Cards with Expensify Travel? -Yes, Expensify Travel integrates with Expensify Cards: -* **Automatic e-receipts** - Travel purchases made with an Expensify Card automatically generate e-receipts, eliminating the need for paper receipts. -* **Real-time expense tracking** - Expenses made with the Expensify Card are logged in real-time and categorized according to your travel policies. -* **Spend controls** - Set card limits and track spend in real-time to ensure that employees stay within budget. - -## Platforms -Expensify Travel is accessible from all platforms, making it easy to manage travel from anywhere: -* **Web app** - Manage your travel plans from your desktop via the Expensify web app. -* **Mobile app** - Book travel and track expenses on the go using the Expensify mobile app for iOS and Android. -* **Desktop app** - Use the Expensify desktop app for Mac or Windows to access the full range of travel and expense management features. +### Travel Member Roles +Assign roles to manage travel permissions within Expensify Travel: +* **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 or remove users, configure corporate cards as payment methods, view analytics and metrics, and use the Safety feature. ## Tutorials -### [Book travel] How do I book a flight, hotel, or car rental? -1. Navigate to the **Travel** section in the Expensify app. -2. Select **Book Flight**, **Book Hotel**, or **Book Car Rental**. -3. Enter your travel dates, destination, and any other required details. -4. Select a flight, hotel, or car from the available options. -5. Confirm your booking and add it to your travel itinerary. - -### [Submit travel request] How do I submit a travel request for approval? +### [Book Travel] How do I book a flight, hotel, or car rental? +To book travel from the Expensify app, follow these steps: +1. Press the **Travel** tab. +2. Press **Book or manage travel**. +3. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +4. Enter the travel information relevant to the selected arrangement (destination, dates of travel, etc.). +5. Select all the details for the arrangement you wish to book. +6. Review the booking details and press **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. + +The traveler is emailed an itinerary of the booking. Additionally, +- Their travel details are added to a Trip chat room under their primary workspace. +- An expense report for the trip is created. +- If booked with an Expensify Card, the trip is automatically reconciled. + +### [Submit Travel Request] How do I submit a travel request for approval? +To submit a travel request: 1. Go to **Create** > **Travel Request**. 2. Enter the details of your trip, including flights, hotels, and rental cars. 3. Review your travel options and ensure they are within policy. 4. Submit the request to your manager for approval. -### [Approve travel] How do I approve a travel request? +### [Approve Travel] How do I approve a travel request? +To approve a travel request: 1. Go to your **Inbox** and find the travel request awaiting approval. 2. Review the trip details, including any out-of-policy flags. -3. Click **Approve** or **Reject** as appropriate. +3. Press **Approve** or **Reject** as appropriate. + +### [Edit or Cancel Travel Arrangements] How do I modify or cancel a travel booking? +If you need to edit or cancel your travel arrangements, you can do so through the Expensify app: + +1. Open the Trip chat in your inbox to review your travel arrangements. +2. Press your profile image or icon in the bottom left menu. +3. Scroll down and press **Workspaces** in the left menu. +4. Select the workspace the travel is booked under. +5. Tap into the booking to see more details. +6. Press **Trip Support** for assistance. + +If there are unexpected changes to your itinerary, such as a flight cancellation, Expensify’s travel partner **Spotnana** will reach out to provide updates. Note that any modifications, exchanges, or cancellations made through support will incur a $25 booking change fee. + +### [Configure Travel Policy] How do I set up a travel policy for my workspace? +Workspace admins can create and update travel policies to establish travel rules for different groups of travelers. To configure a travel policy: + +1. Press the **Travel** tab and select **Book or manage travel**. +2. Select the **Program** tab and choose **Policies**. +3. Under Employee or Non-employee, press **Add new** to create a new policy. +4. In the **Edit members** section, select the group of employees for the policy. +5. Choose travel preferences to modify: General, flight, hotel, car, or rail. +6. Press the paperclip icon next to each setting to de-couple it from the default policy. +7. Update the desired settings and save changes. + +### [Demo Video] How can I watch a demo of Expensify Travel? +To see how Expensify Travel works, watch the demo video: +- The video provides a comprehensive overview of using Expensify Travel for booking and managing travel. + +### [Set Approval Method] How do I set the approval method for travel expenses? +To configure the approval method for travel expenses in Expensify: +1. Press the **Travel** tab and choose **Book or manage travel**. +2. Navigate to the **Program** tab and select **Policies**. +3. Under the General section, select the approval methods for Flights, Hotels, Cars, and Rail, choosing between Soft Approval, Hard Approval, or Passive Approval. + +### [Manage Travel Member Roles] How do I assign roles to travel members? +To manage travel member roles within Expensify: +1. Press the **Travel** tab and select **Book or manage travel**. +2. Select the **Program** tab and choose **Users**. +3. Press the name of the member whose role you wish to update. +4. Press the **Roles** tab and select the desired role. +5. Press **Save** to confirm the changes. + +### [Approve Travel Booking] How do I approve or decline a travel booking? +To manage travel booking approvals effectively: +1. Once an employee books a trip, you will receive an email notification with booking details. +2. For **Soft Approval**, no action is required to approve, but to decline, follow the email prompt within 24 hours and press **Decline booking**, then **Deny Booking**. +3. For **Hard Approval**, press **Approve booking** to confirm or **Decline booking** to reject, then follow the respective prompts. ## FAQ @@ -109,3 +154,23 @@ Yes, Expensify Travel supports international bookings and expense tracking in mu ### How do I integrate Expensify Travel with my company’s existing travel policies? You can configure travel policies directly in Expensify by setting budgets, preferred vendors, and approval workflows. These policies will automatically be enforced whenever employees book travel. +### Are extended approval windows given for trips booked over the weekend or during company holidays? +No, the approval window is fixed at 24 hours from when the trip is booked. + +### How does Expensify Travel handle approvals when the assigned approver is out of office? +It is recommended to have multiple approvers set up for travel, as there is no delegated approval for out-of-office scenarios. + +### Can travelers upload a document when submitting a trip for approval? +Travelers cannot upload a document at the time of trip submission, but companies can use a 'reason code' in the Out of Policy rules, which travelers complete at checkout. Documents can then be added to the expense report during submission in Expensify. + +### [Expense Integration] How do I use Expensify Travel with Expensify Expense? +Expensify Travel works seamlessly with Expensify Expense to automate the handling of travel expenses: +* **Automatic expense capture** - Travel-related expenses (flights, hotels, meals) are automatically imported into Expensify Expense for easy tracking and reimbursement. +* **Real-time tracking** - Travel expenses appear in your expense report as soon as they are incurred, providing real-time visibility into costs. +* **One-click submission** - Employees can submit all travel expenses in a single report, and managers can approve them in bulk. + +### [Corporate Cards] Can I use Expensify Cards with Expensify Travel? +Yes, Expensify Travel integrates with Expensify Cards: +* **Automatic e-receipts** - Travel purchases made with an Expensify Card automatically generate e-receipts, eliminating the need for paper receipts. +* **Real-time expense tracking** - Expenses made with the Expensify Card are logged in real time and categorized according to your travel policies. +* **Spend controls** - Set card limits and track spend in real time to ensure that employees stay within budget. \ No newline at end of file diff --git a/ios/GoogleService-Info-DEV.plist b/ios/GoogleService-Info-DEV.plist new file mode 100644 index 000000000000..5bfb1a332dfc --- /dev/null +++ b/ios/GoogleService-Info-DEV.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 921154746561-8niu5ba8g4dgsqsqso3lugdhe6vikqpq.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.921154746561-8niu5ba8g4dgsqsqso3lugdhe6vikqpq + ANDROID_CLIENT_ID + 921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com + API_KEY + AIzaSyA9Qn7q5Iw26gTzjI7012C4PaFrFagpC_I + GCM_SENDER_ID + 921154746561 + PLIST_VERSION + 1 + BUNDLE_ID + com.expensify.chat.dev + PROJECT_ID + expensify-chat + STORAGE_BUCKET + expensify-chat.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:921154746561:ios:12c3a0b9276d7d2f027c40 + DATABASE_URL + https://expensify-chat.firebaseio.com + + diff --git a/ios/GoogleService-Info.plist b/ios/GoogleService-Info.plist index 147bec8c2875..e8549ed328fd 100644 --- a/ios/GoogleService-Info.plist +++ b/ios/GoogleService-Info.plist @@ -6,6 +6,8 @@ 921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com REVERSED_CLIENT_ID com.googleusercontent.apps.921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3 + ANDROID_CLIENT_ID + 921154746561-cbegir0tnc2gan6k1gre5vtn75p60hom.apps.googleusercontent.com API_KEY AIzaSyA9Qn7q5Iw26gTzjI7012C4PaFrFagpC_I GCM_SENDER_ID @@ -21,7 +23,7 @@ IS_ADS_ENABLED IS_ANALYTICS_ENABLED - + IS_APPINVITE_ENABLED IS_GCM_ENABLED @@ -33,4 +35,4 @@ DATABASE_URL https://expensify-chat.firebaseio.com - \ No newline at end of file + diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index b3ec8febb1df..d8eceab72b95 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 0CDA8E38287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; }; 0DFC45942C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; }; 0DFC45952C884E0A00B56C91 /* RCTShortcutManagerModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */; }; - 0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */; }; 0F5E5350263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; 0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */; }; 1246A3EF20E54E7A9494C8B9 /* ExpensifyNeue-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = F4F8A052A22040339996324B /* ExpensifyNeue-Regular.otf */; }; @@ -34,6 +33,9 @@ 70CF6E82262E297300711ADC /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 70CF6E81262E297300711ADC /* BootSplash.storyboard */; }; 7F5E81F06BCCF61AD02CEA06 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */; }; 7F9DD8DA2B2A445B005E3AFA /* ExpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */; }; + 7FB680AE2CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; + 7FB680AF2CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; + 7FB680B02CC94EDA006693CF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */; }; 7FD73C9E2B23CE9500420AF3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */; }; 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */; }; @@ -43,7 +45,7 @@ D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -94,7 +96,6 @@ 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = NewExpensify/PrivacyInfo.xcprivacy; sourceTree = ""; }; 0DFC45922C884D7900B56C91 /* RCTShortcutManagerModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTShortcutManagerModule.h; sourceTree = ""; }; 0DFC45932C884E0A00B56C91 /* RCTShortcutManagerModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTShortcutManagerModule.m; sourceTree = ""; }; - 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 0F5E534E263B73D5004CA14F /* EnvironmentChecker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnvironmentChecker.h; sourceTree = ""; }; 0F5E534F263B73FD004CA14F /* EnvironmentChecker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnvironmentChecker.m; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* New Expensify Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "New Expensify Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -133,6 +134,7 @@ 7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NewExpensifyReleaseProduction.entitlements; path = NewExpensify/NewExpensifyReleaseProduction.entitlements; sourceTree = ""; }; 7F9C91352CA5EC4900FC4DC1 /* NotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationServiceExtension.entitlements; sourceTree = ""; }; 7F9DD8D92B2A445B005E3AFA /* ExpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpError.swift; sourceTree = ""; }; + 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7FD73C9D2B23CE9500420AF3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 7FD73C9F2B23CE9500420AF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -176,8 +178,8 @@ buildActionMask = 2147483647; files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,13 +214,13 @@ 13B07FAE1A68108700A75B9A /* NewExpensify */ = { isa = PBXGroup; children = ( + 7FB680AD2CC94EDA006693CF /* GoogleService-Info.plist */, 7F3784A72C75131000063508 /* NewExpensifyReleaseProduction.entitlements */, 7F3784A62C7512D900063508 /* NewExpensifyReleaseAdHoc.entitlements */, 7F3784A52C7512CF00063508 /* NewExpensifyReleaseDevelopment.entitlements */, 7F3784A42C7512BF00063508 /* NewExpensifyDebugProduction.entitlements */, 7F3784A32C75129D00063508 /* NewExpensifyDebugAdHoc.entitlements */, 7F3784A22C75103800063508 /* NewExpensifyDebugDevelopment.entitlements */, - 0F5BE0CD252686320097D869 /* GoogleService-Info.plist */, E9DF872C2525201700607FDC /* AirshipConfig.plist */, 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */, 008F07F21AC5B25A0029DE68 /* main.jsbundle */, @@ -500,6 +502,7 @@ buildActionMask = 2147483647; files = ( 0CDA8E38287DD6A0004ECBEC /* Images.xcassets in Resources */, + 7FB680B02CC94EDA006693CF /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -507,7 +510,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 0F5BE0CE252686330097D869 /* GoogleService-Info.plist in Resources */, E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */, F0C450EA2705020500FD2970 /* colors.json in Resources */, 083353EB2B5AB22A00C603C0 /* attention.mp3 in Resources */, @@ -519,6 +521,7 @@ 083353EE2B5AB22A00C603C0 /* success.mp3 in Resources */, 0C7C65547D7346EB923BE808 /* ExpensifyMono-Regular.otf in Resources */, 2A9F8CDA983746B0B9204209 /* ExpensifyNeue-Bold.otf in Resources */, + 7FB680AE2CC94EDA006693CF /* GoogleService-Info.plist in Resources */, 083353EC2B5AB22A00C603C0 /* done.mp3 in Resources */, 083353ED2B5AB22A00C603C0 /* receive.mp3 in Resources */, ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */, @@ -532,6 +535,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7FB680AF2CC94EDA006693CF /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme index 93d775217f11..f9acbe8abe4f 100644 --- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme +++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify Dev.xcscheme @@ -60,6 +60,16 @@ ReferencedContainer = "container:NewExpensify.xcodeproj"> + + + + + + CFBundlePackageType APPL CFBundleShortVersionString - 9.0.54 + 9.0.59 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.54.1 + 9.0.59.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cb867d7af0b5..96070daa066c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.54 + 9.0.59 CFBundleSignature ???? CFBundleVersion - 9.0.54.1 + 9.0.59.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c7c9879bb2ab..e0bef4291004 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.54 + 9.0.59 CFBundleVersion - 9.0.54.1 + 9.0.59.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9a706cc4e8aa..5268e5e9fb24 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2329,10 +2329,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDevMenu (4.1.1): - - React-Core - - React-Core/DevSupport - - React-RCTNetwork - RNFBAnalytics (12.9.3): - Firebase/Analytics (= 8.8.0) - React-Core @@ -2395,7 +2391,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.176): + - RNLiveMarkdown (0.1.180): - DoubleConversion - glog - hermes-engine @@ -2415,9 +2411,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.176) + - RNLiveMarkdown/newarch (= 0.1.180) - Yoga - - RNLiveMarkdown/newarch (0.1.176): + - RNLiveMarkdown/newarch (0.1.180): - DoubleConversion - glog - hermes-engine @@ -2507,7 +2503,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.15.1): + - RNReanimated (3.15.3): - DoubleConversion - glog - hermes-engine @@ -2527,10 +2523,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.15.1) - - RNReanimated/worklets (= 3.15.1) + - RNReanimated/reanimated (= 3.15.3) + - RNReanimated/worklets (= 3.15.3) - Yoga - - RNReanimated/reanimated (3.15.1): + - RNReanimated/reanimated (3.15.3): - DoubleConversion - glog - hermes-engine @@ -2551,7 +2547,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.15.1): + - RNReanimated/worklets (3.15.3): - DoubleConversion - glog - hermes-engine @@ -2824,7 +2820,6 @@ DEPENDENCIES: - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) - - RNDevMenu (from `../node_modules/react-native-dev-menu`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" @@ -3085,8 +3080,6 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-picker/picker" RNDeviceInfo: :path: "../node_modules/react-native-device-info" - RNDevMenu: - :path: "../node_modules/react-native-dev-menu" RNFBAnalytics: :path: "../node_modules/@react-native-firebase/analytics" RNFBApp: @@ -3263,7 +3256,6 @@ SPEC CHECKSUMS: RNCClipboard: c84275d07e3f73ff296b17e6c27e9ccdc194a0bb RNCPicker: 21ae0659666767a5c1253aef985ee5b7c527e345 RNDeviceInfo: 130237d8e97a89b68f2202d5dd18ac6bb68e7648 - RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 @@ -3272,12 +3264,12 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 0b8756147a5e8eeea98d3e1187c0c27d5a96d1ff + RNLiveMarkdown: fc07b203a3ed832e2e5d3950e69cd4fc3b0568b6 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 RNReactNativeHapticFeedback: 73756a3477a5a622fa16862a3ab0d0fc5e5edff5 - RNReanimated: 76901886830e1032f16bbf820153f7dc3f02d51d + RNReanimated: f46df3b08d5d59cd83c47bb6697ce88e565e0dc7 RNScreens: de6e57426ba0e6cbc3fb5b4f496e7f08cb2773c2 RNShare: bd4fe9b95d1ee89a200778cc0753ebe650154bb0 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 diff --git a/package-lock.json b/package-lock.json index 7202ef76ab66..594888c76972 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.59-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.59-0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.176", + "@expensify/react-native-live-markdown": "0.1.180", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -51,7 +51,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.100", + "expensify-common": "2.0.106", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -77,11 +77,10 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", - "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", + "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", - "react-native-dev-menu": "^4.1.1", "react-native-device-info": "10.3.1", "react-native-document-picker": "^9.3.1", "react-native-draggable-flatlist": "^4.0.1", @@ -96,7 +95,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.73", + "react-native-onyx": "2.0.78", "react-native-pager-view": "6.4.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -105,7 +104,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "3.15.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -117,7 +116,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "4.0.0-beta.13", - "react-native-web": "^0.19.12", + "react-native-web": "0.19.13", "react-native-webview": "13.8.6", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", @@ -157,6 +156,7 @@ "@perf-profiler/profiler": "^0.10.10", "@perf-profiler/reporter": "^0.9.0", "@perf-profiler/types": "^0.8.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@react-native-community/eslint-config": "3.2.0", "@react-native/babel-preset": "0.75.2", "@react-native/metro-config": "0.75.2", @@ -219,7 +219,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.66", + "eslint-config-expensify": "^2.0.73", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -252,6 +252,7 @@ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", + "react-refresh": "^0.14.2", "react-test-renderer": "18.3.1", "reassure": "^1.0.0-rc.4", "semver": "7.5.2", @@ -3630,9 +3631,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.176", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.176.tgz", - "integrity": "sha512-0IS0Rfl0qYqrE2V8jsVX58c4K/zxeNC7o1CAL9Xu+HTbTtD58Yu5gOOwp5AljkS2qdPR86swGRZyLXGkGRKkPg==", + "version": "0.1.180", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.180.tgz", + "integrity": "sha512-toyFMl5nXQiC2lY6x/bGagsXaeCevZjVuebnClwVZskrPMI65o8OH/Y1VvTly9eNWD04Br++ANmOPJZYMisEiQ==", "license": "MIT", "workspaces": [ "parser", @@ -7431,6 +7432,85 @@ "node": ">=14" } }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", + "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", + "dev": true, + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.25", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", @@ -17361,6 +17441,18 @@ "node": ">=6" } }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, "node_modules/ansi-html-community": { "version": "0.0.8", "dev": true, @@ -20713,6 +20805,17 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-pure": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.38.1.tgz", + "integrity": "sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "license": "MIT" @@ -22800,10 +22903,11 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.66", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.66.tgz", - "integrity": "sha512-6L9EIAiOxZnqOcFEsIwEUmX0fvglvboyqQh7LTqy+1O2h2W3mmrMSx87ymXeyFMg1nJQtqkFnrLv5ENGS0QC3Q==", + "version": "2.0.73", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.73.tgz", + "integrity": "sha512-LHHyujwjTBizm9mIQMv6g/MsAbYdeOLZrOBdFqY/LyGPUJxOr9jt22xlmTFSdKhieLrbDwkcgkXjM38Z46Nb9A==", "dev": true, + "license": "ISC", "dependencies": { "@babel/eslint-parser": "^7.25.7", "@lwc/eslint-plugin-lwc": "^1.7.2", @@ -24050,9 +24154,10 @@ } }, "node_modules/expensify-common": { - "version": "2.0.100", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.100.tgz", - "integrity": "sha512-mektI+OuTywYU47Valjsn2+kLQ1/Wc9sWCY1/a0Vo8IHTXroQWvbKs5IXlkiqODi4SRonVZwOL3ha/oJD7o7nQ==", + "version": "2.0.106", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.106.tgz", + "integrity": "sha512-KmxKvglbIUJb0sAcmNxb/AXYAqa3GIZfu3MbmtlYDNJx24mjDjtbGkKhm+16TICDoPj2PDRNogIqgUGWmSSZFQ==", + "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -34477,13 +34582,6 @@ } } }, - "node_modules/react-native-dev-menu": { - "version": "4.1.1", - "license": "MIT", - "peerDependencies": { - "react-native": ">=0.61.0" - } - }, "node_modules/react-native-device-info": { "version": "10.3.1", "license": "MIT", @@ -35406,9 +35504,10 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.73", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.73.tgz", - "integrity": "sha512-ZgzTS9TV3wIh6cYfBM5sXrYz5A37x47a61n07e24p22gr7DosBX6J8ixaVCkC25G58A+2A+jRfzdtwRC5yW34A==", + "version": "2.0.78", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.78.tgz", + "integrity": "sha512-YL6Zk470TOjhpccf2wqAi4bvvJyDrQccTAYsz+woZu+rKr74UX693U2EhP8ncZ7+dzgfS7zGKep2mwKVesfiWw==", + "license": "MIT", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -35528,9 +35627,10 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.1.tgz", - "integrity": "sha512-DbBeUUExtJ1x1nfE94I8qgDgWjq5ztM3IO/+XFO+agOkPeVpBs5cRnxHfJKrjqJ2MgwhJOUDmtHxo+tDsoeitg==", + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.15.3.tgz", + "integrity": "sha512-5QBk/7PZvZ98Adxm4MRyglwzsRzReTQIe4Hd2wbBBAZ68IC4OYKvsc8cPEjgx3/1mG8HgHFYhbcDe5U2RjeFqw==", + "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -35726,8 +35826,9 @@ } }, "node_modules/react-native-web": { - "version": "0.19.12", - "license": "MIT", + "version": "0.19.13", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", + "integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==", "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -36745,7 +36846,8 @@ }, "node_modules/react-refresh": { "version": "0.14.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index f0425a747967..a7f306235c0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.54-1", + "version": "9.0.59-0", "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.", @@ -33,6 +33,7 @@ "ios-build": "bundle exec fastlane ios build_unsigned", "android-build": "bundle exec fastlane android build_local", "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", "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 eslint --max-warnings=0 --config ./.eslintrc.changed.js $(git diff --diff-filter=AM --name-only origin/main HEAD -- \"*.ts\" \"*.tsx\")", @@ -67,7 +68,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.176", + "@expensify/react-native-live-markdown": "0.1.180", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -107,7 +108,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.100", + "expensify-common": "2.0.106", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -133,11 +134,10 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", - "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", + "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", - "react-native-dev-menu": "^4.1.1", "react-native-device-info": "10.3.1", "react-native-document-picker": "^9.3.1", "react-native-draggable-flatlist": "^4.0.1", @@ -152,7 +152,7 @@ "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.73", + "react-native-onyx": "2.0.78", "react-native-pager-view": "6.4.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -161,7 +161,7 @@ "react-native-plaid-link-sdk": "11.11.0", "react-native-qrcode-svg": "6.3.11", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#99f34ebefa91698945f3ed26622e002bd79489e0", - "react-native-reanimated": "3.15.1", + "react-native-reanimated": "3.15.3", "react-native-release-profiler": "^0.2.1", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.10.9", @@ -173,7 +173,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", "react-native-vision-camera": "4.0.0-beta.13", - "react-native-web": "^0.19.12", + "react-native-web": "0.19.13", "react-native-webview": "13.8.6", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", @@ -213,6 +213,7 @@ "@perf-profiler/profiler": "^0.10.10", "@perf-profiler/reporter": "^0.9.0", "@perf-profiler/types": "^0.8.0", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@react-native-community/eslint-config": "3.2.0", "@react-native/babel-preset": "0.75.2", "@react-native/metro-config": "0.75.2", @@ -275,7 +276,7 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.66", + "eslint-config-expensify": "^2.0.73", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-jest": "^28.6.0", @@ -308,6 +309,7 @@ "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", + "react-refresh": "^0.14.2", "react-test-renderer": "18.3.1", "reassure": "^1.0.0-rc.4", "semver": "7.5.2", @@ -367,13 +369,11 @@ }, "electronmon": { "patterns": [ - "!node_modules", - "!node_modules/**/*", - "!**/*.map", + "!src/**", "!ios/**", "!android/**", - "*.test.*", - "*.spec.*" + "!tests/**", + "*.test.*" ] }, "engines": { diff --git a/patches/react-native+0.75.2+012+Add-onPaste-to-TextInput.patch b/patches/react-native+0.75.2+012+Add-onPaste-to-TextInput.patch index 55657e61dc09..e5ddeee282fb 100644 --- a/patches/react-native+0.75.2+012+Add-onPaste-to-TextInput.patch +++ b/patches/react-native+0.75.2+012+Add-onPaste-to-TextInput.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index a77e5b4..5e58ec4 100644 +index 6c4bbb2..770dfee 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -455,6 +455,21 @@ export type NativeProps = $ReadOnly<{| +@@ -462,6 +462,21 @@ export type NativeProps = $ReadOnly<{| |}>, >, @@ -24,7 +24,7 @@ index a77e5b4..5e58ec4 100644 /** * The string that will be rendered before text input has been entered. */ -@@ -658,6 +673,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { +@@ -668,6 +683,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { topScroll: { registrationName: 'onScroll', }, @@ -34,7 +34,7 @@ index a77e5b4..5e58ec4 100644 }, validAttributes: { maxFontSizeMultiplier: true, -@@ -711,6 +729,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { +@@ -722,6 +740,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { secureTextEntry: true, textBreakStrategy: true, onScroll: true, @@ -43,7 +43,7 @@ index a77e5b4..5e58ec4 100644 disableFullscreenUI: true, includeFontPadding: true, diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 3bfe22c..1cb122f 100644 +index 8326797..dbfe5d5 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -88,6 +88,9 @@ const RCTTextInputViewConfig = { @@ -56,7 +56,7 @@ index 3bfe22c..1cb122f 100644 }, validAttributes: { fontSize: true, -@@ -153,6 +156,7 @@ const RCTTextInputViewConfig = { +@@ -154,6 +157,7 @@ const RCTTextInputViewConfig = { onSelectionChange: true, onContentSizeChange: true, onScroll: true, @@ -170,7 +170,7 @@ index a94fb19..8cfde15 100644 * The string that will be rendered before text input has been entered. */ diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm -index d5e2e22..a11679a 100644 +index d5e2e22..065a819 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -13,6 +13,10 @@ @@ -184,7 +184,7 @@ index d5e2e22..a11679a 100644 @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; -@@ -172,7 +176,32 @@ - (void)scrollRangeToVisible:(NSRange)range +@@ -172,7 +176,31 @@ - (void)scrollRangeToVisible:(NSRange)range - (void)paste:(id)sender { _textWasPasted = YES; @@ -197,8 +197,7 @@ index d5e2e22..a11679a 100644 + if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { + NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); + NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *fileName = [NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString], fileExtension]; -+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; ++ NSString *filePath = RCTTempFilePath(fileExtension, nil); + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + NSData *fileData = [clipboard dataForPasteboardType:identifier]; + [fileData writeToFile:filePath atomically:YES]; @@ -218,7 +217,7 @@ index d5e2e22..a11679a 100644 } // Turn off scroll animation to fix flaky scrolling. -@@ -264,6 +293,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender +@@ -264,6 +292,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } @@ -346,7 +345,7 @@ index f58f147..e367394 100644 RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm -index 0318671..bb165d7 100644 +index 0318671..667e646 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -12,6 +12,10 @@ @@ -371,7 +370,7 @@ index 0318671..bb165d7 100644 return [super canPerformAction:action withSender:sender]; } -@@ -222,7 +230,32 @@ - (void)scrollRangeToVisible:(NSRange)range +@@ -222,7 +230,31 @@ - (void)scrollRangeToVisible:(NSRange)range - (void)paste:(id)sender { _textWasPasted = YES; @@ -384,8 +383,7 @@ index 0318671..bb165d7 100644 + if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { + NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); + NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); -+ NSString *fileName = [NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString], fileExtension]; -+ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; ++ NSString *filePath = RCTTempFilePath(fileExtension, nil); + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + NSData *fileData = [clipboard dataForPasteboardType:identifier]; + [fileData writeToFile:filePath atomically:YES]; diff --git a/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch b/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch new file mode 100644 index 000000000000..27fa6074e6ce --- /dev/null +++ b/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +index ed1aba8..0a9284f 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +@@ -891,7 +891,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { + sendEvent( + "keyboardDidHide", + createKeyboardEventPayload( +- PixelUtil.toDIPFromPixel(mVisibleViewArea.height()), ++ // Use mLastHeight to account for the translucent status bar, and fall back to getMeasuredHeight() on Bridgeless mode. ++ // Remove this patch once the upstream fix for https://github.com/facebook/react-native/issues/47140 is released. ++ PixelUtil.toDIPFromPixel(mWasMeasured ? mLastHeight : getMeasuredHeight()), + 0, + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + 0)); +@@ -940,7 +942,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { + sendEvent( + "keyboardDidHide", + createKeyboardEventPayload( +- PixelUtil.toDIPFromPixel(mVisibleViewArea.height()), ++ // Use mLastHeight to account for the translucent status bar, and fall back to getMeasuredHeight() on Bridgeless mode. ++ // Remove this patch once the upstream fix for https://github.com/facebook/react-native/issues/47140 is released. ++ PixelUtil.toDIPFromPixel(mWasMeasured ? mLastHeight : getMeasuredHeight()), + 0, + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + 0)); diff --git a/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch b/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch new file mode 100644 index 000000000000..3bea9012e9d5 --- /dev/null +++ b/patches/react-native+0.75.2+019+Android-onHostResume-resume-frame-callback.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java +index ce8520e..e2b22b0 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FabricEventDispatcher.java +@@ -122,6 +122,7 @@ public class FabricEventDispatcher implements EventDispatcher, LifecycleEventLis + @Override + public void onHostResume() { + maybePostFrameCallbackFromNonUI(); ++ mCurrentFrameCallback.resume(); + } + + @Override +@@ -190,6 +191,10 @@ public class FabricEventDispatcher implements EventDispatcher, LifecycleEventLis + mShouldStop = true; + } + ++ public void resume() { ++ mShouldStop = false; ++ } ++ + public void maybePost() { + if (!mIsPosted) { + mIsPosted = true; diff --git a/patches/react-native-reanimated+3.15.1+001+hybrid-app.patch b/patches/react-native-reanimated+3.15.3+001+hybrid-app.patch similarity index 100% rename from patches/react-native-reanimated+3.15.1+001+hybrid-app.patch rename to patches/react-native-reanimated+3.15.3+001+hybrid-app.patch diff --git a/patches/react-native-reanimated+3.15.1+002+dontWhitelistTextProp.patch b/patches/react-native-reanimated+3.15.3+002+dontWhitelistTextProp.patch similarity index 100% rename from patches/react-native-reanimated+3.15.1+002+dontWhitelistTextProp.patch rename to patches/react-native-reanimated+3.15.3+002+dontWhitelistTextProp.patch diff --git a/patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch b/patches/react-native-reanimated+3.15.3+003+fixNullViewTag.patch similarity index 100% rename from patches/react-native-reanimated+3.15.1+003+fixNullViewTag.patch rename to patches/react-native-reanimated+3.15.3+003+fixNullViewTag.patch diff --git a/patches/react-native-web+0.19.12+001+initial.patch b/patches/react-native-web+0.19.13+001+initial.patch similarity index 95% rename from patches/react-native-web+0.19.12+001+initial.patch rename to patches/react-native-web+0.19.13+001+initial.patch index c77cfc7829ed..75efdf4da117 100644 --- a/patches/react-native-web+0.19.12+001+initial.patch +++ b/patches/react-native-web+0.19.13+001+initial.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index e137def..c3e5054 100644 +index 1f52b73..53b1a83 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -287,7 +287,7 @@ class VirtualizedList extends StateSafePureComponent { // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. constructor(_props) { @@ -11,7 +11,7 @@ index e137def..c3e5054 100644 super(_props); this._getScrollMetrics = () => { return this._scrollMetrics; -@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { +@@ -522,6 +522,11 @@ class VirtualizedList extends StateSafePureComponent { visibleLength, zoomScale }; @@ -23,7 +23,7 @@ index e137def..c3e5054 100644 this._updateViewableItems(this.props, this.state.cellsAroundViewport); if (!this.props) { return; -@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -571,7 +576,7 @@ class VirtualizedList extends StateSafePureComponent { this._updateCellsToRender = () => { this._updateViewableItems(this.props, this.state.cellsAroundViewport); this.setState((state, props) => { @@ -32,7 +32,7 @@ index e137def..c3e5054 100644 var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { return null; -@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -591,7 +596,7 @@ class VirtualizedList extends StateSafePureComponent { return { index, item, @@ -41,7 +41,7 @@ index e137def..c3e5054 100644 isViewable }; }; -@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { +@@ -623,12 +628,10 @@ class VirtualizedList extends StateSafePureComponent { }; this._getFrameMetrics = (index, props) => { var data = props.data, @@ -55,7 +55,7 @@ index e137def..c3e5054 100644 if (!frame || frame.index !== index) { if (getItemLayout) { /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -652,7 +655,7 @@ class VirtualizedList extends StateSafePureComponent { // The last cell we rendered may be at a new index. Bail if we don't know // where it is. @@ -64,7 +64,7 @@ index e137def..c3e5054 100644 return []; } var first = focusedCellIndex; -@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { +@@ -692,9 +695,15 @@ class VirtualizedList extends StateSafePureComponent { } } var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); @@ -81,7 +81,7 @@ index e137def..c3e5054 100644 }; // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { +@@ -750,6 +759,26 @@ class VirtualizedList extends StateSafePureComponent { } } } @@ -108,7 +108,7 @@ index e137def..c3e5054 100644 static _createRenderMask(props, cellsAroundViewport, additionalRegions) { var itemCount = props.getItemCount(props.data); invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -798,7 +827,7 @@ class VirtualizedList extends StateSafePureComponent { } } } @@ -117,7 +117,7 @@ index e137def..c3e5054 100644 var data = props.data, getItemCount = props.getItemCount; var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { +@@ -821,17 +850,9 @@ class VirtualizedList extends StateSafePureComponent { last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) }; } else { @@ -138,7 +138,7 @@ index e137def..c3e5054 100644 return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; } newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { +@@ -904,16 +925,36 @@ class VirtualizedList extends StateSafePureComponent { } } static getDerivedStateFromProps(newProps, prevState) { @@ -177,7 +177,7 @@ index e137def..c3e5054 100644 }; } _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -936,7 +977,7 @@ class VirtualizedList extends StateSafePureComponent { last = Math.min(end, last); var _loop = function _loop() { var item = getItem(data, ii); @@ -186,7 +186,7 @@ index e137def..c3e5054 100644 _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { stickyHeaderIndices.push(cells.length); -@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { +@@ -971,20 +1012,23 @@ class VirtualizedList extends StateSafePureComponent { } static _constrainToItemCount(cells, props) { var itemCount = props.getItemCount(props.data); @@ -216,8 +216,8 @@ index e137def..c3e5054 100644 if (props.keyExtractor != null) { return props.keyExtractor(item, index); } -@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { - cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { +@@ -1024,7 +1068,12 @@ class VirtualizedList extends StateSafePureComponent { + cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" - }, /*#__PURE__*/React.createElement(View, { @@ -230,7 +230,7 @@ index e137def..c3e5054 100644 onLayout: this._onLayoutHeader, style: [inversionStyle, this.props.ListHeaderComponentStyle] }, -@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1126,7 +1175,11 @@ class VirtualizedList extends StateSafePureComponent { // TODO: Android support invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, stickyHeaderIndices, @@ -243,7 +243,7 @@ index e137def..c3e5054 100644 }); this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1317,8 +1370,12 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { onStartReached = _this$props8.onStartReached, onStartReachedThreshold = _this$props8.onStartReachedThreshold, onEndReached = _this$props8.onEndReached, @@ -258,7 +258,7 @@ index e137def..c3e5054 100644 var _this$_scrollMetrics2 = this._scrollMetrics, contentLength = _this$_scrollMetrics2.contentLength, visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1358,16 +1415,10 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { // and call onStartReached only once for a given content length, // and only if onEndReached is not being executed else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { @@ -279,7 +279,7 @@ index e137def..c3e5054 100644 } // If the user scrolls away from the start or end and back again, -@@ -1433,6 +1484,11 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1435,6 +1486,11 @@ class VirtualizedList extends StateSafePureComponent { */ _updateViewableItems(props, cellsAroundViewport) { diff --git a/patches/react-native-web+0.19.12+002+fixLastSpacer.patch b/patches/react-native-web+0.19.13+002+fixLastSpacer.patch similarity index 94% rename from patches/react-native-web+0.19.12+002+fixLastSpacer.patch rename to patches/react-native-web+0.19.13+002+fixLastSpacer.patch index 581298613492..c400dcfc8cca 100644 --- a/patches/react-native-web+0.19.12+002+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.13+002+fixLastSpacer.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/modules/AccessibilityUtil/propsToAccessibilityComponent.js b/node_modules/react-native-web/dist/modules/AccessibilityUtil/propsToAccessibilityComponent.js -index 9c9a533..7794181 100644 +index 7d1d587..de51afe 100644 --- a/node_modules/react-native-web/dist/modules/AccessibilityUtil/propsToAccessibilityComponent.js +++ b/node_modules/react-native-web/dist/modules/AccessibilityUtil/propsToAccessibilityComponent.js @@ -27,7 +27,8 @@ var roleComponents = { @@ -13,7 +13,7 @@ index 9c9a533..7794181 100644 var emptyObject = {}; var propsToAccessibilityComponent = function propsToAccessibilityComponent(props) { diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index 7f6c880..b05da08 100644 +index 53b1a83..5689220 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js @@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { @@ -31,7 +31,7 @@ index 7f6c880..b05da08 100644 /** * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) -@@ -1107,7 +1099,8 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1109,7 +1101,8 @@ class VirtualizedList extends StateSafePureComponent { _keylessItemComponentName = ''; var spacerKey = this._getSpacerKey(!horizontal); var renderRegions = this.state.renderMask.enumerateRegions(); diff --git a/patches/react-native-web+0.19.12+003+image-header-support.patch b/patches/react-native-web+0.19.13+003+image-header-support.patch similarity index 95% rename from patches/react-native-web+0.19.12+003+image-header-support.patch rename to patches/react-native-web+0.19.13+003+image-header-support.patch index d0a490a4ed70..15e83ce31f8a 100644 --- a/patches/react-native-web+0.19.12+003+image-header-support.patch +++ b/patches/react-native-web+0.19.13+003+image-header-support.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 9649d27..66ef95c 100644 +index 348831d..ca40ee8 100644 --- a/node_modules/react-native-web/dist/exports/Image/index.js +++ b/node_modules/react-native-web/dist/exports/Image/index.js -@@ -135,7 +135,22 @@ function resolveAssetUri(source) { +@@ -137,7 +137,22 @@ function resolveAssetUri(source) { } return uri; } @@ -13,7 +13,7 @@ index 9649d27..66ef95c 100644 + if (onError) { + onError({ + nativeEvent: { -+ error: "Failed to load resource " + uri + " (404)" ++ error: "Failed to load resource " + uri + } + }); + } @@ -26,14 +26,14 @@ index 9649d27..66ef95c 100644 var _ariaLabel = props['aria-label'], accessibilityLabel = props.accessibilityLabel, blurRadius = props.blurRadius, -@@ -238,16 +253,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -240,16 +255,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { } }, function error() { updateState(ERRORED); - if (onError) { - onError({ - nativeEvent: { -- error: "Failed to load resource " + uri + " (404)" +- error: "Failed to load resource " + uri - } - }); - } @@ -47,7 +47,7 @@ index 9649d27..66ef95c 100644 }); } function abortPendingRequest() { -@@ -279,10 +288,79 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { +@@ -281,10 +290,79 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { suppressHydrationWarning: true }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); }); diff --git a/patches/react-native-web+0.19.12+004+fixPointerEventDown.patch b/patches/react-native-web+0.19.13+004+fixPointerEventDown.patch similarity index 100% rename from patches/react-native-web+0.19.12+004+fixPointerEventDown.patch rename to patches/react-native-web+0.19.13+004+fixPointerEventDown.patch diff --git a/patches/react-native-web+0.19.12+005+osr-improvement.patch b/patches/react-native-web+0.19.13+005+osr-improvement.patch similarity index 94% rename from patches/react-native-web+0.19.12+005+osr-improvement.patch rename to patches/react-native-web+0.19.13+005+osr-improvement.patch index b1afa699e7a2..d0a952172768 100644 --- a/patches/react-native-web+0.19.12+005+osr-improvement.patch +++ b/patches/react-native-web+0.19.13+005+osr-improvement.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index bede95b..2aef4c6 100644 +index 5689220..df40877 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -332,7 +332,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -334,7 +334,7 @@ class VirtualizedList extends StateSafePureComponent { zoomScale: 1 }; this._scrollRef = null; @@ -11,7 +11,7 @@ index bede95b..2aef4c6 100644 this._sentEndForContentLength = 0; this._totalCellLength = 0; this._totalCellsMeasured = 0; -@@ -684,16 +684,18 @@ class VirtualizedList extends StateSafePureComponent { +@@ -686,16 +686,18 @@ class VirtualizedList extends StateSafePureComponent { }); } } @@ -32,7 +32,7 @@ index bede95b..2aef4c6 100644 }; // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -919,13 +921,13 @@ class VirtualizedList extends StateSafePureComponent { +@@ -921,13 +923,13 @@ class VirtualizedList extends StateSafePureComponent { // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make // sure we're rendering a reasonable range here. var itemCount = newProps.getItemCount(newProps.data); @@ -48,7 +48,7 @@ index bede95b..2aef4c6 100644 if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { // Fast path if items were added at the start of the list. -@@ -944,7 +946,8 @@ class VirtualizedList extends StateSafePureComponent { +@@ -946,7 +948,8 @@ class VirtualizedList extends StateSafePureComponent { cellsAroundViewport: constrainedCells, renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), firstVisibleItemKey: newFirstVisibleItemKey, @@ -58,7 +58,7 @@ index bede95b..2aef4c6 100644 }; } _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -1220,7 +1223,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1222,7 +1225,7 @@ class VirtualizedList extends StateSafePureComponent { return ret; } } @@ -67,7 +67,7 @@ index bede95b..2aef4c6 100644 var _this$props7 = this.props, data = _this$props7.data, extraData = _this$props7.extraData; -@@ -1244,6 +1247,11 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1246,6 +1249,11 @@ class VirtualizedList extends StateSafePureComponent { if (hiPriInProgress) { this._hiPriInProgress = false; } @@ -79,7 +79,7 @@ index bede95b..2aef4c6 100644 } // Used for preventing scrollToIndex from being called multiple times for initialScrollIndex -@@ -1407,8 +1415,8 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1409,8 +1417,8 @@ class VirtualizedList extends StateSafePureComponent { // Next check if the user just scrolled within the start threshold // and call onStartReached only once for a given content length, // and only if onEndReached is not being executed @@ -90,7 +90,7 @@ index bede95b..2aef4c6 100644 onStartReached({ distanceFromStart }); -@@ -1417,7 +1425,7 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1419,7 +1427,7 @@ class VirtualizedList extends StateSafePureComponent { // If the user scrolls away from the start or end and back again, // cause onStartReached or onEndReached to be triggered again else { @@ -100,7 +100,7 @@ index bede95b..2aef4c6 100644 } } diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index 459f017..d20115c 100644 +index 459f017..fb2d269 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js @@ -79,6 +79,7 @@ type State = { diff --git a/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch b/patches/react-native-web+0.19.13+006+remove-focus-trap-from-modal.patch similarity index 88% rename from patches/react-native-web+0.19.12+006+remove focus trap from modal.patch rename to patches/react-native-web+0.19.13+006+remove-focus-trap-from-modal.patch index 14dbc88b0b1c..eac73db57e35 100644 --- a/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch +++ b/patches/react-native-web+0.19.13+006+remove-focus-trap-from-modal.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-web/dist/exports/Modal/index.js b/node_modules/react-native-web/dist/exports/Modal/index.js -index d5df021..e2c46cf 100644 +index a9a7c36..522ef93 100644 --- a/node_modules/react-native-web/dist/exports/Modal/index.js +++ b/node_modules/react-native-web/dist/exports/Modal/index.js -@@ -86,13 +86,11 @@ var Modal = /*#__PURE__*/React.forwardRef((props, forwardedRef) => { +@@ -88,13 +88,11 @@ var Modal = /*#__PURE__*/React.forwardRef((props, forwardedRef) => { onDismiss: onDismissCallback, onShow: onShowCallback, visible: visible diff --git a/patches/react-native-web+0.19.12+007+fix-scrollable-overflown-text.patch b/patches/react-native-web+0.19.13+007+fix-scrollable-overflown-text.patch similarity index 84% rename from patches/react-native-web+0.19.12+007+fix-scrollable-overflown-text.patch rename to patches/react-native-web+0.19.13+007+fix-scrollable-overflown-text.patch index 11b85afcf86c..304a57ad0657 100644 --- a/patches/react-native-web+0.19.12+007+fix-scrollable-overflown-text.patch +++ b/patches/react-native-web+0.19.13+007+fix-scrollable-overflown-text.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-native-web/dist/exports/Text/index.js b/node_modules/react-native-web/dist/exports/Text/index.js -index 8c5f79b..4a47f80 100644 +index 4130386..1076f55 100644 --- a/node_modules/react-native-web/dist/exports/Text/index.js +++ b/node_modules/react-native-web/dist/exports/Text/index.js -@@ -166,7 +166,7 @@ var styles = StyleSheet.create({ +@@ -176,7 +176,7 @@ var styles = StyleSheet.create({ textMultiLine: { display: '-webkit-box', maxWidth: '100%', @@ -12,10 +12,10 @@ index 8c5f79b..4a47f80 100644 WebkitBoxOrient: 'vertical' }, diff --git a/node_modules/react-native-web/src/exports/Text/index.js b/node_modules/react-native-web/src/exports/Text/index.js -index 071ae10..e43042c 100644 +index f79e82c..f27ccec 100644 --- a/node_modules/react-native-web/src/exports/Text/index.js +++ b/node_modules/react-native-web/src/exports/Text/index.js -@@ -219,7 +219,7 @@ const styles = StyleSheet.create({ +@@ -223,7 +223,7 @@ const styles = StyleSheet.create({ textMultiLine: { display: '-webkit-box', maxWidth: '100%', diff --git a/src/App.tsx b/src/App.tsx index 177cc00c7dee..643e2146e501 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,8 @@ LogBox.ignoreLogs([ // the timer is lost. Currently Expensify is using a 30 minutes interval to refresh personal details. // More details here: https://git.io/JJYeb 'Setting a timer for a long period of time', + // We are not using expo-const, so ignore the warning. + 'No native ExponentConstants module found', ]); const fill = {flex: 1}; diff --git a/src/CONFIG.ts b/src/CONFIG.ts index d82a261c2ec6..8a30c8bf57c2 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -97,9 +97,9 @@ export default { }, GCP_GEOLOCATION_API_KEY: googleGeolocationAPIKey, FIREBASE_WEB_CONFIG: { - apiKey: get(Config, 'FB_API_KEY', 'AIzaSyDxzigVLZl4G8MP7jACQ0qpmADMzmrrON0'), - appId: get(Config, 'FB_APP_ID', '1:921154746561:web:7b8213357d07d6e4027c40'), - projectId: get(Config, 'FB_PROJECT_ID', 'expensify-chat'), + apiKey: get(Config, 'FB_API_KEY', 'AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow'), + appId: get(Config, 'FB_APP_ID', '1:1008697809946:web:ca25268d2645fc285445a3'), + projectId: get(Config, 'FB_PROJECT_ID', 'expensify-mobile-app'), }, // to read more about StrictMode see: contributingGuides/STRICT_MODE.md USE_REACT_STRICT_MODE_IN_DEV: false, diff --git a/src/CONST.ts b/src/CONST.ts index dd048f95d374..e95e3d4a5603 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -20,6 +20,7 @@ const CLOUDFRONT_DOMAIN = 'cloudfront.net'; const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`; const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com'); const USE_EXPENSIFY_URL = 'https://use.expensify.com'; +const EXPENSIFY_URL = 'https://www.expensify.com'; const PLATFORM_OS_MACOS = 'Mac OS'; const PLATFORM_IOS = 'iOS'; const ANDROID_PACKAGE_NAME = 'com.expensify.chat'; @@ -77,12 +78,25 @@ const onboardingChoices = { ...backendOnboardingChoices, } as const; +const combinedTrackSubmitOnboardingChoices = { + PERSONAL_SPEND: selectableOnboardingChoices.PERSONAL_SPEND, + EMPLOYER: selectableOnboardingChoices.EMPLOYER, + SUBMIT: backendOnboardingChoices.SUBMIT, +} as const; + const signupQualifiers = { INDIVIDUAL: 'individual', VSB: 'vsb', SMB: 'smb', } as const; +const selfGuidedTourTask: OnboardingTaskType = { + type: 'viewTour', + autoCompleted: false, + title: 'Take a 2-minute tour', + description: ({navatticURL}) => `[Take a self-guided product tour](${navatticURL}) and learn about everything Expensify has to offer.`, +}; + const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { @@ -93,6 +107,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { height: 960, }, tasks: [ + selfGuidedTourTask, { type: 'submitExpense', autoCompleted: false, @@ -127,6 +142,95 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessageType = { ], }; +const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessageType = { + ...onboardingEmployerOrSubmitMessage, + tasks: [ + { + type: 'submitExpense', + autoCompleted: false, + title: 'Submit an expense', + description: + '*Submit an expense* by entering an amount or scanning a receipt.\n' + + '\n' + + 'Here’s how to submit an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Create expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Add your reimburser to the request.\n' + + '5. Click *Submit*.\n' + + '\n' + + 'And you’re done! Now wait for that sweet “Cha-ching!” when it’s complete.', + }, + { + type: 'addBankAccount', + autoCompleted: false, + title: 'Add personal bank account', + description: + 'You’ll need to add your personal bank account to get paid back. Don’t worry, it’s easy!\n' + + '\n' + + 'Here’s how to set up your bank account:\n' + + '\n' + + '1. Click your profile picture.\n' + + '2. Click *Wallet* > *Bank accounts* > *+ Add bank account*.\n' + + '3. Connect your bank account.\n' + + '\n' + + 'Once that’s done, you can request money from anyone and get paid back right into your personal bank account.', + }, + ], +}; + +const onboardingPersonalSpendMessage: OnboardingMessageType = { + message: 'Here’s how to track your spend in a few clicks.', + video: { + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, + thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, + duration: 55, + width: 1280, + height: 960, + }, + tasks: [ + { + type: 'trackExpense', + autoCompleted: false, + title: 'Track an expense', + description: + '*Track an expense* in any currency, whether you have a receipt or not.\n' + + '\n' + + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green *+* button.\n' + + '2. Choose *Track expense*.\n' + + '3. Enter an amount or scan a receipt.\n' + + '4. Click *Track*.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], +}; +const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessageType = { + ...onboardingPersonalSpendMessage, + tasks: [ + { + type: 'trackExpense', + autoCompleted: false, + title: 'Track an expense', + description: + '*Track an expense* in any currency, whether you have a receipt or not.\n' + + '\n' + + 'Here’s how to track an expense:\n' + + '\n' + + '1. Click the green *+* 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' + + '5. Click *Track*.\n' + + '\n' + + 'And you’re done! Yep, it’s that easy.', + }, + ], +}; + type OnboardingPurposeType = ValueOf; type OnboardingCompanySizeType = ValueOf; @@ -152,8 +256,26 @@ type OnboardingInviteType = ValueOf; type OnboardingTaskType = { type: string; autoCompleted: boolean; - title: string; - description: string | ((params: Partial<{adminsRoomLink: string; workspaceCategoriesLink: string; workspaceMoreFeaturesLink: string; workspaceMembersLink: string}>) => string); + title: + | string + | (( + params: Partial<{ + integrationName: string; + }>, + ) => string); + description: + | string + | (( + params: Partial<{ + adminsRoomLink: string; + workspaceCategoriesLink: string; + workspaceMoreFeaturesLink: string; + workspaceMembersLink: string; + integrationName: string; + workspaceAccountingLink: string; + navatticURL: string; + }>, + ) => string); }; type OnboardingMessageType = { @@ -353,6 +475,7 @@ const CONST = { OLD_DOT_ANDROID: 'https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&pli=1', OLD_DOT_IOS: 'https://apps.apple.com/us/app/expensify-expense-tracker/id471713959', }, + COMPANY_WEBSITE_DEFAULT_SCHEME: 'http', DATE: { SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss', FNS_FORMAT_STRING: 'yyyy-MM-dd', @@ -475,6 +598,9 @@ const CONST = { }, }, NON_USD_BANK_ACCOUNT: { + ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'], + FILE_LIMIT: 10, + TOTAL_FILES_SIZE_LIMIT: 5242880, STEP: { COUNTRY: 'CountryStep', BANK_INFO: 'BankInfoStep', @@ -505,11 +631,9 @@ const CONST = { COMPANY_CARD_FEEDS: 'companyCardFeeds', DIRECT_FEEDS: 'directFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', - NEW_DOT_COPILOT: 'newDotCopilot', - WORKSPACE_RULES: 'workspaceRules', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', - NEW_DOT_QBD: 'quickbooksDesktopOnNewDot', + PER_DIEM: 'newDotPerDiem', }, BUTTON_STATES: { DEFAULT: 'default', @@ -542,6 +666,7 @@ const CONST = { ANDROID: 'android', WEB: 'web', DESKTOP: 'desktop', + MOBILEWEB: 'mobileweb', }, PLATFORM_SPECIFIC_KEYS: { CTRL: { @@ -708,6 +833,7 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, + EXPENSIFY_URL, GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', IMAGE_BASE64_MATCH: 'base64', @@ -720,13 +846,14 @@ const CONST = { UPWORK_URL: 'https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22', DEEP_DIVE_EXPENSIFY_CARD: 'https://community.expensify.com/discussion/4848/deep-dive-expensify-card-and-quickbooks-online-auto-reconciliation-how-it-works', DEEP_DIVE_ERECEIPTS: 'https://community.expensify.com/discussion/5542/deep-dive-what-are-ereceipts/', + DEEP_DIVE_PER_DIEM: 'https://community.expensify.com/discussion/4772/how-to-add-a-single-rate-per-diem', GITHUB_URL: 'https://github.com/Expensify/App', - TERMS_URL: `${USE_EXPENSIFY_URL}/terms`, - PRIVACY_URL: `${USE_EXPENSIFY_URL}/privacy`, + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, - ACH_TERMS_URL: `${USE_EXPENSIFY_URL}/achterms`, - WALLET_AGREEMENT_URL: `${USE_EXPENSIFY_URL}/walletagreement`, - BANCORP_WALLET_AGREEMENT_URL: `${USE_EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, + ACH_TERMS_URL: `${EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/expensify-payments-wallet-terms-of-service`, + BANCORP_WALLET_AGREEMENT_URL: `${EXPENSIFY_URL}/bancorp-bank-wallet-terms-of-service`, HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', @@ -739,7 +866,6 @@ const CONST = { NEWHELP_URL: 'https://help.expensify.com', INTERNAL_DEV_EXPENSIFY_URL: 'https://www.expensify.com.dev', STAGING_EXPENSIFY_URL: 'https://staging.expensify.com', - EXPENSIFY_URL: 'https://www.expensify.com', BANK_ACCOUNT_PERSONAL_DOCUMENTATION_INFO_URL: 'https://community.expensify.com/discussion/6983/faq-why-do-i-need-to-provide-personal-documentation-when-setting-up-updating-my-bank-account', PERSONAL_DATA_PROTECTION_INFO_URL: 'https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information', @@ -747,7 +873,7 @@ const CONST = { ONFIDO_PRIVACY_POLICY_URL: 'https://onfido.com/privacy/', ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', LIST_OF_RESTRICTED_BUSINESSES: 'https://community.expensify.com/discussion/6191/list-of-restricted-businesses', - TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, + TRAVEL_TERMS_URL: `${EXPENSIFY_URL}/travelterms`, EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT: 'https://www.expensify.com/tools/integrations/downloadPackage', EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct', SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct', @@ -764,8 +890,10 @@ const CONST = { // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', NAVATTIC: { - ADMIN_TOUR: 'https://expensify.navattic.com/kh204a7', - EMPLOYEE_TOUR: 'https://expensify.navattic.com/35609gb', + ADMIN_TOUR_PRODUCTION: 'https://expensify.navattic.com/kh204a7', + ADMIN_TOUR_STAGING: 'https://expensify.navattic.com/3i300k18', + EMPLOYEE_TOUR_PRODUCTION: 'https://expensify.navattic.com/35609gb', + EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', }, OLDDOT_URLS: { @@ -1099,6 +1227,7 @@ const CONST = { MODAL_TYPE: { CONFIRM: 'confirm', CENTERED: 'centered', + CENTERED_SWIPABLE_TO_RIGHT: 'centered_swipable_to_right', CENTERED_UNSWIPEABLE: 'centered_unswipeable', CENTERED_SMALL: 'centered_small', BOTTOM_DOCKED: 'bottom_docked', @@ -1146,11 +1275,9 @@ const CONST = { SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, - SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values', - SEARCH_MAKE_TREE: 'search_make_tree', - SEARCH_BUILD_TREE: 'search_build_tree', SEARCH_FILTER_OPTIONS: 'search_filter_options', USE_DEBOUNCED_STATE_DELAY: 300, + LIST_SCROLLING_DEBOUNCE_TIME: 200, }, PRIORITY_MODE: { GSD: 'gsd', @@ -1182,7 +1309,13 @@ const CONST = { PENDING: 'Pending', POSTED: 'Posted', }, + STATE: { + CURRENT: 'current', + DRAFT: 'draft', + BACKUP: 'backup', + }, }, + MCC_GROUPS: { AIRLINES: 'Airlines', COMMUTER: 'Commuter', @@ -1480,6 +1613,7 @@ const CONST = { CONTRIBUTORS: 'contributors@expensify.com', FIRST_RESPONDER: 'firstresponders@expensify.com', GUIDES_DOMAIN: 'team.expensify.com', + QA_DOMAIN: 'applause.expensifail.com', HELP: 'help@expensify.com', INTEGRATION_TESTING_CREDS: 'integrationtestingcreds@expensify.com', NOTIFICATIONS: 'notifications@expensify.com', @@ -2334,6 +2468,7 @@ const CONST = { ARE_INVOICES_ENABLED: 'areInvoicesEnabled', ARE_TAXES_ENABLED: 'tax', ARE_RULES_ENABLED: 'areRulesEnabled', + ARE_PER_DIEM_RATES_ENABLED: 'arePerDiemRatesEnabled', }, DEFAULT_CATEGORIES: [ 'Advertising', @@ -2500,6 +2635,7 @@ const CONST = { CUSTOM_UNITS: { NAME_DISTANCE: 'Distance', + NAME_PER_DIEM_INTERNATIONAL: 'Per Diem International', DISTANCE_UNIT_MILES: 'mi', DISTANCE_UNIT_KILOMETERS: 'km', MILEAGE_IRS_RATE: 0.67, @@ -2564,6 +2700,7 @@ const CONST = { STEP: { ASSIGNEE: 'Assignee', CARD: 'Card', + CARD_NAME: 'CardName', TRANSACTION_START_DATE: 'TransactionStartDate', CONFIRMATION: 'Confirmation', }, @@ -2580,6 +2717,11 @@ const CONST = { INDIVIDUAL: 'individual', NONE: 'none', }, + VERIFICATION_STATE: { + LOADING: 'loading', + VERIFIED: 'verified', + ON_WAITLIST: 'onWaitlist', + }, STATE: { STATE_NOT_ISSUED: 2, OPEN: 3, @@ -2594,6 +2736,7 @@ const CONST = { MONTHLY: 'monthly', FIXED: 'fixed', }, + LIMIT_VALUE: 21474836, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', @@ -2611,7 +2754,6 @@ const CONST = { DAILY: 'daily', MONTHLY: 'monthly', }, - CARD_TITLE_INPUT_LIMIT: 255, MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards', }, COMPANY_CARDS: { @@ -2664,6 +2806,7 @@ const CONST = { RESTRICT: 'corporate', ALLOW: 'personal', }, + CARD_LIST_THRESHOLD: 8, EXPORT_CARD_TYPES: { /** * Name of Card NVP for QBO custom export accounts @@ -2841,6 +2984,7 @@ const CONST = { SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*(?![^\`]*\`)`, 'gim'), REPORT_ID_FROM_PATH: /\/r\/(\d+)/, DISTANCE_MERCHANT: /^[0-9.]+ \w+ @ (-|-\()?[^0-9.\s]{1,3} ?[0-9.]+\)? \/ \w+$/, + WHITESPACE: /\s+/g, get EXPENSIFY_POLICY_DOMAIN_NAME() { return new RegExp(`${EXPENSIFY_POLICY_DOMAIN}([a-zA-Z0-9]+)\\${EXPENSIFY_POLICY_DOMAIN_EXTENSION}`); @@ -2914,10 +3058,6 @@ const CONST = { get RESTRICTED_ACCOUNT_IDS() { return [this.ACCOUNT_ID.NOTIFICATIONS]; }, - // Account IDs that can't be added as a group member - get NON_ADDABLE_ACCOUNT_IDS() { - return [this.ACCOUNT_ID.NOTIFICATIONS, this.ACCOUNT_ID.CHRONOS]; - }, // Auth limit is 60k for the column but we store edits and other metadata along the html so let's use a lower limit to accommodate for it. MAX_COMMENT_LENGTH: 10000, @@ -2929,6 +3069,8 @@ const CONST = { // Character Limits FORM_CHARACTER_LIMIT: 50, + STANDARD_LENGTH_LIMIT: 100, + STANDARD_LIST_ITEM_LIMIT: 8, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, @@ -3002,8 +3144,8 @@ const CONST = { EXPENSIFY_APPROVED_URL: `${USE_EXPENSIFY_URL}/accountants`, PRESS_KIT_URL: 'https://we.are.expensify.com/press-kit', SUPPORT_URL: `${USE_EXPENSIFY_URL}/support`, - COMMUNITY_URL: 'https://community.expensify.com/', - PRIVACY_URL: `${USE_EXPENSIFY_URL}/privacy`, + TERMS_URL: `${EXPENSIFY_URL}/terms`, + PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, ABOUT_URL: 'https://we.are.expensify.com/how-we-got-here', BLOG_URL: 'https://blog.expensify.com/', JOBS_URL: 'https://we.are.expensify.com/apply', @@ -3325,6 +3467,63 @@ const CONST = { ZW: 'Zimbabwe', }, + ALL_EUROPEAN_COUNTRIES: { + AL: 'Albania', + AD: 'Andorra', + AT: 'Austria', + BY: 'Belarus', + BE: 'Belgium', + BA: 'Bosnia & Herzegovina', + BG: 'Bulgaria', + HR: 'Croatia', + CY: 'Cyprus', + CZ: 'Czech Republic', + DK: 'Denmark', + EE: 'Estonia', + FO: 'Faroe Islands', + FI: 'Finland', + FR: 'France', + GE: 'Georgia', + DE: 'Germany', + GI: 'Gibraltar', + GR: 'Greece', + GL: 'Greenland', + HU: 'Hungary', + IS: 'Iceland', + IE: 'Ireland', + IM: 'Isle of Man', + IT: 'Italy', + JE: 'Jersey', + XK: 'Kosovo', + LV: 'Latvia', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MT: 'Malta', + MD: 'Moldova', + MC: 'Monaco', + ME: 'Montenegro', + NL: 'Netherlands', + MK: 'North Macedonia', + NO: 'Norway', + PL: 'Poland', + PT: 'Portugal', + RO: 'Romania', + RU: 'Russia', + SM: 'San Marino', + RS: 'Serbia', + SK: 'Slovakia', + SI: 'Slovenia', + ES: 'Spain', + SJ: 'Svalbard & Jan Mayen', + SE: 'Sweden', + CH: 'Switzerland', + TR: 'Turkey', + UA: 'Ukraine', + GB: 'United Kingdom', + VA: 'Vatican City', + }, + // Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427 // https://github.com/Expensify/App/issues/14958#issuecomment-1456026810 COUNTRY_ZIP_REGEX_DATA: { @@ -4186,6 +4385,7 @@ const CONST = { // The attribute used in the SelectionScraper.js helper to query all the DOM elements // that should be removed from the copied contents in the getHTMLOfSelection() method SELECTION_SCRAPER_HIDDEN_ELEMENT: 'selection-scrapper-hidden-element', + INNER_BOX_SHADOW_ELEMENT: 'inner-box-shadow-element', MODERATION: { MODERATOR_DECISION_PENDING: 'pending', MODERATOR_DECISION_PENDING_HIDE: 'pendingHide', @@ -4619,6 +4819,7 @@ const CONST = { ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices}, + COMBINED_TRACK_SUBMIT_ONBOARDING_CHOICES: {...combinedTrackSubmitOnboardingChoices}, ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers}, ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize}, @@ -4662,7 +4863,13 @@ const CONST = { '\n' + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", }, - + ONBOARDING_ACCOUNTING_MAPPING: { + quickbooksOnline: 'QuickBooks Online', + xero: 'Xero', + netsuite: 'NetSuite', + intacct: 'Sage Intacct', + quickbooksDesktop: 'QuickBooks Desktop', + }, ONBOARDING_MESSAGES: { [onboardingChoices.EMPLOYER]: onboardingEmployerOrSubmitMessage, [onboardingChoices.SUBMIT]: onboardingEmployerOrSubmitMessage, @@ -4690,6 +4897,7 @@ const CONST = { '\n' + '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', }, + selfGuidedTourTask, { type: 'meetGuide', autoCompleted: false, @@ -4774,36 +4982,30 @@ const CONST = { '\n' + `[Take me to workspace members](${workspaceMembersLink}). That’s it, happy expensing! :)`, }, - ], - }, - [onboardingChoices.PERSONAL_SPEND]: { - message: 'Here’s how to track your spend in a few clicks.', - video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, - duration: 55, - width: 1280, - height: 960, - }, - tasks: [ { - type: 'trackExpense', + type: 'addAccountingIntegration', autoCompleted: false, - title: 'Track an expense', - description: - '*Track an expense* in any currency, whether you have a receipt or not.\n' + + title: ({integrationName}) => `Connect to ${integrationName}`, + description: ({integrationName, workspaceAccountingLink}) => + `Connect to ${integrationName} for automatic expense coding and syncing that makes month-end close a breeze.\n` + '\n' + - 'Here’s how to track an expense:\n' + + `Here’s how to connect to ${integrationName}:\n` + '\n' + - '1. Click the green *+* button.\n' + - '2. Choose *Track expense*.\n' + - '3. Enter an amount or scan a receipt.\n' + - '4. Click *Track*.\n' + + '1. Click your profile photo.\n' + + '2. Go to Workspaces.\n' + + '3. Select your workspace.\n' + + '4. Click Accounting.\n' + + `5. Find ${integrationName}.\n` + + '6. Click Connect.\n' + '\n' + - 'And you’re done! Yep, it’s that easy.', + `[Take me to Accounting!](${workspaceAccountingLink})`, }, ], }, + [onboardingChoices.PERSONAL_SPEND]: { + ...onboardingPersonalSpendMessage, + tasks: [selfGuidedTourTask, ...onboardingPersonalSpendMessage.tasks], + }, [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { @@ -4814,6 +5016,7 @@ const CONST = { height: 960, }, tasks: [ + selfGuidedTourTask, { type: 'startChat', autoCompleted: false, @@ -4872,6 +5075,12 @@ const CONST = { }, } satisfies Record, + COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES: { + [combinedTrackSubmitOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage, + [combinedTrackSubmitOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + [combinedTrackSubmitOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + } satisfies Record, OnboardingMessageType>, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, @@ -5631,6 +5840,7 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + MIN_TAX_RATE_DECIMAL_PLACES: 2, DOWNLOADS_PATH: '/Downloads', DOWNLOADS_TIMEOUT: 5000, @@ -5746,6 +5956,11 @@ const CONST = { IN: 'in', }, EMPTY_VALUE: 'none', + SEARCH_ROUTER_ITEM_TYPE: { + CONTEXTUAL_SUGGESTION: 'contextualSuggestion', + AUTOCOMPLETE_SUGGESTION: 'autocompleteSuggestion', + SEARCH: 'searchItem', + }, }, REFERRER: { @@ -5886,6 +6101,14 @@ const CONST = { description: 'workspace.upgrade.rules.description' as const, icon: 'Rules', }, + perDiem: { + id: 'perDiem' as const, + alias: 'per-diem', + name: 'Per diem', + title: 'workspace.upgrade.perDiem.title' as const, + description: 'workspace.upgrade.perDiem.description' as const, + icon: 'PerDiem', + }, }; }, REPORT_FIELD_TYPES: { @@ -5960,6 +6183,7 @@ const CONST = { HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors', HAS_LOGIN_LIST_INFO: 'hasLoginListInfo', HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo', + HAS_PHONE_NUMBER_ERROR: 'hasPhoneNumberError', }, DEBUG: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 427e05052ae3..c5ec21b8b1c2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,6 +1,7 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; +import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; @@ -122,6 +123,9 @@ const ONYXKEYS = { /** This NVP contains data associated with HybridApp */ NVP_TRYNEWDOT: 'nvp_tryNewDot', + /** Contains the platforms for which the user muted the sounds */ + NVP_MUTED_PLATFORMS: 'nvp_mutedPlatforms', + /** Contains the user preference for the LHN priority mode */ NVP_PRIORITY_MODE: 'nvp_priorityMode', @@ -445,9 +449,15 @@ const ONYXKEYS = { /** Stores recently used currencies */ RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies', + /** States whether we transitioned from OldDot to show only certain group of screens. It should be undefined on pure NewDot. */ + IS_SINGLE_NEW_DOT_ENTRY: 'isSingleNewDotEntry', + /** Company cards custom names */ NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames', + /** The user's Concierge reportID */ + CONCIERGE_REPORT_ID: 'conciergeReportID', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -527,6 +537,9 @@ const ONYXKEYS = { /** Currently displaying feed */ LAST_SELECTED_FEED: 'lastSelectedFeed_', + + /** Whether the bank account chosen for Expensify Card in on verification waitlist */ + NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_', }, /** List of Form ids */ @@ -857,6 +870,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName; [ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean; [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed; + [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist; }; type OnyxValuesMapping = { @@ -903,6 +917,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; + [ONYXKEYS.NVP_MUTED_PLATFORMS]: Partial>; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; @@ -1009,9 +1024,11 @@ type OnyxValuesMapping = { [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.LAST_ROUTE]: string; + [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; + [ONYXKEYS.CONCIERGE_REPORT_ID]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c346da6cadcb..cd94035e0fff 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -208,6 +208,7 @@ const ROUTES = { }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', + SETTINGS_PHONE_NUMBER: 'settings/profile/phone', SETTINGS_ADDRESS: 'settings/profile/address', SETTINGS_ADDRESS_COUNTRY: { route: 'settings/profile/address/country', @@ -950,6 +951,10 @@ const ROUTES = { getRoute: (policyID: string, featureName: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), }, + WORKSPACE_DOWNGRADE: { + route: 'settings/workspaces/:policyID/downgrade/', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/downgrade/` as const, + }, WORKSPACE_CATEGORIES_SETTINGS: { route: 'settings/workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, @@ -1268,6 +1273,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-rate/edit', getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-rate/edit` as const, }, + WORKSPACE_PER_DIEM: { + route: 'settings/workspaces/:policyID/per-diem', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2e44c5ed5695..9b8fe54111cf 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -84,6 +84,7 @@ const SCREENS = { TIMEZONE_SELECT: 'Settings_Timezone_Select', LEGAL_NAME: 'Settings_LegalName', DATE_OF_BIRTH: 'Settings_DateOfBirth', + PHONE_NUMBER: 'Settings_PhoneNumber', ADDRESS: 'Settings_Address', ADDRESS_COUNTRY: 'Settings_Address_Country', ADDRESS_STATE: 'Settings_Address_State', @@ -531,6 +532,7 @@ const SCREENS = { DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit', DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit', UPGRADE: 'Workspace_Upgrade', + DOWNGRADE: 'Workspace_Downgrade', RULES: 'Policy_Rules', RULES_CUSTOM_NAME: 'Rules_Custom_Name', RULES_AUTO_APPROVE_REPORTS_UNDER: 'Rules_Auto_Approve_Reports_Under', @@ -540,6 +542,7 @@ const SCREENS = { RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount', RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age', RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default', + PER_DIEM: 'Per_Diem', }, EDIT_REQUEST: { diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 8ccab44a2cb9..9a90de17595d 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -5,7 +5,6 @@ import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -34,7 +33,6 @@ function AccountSwitcher() { const theme = useTheme(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {canUseNewDotCopilot} = usePermissions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [session] = useOnyx(ONYXKEYS.SESSION); @@ -47,7 +45,7 @@ function AccountSwitcher() { const delegators = account?.delegatedAccess?.delegators ?? []; const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; - const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate); + const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, @@ -87,7 +85,7 @@ function AccountSwitcher() { } const delegatePersonalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail); - const error = ErrorUtils.getLatestErrorField(account?.delegatedAccess, 'connect'); + const error = ErrorUtils.getLatestError(account?.delegatedAccess?.errorFields?.disconnect); return [ createBaseMenuItem(delegatePersonalDetails, error, { @@ -105,8 +103,9 @@ function AccountSwitcher() { const delegatorMenuItems: PopoverMenuItem[] = delegators .filter(({email}) => email !== currentUserPersonalDetails.login) - .map(({email, role, errorFields}) => { - const error = ErrorUtils.getLatestErrorField({errorFields}, 'connect'); + .map(({email, role}) => { + const errorFields = account?.delegatedAccess?.errorFields ?? {}; + const error = ErrorUtils.getLatestError(errorFields?.connect?.[email]); const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email); return createBaseMenuItem(personalDetails, error, { badgeText: translate('delegate.role', {role}), @@ -152,7 +151,7 @@ function AccountSwitcher() { > {currentUserPersonalDetails?.displayName} - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( - {canSwitchAccounts && ( + {!!canSwitchAccounts && ( { diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx index f38ea60f1aad..5aaa23b238f7 100644 --- a/src/components/AddPaymentCard/PaymentCardForm.tsx +++ b/src/components/AddPaymentCard/PaymentCardForm.tsx @@ -177,18 +177,26 @@ function PaymentCardForm({ }; const onChangeCardNumber = useCallback((newValue: string) => { - // replace all characters that are not spaces or digits + // Replace all characters that are not spaces or digits let validCardNumber = newValue.replace(/[^\d ]/g, ''); - // gets only the first 16 digits if the inputted number have more digits than that + // Gets only the first 16 digits if the inputted number have more digits than that validCardNumber = validCardNumber.match(/(?:\d *){1,16}/)?.[0] ?? ''; - // add the spacing between every 4 digits - validCardNumber = - validCardNumber - .replace(/ /g, '') - .match(/.{1,4}/g) - ?.join(' ') ?? ''; + // Remove all spaces to simplify formatting + const cleanedNumber = validCardNumber.replace(/ /g, ''); + + // Check if the number is a potential Amex card (starts with 34 or 37 and has up to 15 digits) + const isAmex = /^3[47]\d{0,13}$/.test(cleanedNumber); + + // Format based on Amex or standard 4-4-4-4 pattern + if (isAmex) { + // Format as 4-6-5 for Amex + validCardNumber = cleanedNumber.replace(/(\d{1,4})(\d{1,6})?(\d{1,5})?/, (match, p1, p2, p3) => [p1, p2, p3].filter(Boolean).join(' ')); + } else { + // Format as 4-4-4-4 for non-Amex + validCardNumber = cleanedNumber.match(/.{1,4}/g)?.join(' ') ?? ''; + } setCardNumber(validCardNumber); }, []); diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 9c72b371c40f..f4067d357c9d 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -63,6 +63,7 @@ function AddressSearch( onBlur, onInputChange, onPress, + onCountryChange, predefinedPlaces = [], preferredLocale, renamedInputKeys = { @@ -195,7 +196,7 @@ function AddressSearch( // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. - if (country !== CONST.COUNTRY.US) { + if (country !== CONST.COUNTRY.US && country !== CONST.COUNTRY.CA) { values.state = longStateName; } @@ -244,6 +245,7 @@ function AddressSearch( onInputChange?.(values); } + onCountryChange?.(values.country); onPress?.(values); }; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index b654fcad99da..daa28c3d69af 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -87,6 +87,9 @@ type AddressSearchProps = { /** The user's preferred locale e.g. 'en', 'es-ES' */ preferredLocale?: Locale; + + /** Callback to be called when the country is changed */ + onCountryChange?: (country: unknown) => void; }; type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 4848577bdea0..a230dfa1af8d 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {NativeSyntheticEvent} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +16,7 @@ import TextInput from './TextInput'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; -import type TextInputWithCurrencySymbolProps from './TextInputWithCurrencySymbol/types'; +import type BaseTextInputWithCurrencySymbolProps from './TextInputWithCurrencySymbol/types'; type AmountFormProps = { /** Amount supplied by the FormProvider */ @@ -51,7 +51,7 @@ type AmountFormProps = { /** Number of decimals to display */ fixedDecimals?: number; -} & Pick & +} & Pick & Pick; /** @@ -290,11 +290,11 @@ function AmountForm( }} selectedCurrencyCode={currency} selection={selection} - onSelectionChange={(e: NativeSyntheticEvent) => { + onSelectionChange={(start, end) => { if (!shouldUpdateSelection) { return; } - setSelection(e.nativeEvent.selection); + setSelection({start, end}); }} onKeyPress={textInputKeyPress} isCurrencyPressable={isCurrencyPressable} diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index d3a51c7fc0f0..70966a05b918 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -16,6 +16,8 @@ import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import CONST from '@src/CONST'; @@ -33,46 +35,46 @@ type Item = { pickAttachment: () => Promise; }; -/** - * See https://github.com/react-native-image-picker/react-native-image-picker/#options - * for ImagePicker configuration options - */ -const imagePickerOptions: Partial = { - includeBase64: false, - saveToPhotos: false, - selectionLimit: 1, - includeExtra: false, - assetRepresentationMode: 'current', -}; - /** * Return imagePickerOptions based on the type */ -const getImagePickerOptions = (type: string): CameraOptions => { +const getImagePickerOptions = (type: string, fileLimit: number): CameraOptions | ImageLibraryOptions => { // mediaType property is one of the ImagePicker configuration to restrict types' const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed'; + + /** + * See https://github.com/react-native-image-picker/react-native-image-picker/#options + * for ImagePicker configuration options + */ return { mediaType, - ...imagePickerOptions, + includeBase64: false, + saveToPhotos: false, + includeExtra: false, + assetRepresentationMode: 'current', + selectionLimit: fileLimit, }; }; /** * Return documentPickerOptions based on the type * @param {String} type + * @param {Number} fileLimit * @returns {Object} */ -const getDocumentPickerOptions = (type: string): DocumentPickerOptions => { +const getDocumentPickerOptions = (type: string, fileLimit: number): DocumentPickerOptions => { if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { return { type: [RNDocumentPicker.types.images], copyTo: 'cachesDirectory', + allowMultiSelection: fileLimit !== 1, }; } return { type: [RNDocumentPicker.types.allFiles], copyTo: 'cachesDirectory', + allowMultiSelection: fileLimit !== 1, }; }; @@ -111,13 +113,16 @@ function AttachmentPicker({ type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, - shouldHideGalleryOption = false, shouldValidateImage = true, + shouldHideGalleryOption = false, + fileLimit = 1, }: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); - const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {}); + const completeAttachmentSelection = useRef<(data: FileObject[]) => void>(() => {}); const onModalHide = useRef<() => void>(); const onCanceled = useRef<() => void>(() => {}); const popoverRef = useRef(null); @@ -143,7 +148,7 @@ function AttachmentPicker({ const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -200,7 +205,7 @@ function AttachmentPicker({ } }); }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); /** * Launch the DocumentPicker. Results are in the same format as ImagePicker @@ -209,7 +214,7 @@ function AttachmentPicker({ */ const showDocumentPicker = useCallback( (): Promise => - RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => { + RNDocumentPicker.pick(getDocumentPickerOptions(type, fileLimit)).catch((error: Error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -217,7 +222,7 @@ function AttachmentPicker({ showGeneralAlert(error.message); throw error; }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); const menuItemData: Item[] = useMemo(() => { @@ -261,7 +266,7 @@ function AttachmentPicker({ * @param onPickedHandler A callback that will be called with the selected attachment * @param onCanceledHandler A callback that will be called without a selected attachment */ - const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => { + const open = (onPickedHandler: (files: FileObject[]) => void, onCanceledHandler: () => void = () => {}) => { // eslint-disable-next-line react-compiler/react-compiler completeAttachmentSelection.current = onPickedHandler; onCanceled.current = onCanceledHandler; @@ -286,7 +291,7 @@ function AttachmentPicker({ } return getDataForUpload(fileData) .then((result) => { - completeAttachmentSelection.current(result); + completeAttachmentSelection.current([result]); }) .catch((error: Error) => { showGeneralAlert(error.message); @@ -301,63 +306,78 @@ function AttachmentPicker({ * sends the selected attachment to the caller (parent component) */ const pickAttachment = useCallback( - (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => { + (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => { if (!attachments || attachments.length === 0) { onCanceled.current(); - return Promise.resolve(); + return Promise.resolve([]); } - const fileData = attachments[0]; - if (!fileData) { - onCanceled.current(); - return Promise.resolve(); - } - /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ - const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || ''; - const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || ''; - - const fileDataObject: FileResponse = { - name: fileDataName ?? '', - uri: fileDataUri, - size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null, - type: fileData.type ?? '', - width: ('width' in fileData && fileData.width) || undefined, - height: ('height' in fileData && fileData.height) || undefined, - }; + const filesToProcess = attachments.map((fileData) => { + if (!fileData) { + onCanceled.current(); + return Promise.resolve(); + } - if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) { - ImageSize.getSize(fileDataUri) - .then(({width, height}) => { - fileDataObject.width = width; - fileDataObject.height = height; - return fileDataObject; - }) - .then((file) => { - getDataForUpload(file) - .then((result) => { - completeAttachmentSelection.current(result); - }) - .catch((error: Error) => { - showGeneralAlert(error.message); - throw error; - }); - }); - return; - } - /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ - if (fileDataName && Str.isImage(fileDataName)) { - ImageSize.getSize(fileDataUri) - .then(({width, height}) => { - fileDataObject.width = width; - fileDataObject.height = height; - validateAndCompleteAttachmentSelection(fileDataObject); - }) - .catch(() => showImageCorruptionAlert()); - } else { + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || ''; + const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || ''; + + const fileDataObject: FileResponse = { + name: fileDataName ?? '', + uri: fileDataUri, + size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null, + type: fileData.type ?? '', + width: ('width' in fileData && fileData.width) || undefined, + height: ('height' in fileData && fileData.height) || undefined, + }; + + if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) { + return ImageSize.getSize(fileDataUri) + .then(({width, height}) => { + fileDataObject.width = width; + fileDataObject.height = height; + return fileDataObject; + }) + .then((file) => { + return getDataForUpload(file) + .then((result) => completeAttachmentSelection.current([result])) + .catch((error) => { + if (error instanceof Error) { + showGeneralAlert(error.message); + } else { + showGeneralAlert('An unknown error occurred'); + } + throw error; + }); + }) + .catch(() => { + showImageCorruptionAlert(); + }); + } + + if (fileDataName && Str.isImage(fileDataName)) { + return ImageSize.getSize(fileDataUri) + .then(({width, height}) => { + fileDataObject.width = width; + fileDataObject.height = height; + + if (fileDataObject.width <= 0 || fileDataObject.height <= 0) { + showImageCorruptionAlert(); + return Promise.resolve(); // Skip processing this corrupted file + } + + return validateAndCompleteAttachmentSelection(fileDataObject); + }) + .catch(() => { + showImageCorruptionAlert(); + }); + } return validateAndCompleteAttachmentSelection(fileDataObject); - } + }); + + return Promise.all(filesToProcess); }, - [validateAndCompleteAttachmentSelection, showImageCorruptionAlert, shouldValidateImage, showGeneralAlert], + [shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert], ); /** @@ -428,6 +448,7 @@ function AttachmentPicker({ title={translate(item.textTranslationKey)} onPress={() => selectItem(item)} focused={focusedIndex === menuIndex} + wrapperStyle={StyleUtils.getItemBackgroundColorStyle(false, focusedIndex === menuIndex, false, theme.activeComponentBG, theme.hoverComponentBG)} /> ))} diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index f3c880fcb835..2484198d3916 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -1,5 +1,6 @@ import React, {useRef} from 'react'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import * as Browser from '@libs/Browser'; import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; @@ -42,9 +43,9 @@ function getAcceptableFileTypesFromAList(fileTypes: Array