diff --git a/.env.example b/.env.example index bed835645756..944da2aa9296 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,6 @@ USE_WEB_PROXY=false USE_WDYR=false CAPTURE_METRICS=false ONYX_METRICS=false -GOOGLE_GEOLOCATION_API_KEY=AIzaSyBqg6bMvQU7cPWDKhhzpYqJrTEnSorpiLI EXPENSIFY_ACCOUNT_ID_ACCOUNTING=-1 EXPENSIFY_ACCOUNT_ID_ADMIN=-1 diff --git a/.env.production b/.env.production index 4b0a98e77557..5e676134d681 100644 --- a/.env.production +++ b/.env.production @@ -7,4 +7,3 @@ PUSHER_APP_KEY=268df511a204fbb60884 USE_WEB_PROXY=false ENVIRONMENT=production SEND_CRASH_REPORTS=true -GOOGLE_GEOLOCATION_API_KEY=AIzaSyBFKujMpzExz0_z2pAGfPUwkmlaUc-uw1Q diff --git a/.env.staging b/.env.staging index 1b3ec15fc172..17d82ac2d136 100644 --- a/.env.staging +++ b/.env.staging @@ -6,5 +6,4 @@ EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 PUSHER_APP_KEY=268df511a204fbb60884 USE_WEB_PROXY=false ENVIRONMENT=staging -SEND_CRASH_REPORTS=true -GOOGLE_GEOLOCATION_API_KEY=AIzaSyD2T1mlByThbUN88O8OPOD8vKuMMwLD4-M \ No newline at end of file +SEND_CRASH_REPORTS=true \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 135252825dcf..ed6f162ad8d4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -235,8 +235,21 @@ module.exports = { ], }, - // Remove once no JS files are left overrides: [ + // Enforces every Onyx type and its properties to have a comment explaining its purpose. + { + files: ['src/types/onyx/**/*.ts'], + rules: { + 'jsdoc/require-jsdoc': [ + 'error', + { + contexts: ['TSInterfaceDeclaration', 'TSTypeAliasDeclaration', 'TSPropertySignature'], + }, + ], + }, + }, + + // Remove once no JS files are left { files: ['*.js', '*.jsx'], rules: { diff --git a/.github/scripts/printPodspec.rb b/.github/scripts/printPodspec.rb new file mode 100755 index 000000000000..80012edbc0aa --- /dev/null +++ b/.github/scripts/printPodspec.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby + +# This file is a lightweight port of the `pod ipc spec` command. +# It was built from scratch to imports some 3rd party functions before reading podspecs + +require 'cocoapods' +require 'json' + +# Require 3rd party functions needed to parse podspecs. This code is copied from ios/Podfile +def node_require(script) + # Resolve script with node to allow for hoisting + require Pod::Executable.execute_command('node', ['-p', + "require.resolve( + '#{script}', + {paths: [process.argv[1]]}, + )", __dir__]).strip +end +node_require('react-native/scripts/react_native_pods.rb') +node_require('react-native-permissions/scripts/setup.rb') + +# Configure pod in silent mode +Pod::Config.instance.silent = true + +# Process command-line arguments +podspec_files = ARGV + +# Validate each podspec file +podspec_files.each do |podspec_file| + begin + spec = Pod::Specification.from_file(podspec_file) + puts(spec.to_pretty_json) + rescue => e + STDERR.puts "Failed to validate #{podspec_file}: #{e.message}" + end +end diff --git a/.github/scripts/verifyPodfile.sh b/.github/scripts/verifyPodfile.sh index cd94a49bb091..0d04d8f1b3ed 100755 --- a/.github/scripts/verifyPodfile.sh +++ b/.github/scripts/verifyPodfile.sh @@ -8,7 +8,12 @@ source scripts/shellUtils.sh title "Verifying that Podfile.lock is synced with the project" -declare EXIT_CODE=0 +# Cleanup and exit +# param - status code +function cleanupAndExit { + cd "$START_DIR" || exit 1 + exit "$1" +} # Check Provisioning Style. If automatic signing is enabled, iOS builds will fail, so ensure we always have the proper profile specified info "Verifying that automatic signing is not enabled" @@ -16,7 +21,7 @@ if grep -q 'PROVISIONING_PROFILE_SPECIFIER = "(NewApp) AppStore"' ios/NewExpensi success "Automatic signing not enabled" else error "Error: Automatic provisioning style is not allowed!" - EXIT_CODE=1 + cleanupAndExit 1 fi PODFILE_SHA=$(openssl sha1 ios/Podfile | awk '{print $2}') @@ -29,7 +34,7 @@ if [[ "$PODFILE_SHA" == "$PODFILE_LOCK_SHA" ]]; then success "Podfile checksum verified!" else error "Podfile.lock checksum mismatch. Did you forget to run \`npx pod-install\`?" - EXIT_CODE=1 + cleanupAndExit 1 fi info "Ensuring correct version of cocoapods is used..." @@ -45,29 +50,36 @@ if [[ "$POD_VERSION_FROM_GEMFILE" == "$POD_VERSION_FROM_PODFILE_LOCK" ]]; then success "Cocoapods version from Podfile.lock matches cocoapods version from Gemfile" else error "Cocoapods version from Podfile.lock does not match cocoapods version from Gemfile. Please use \`npm run pod-install\` or \`bundle exec pod install\` instead of \`pod install\` to install pods." - EXIT_CODE=1 + cleanupAndExit 1 fi info "Comparing Podfile.lock with node packages..." # Retrieve a list of podspec directories as listed in the Podfile.lock -SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modules*")' < ios/Podfile.lock) +if ! SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modules*")' < ios/Podfile.lock); then + error "Error: Could not parse podspec directories from Podfile.lock" + cleanupAndExit 1 +fi + +if ! read_lines_into_array PODSPEC_PATHS < <(npx react-native config | jq --raw-output '.dependencies[].platforms.ios.podspecPath | select ( . != null)'); then + error "Error: could not parse podspec paths from react-native config command" + cleanupAndExit 1 +fi # Format a list of Pods based on the output of the config command -FORMATTED_PODS=$( \ - jq --raw-output --slurp 'map((.name + " (" + .version + ")")) | .[]' <<< "$( \ - npx react-native config | \ - jq '.dependencies[].platforms.ios.podspecPath | select( . != null )' | \ - xargs -L 1 pod ipc spec --silent - )" -) +if ! FORMATTED_PODS=$( \ + jq --raw-output --slurp 'map((.name + " (" + .version + ")")) | .[]' <<< "$(./.github/scripts/printPodspec.rb "${PODSPEC_PATHS[@]}")" \ +); then + error "Error: could not parse podspecs at paths parsed from react-native config" + cleanupAndExit 1 +fi # Check for uncommitted package removals # If they are listed in Podfile.lock but the directories don't exist they have been removed while read -r DIR; do if [[ ! -d "${DIR#../}" ]]; then error "Directory \`${DIR#../node_modules/}\` not found in node_modules. Did you forget to run \`npx pod-install\` after removing the package?" - EXIT_CODE=1 + cleanupAndExit 1 fi done <<< "$SPEC_DIRS" @@ -75,15 +87,9 @@ done <<< "$SPEC_DIRS" while read -r POD; do if ! grep -q "$POD" ./ios/Podfile.lock; then error "$POD not found in Podfile.lock. Did you forget to run \`npx pod-install\`?" - EXIT_CODE=1 + cleanupAndExit 1 fi done <<< "$FORMATTED_PODS" -if [[ "$EXIT_CODE" == 0 ]]; then - success "Podfile.lock is up to date." -fi - -# Cleanup -cd "$START_DIR" || exit 1 - -exit $EXIT_CODE +success "Podfile.lock is up to date." +cleanupAndExit 0 diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 353a898a941f..4ad6d54e2f24 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -366,6 +366,17 @@ 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: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) && github.event_name == 'push' }} + steps: + - name: 'Deploy HybridApp' + run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + postSlackMessageOnSuccess: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index aeee5f730bfc..dcbec8a96e46 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,11 @@ local.properties android/app/src/main/java/com/expensify/chat/generated/ .cxx/ +# VIM +*.swp +*.swo +*~ + # Vscode .vscode diff --git a/.nvmrc b/.nvmrc index 62d44807d084..48b14e6b2b56 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.13.0 +20.14.0 diff --git a/android/app/build.gradle b/android/app/build.gradle index fb2791852d51..9343423c9f14 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047803 - versionName "1.4.78-3" + versionCode 1001048016 + versionName "1.4.80-16" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/bed.svg b/assets/images/bed.svg new file mode 100644 index 000000000000..fd654c036a7c --- /dev/null +++ b/assets/images/bed.svg @@ -0,0 +1 @@ + diff --git a/assets/images/car-with-key.svg b/assets/images/car-with-key.svg new file mode 100644 index 000000000000..1586c0dfecfa --- /dev/null +++ b/assets/images/car-with-key.svg @@ -0,0 +1 @@ + diff --git a/assets/images/check-circle.svg b/assets/images/check-circle.svg new file mode 100644 index 000000000000..c13b83cbf281 --- /dev/null +++ b/assets/images/check-circle.svg @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/assets/images/checkmark-circle.svg b/assets/images/checkmark-circle.svg new file mode 100644 index 000000000000..3497548bc1bc --- /dev/null +++ b/assets/images/checkmark-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/credit-card-exclamation.svg b/assets/images/credit-card-exclamation.svg new file mode 100644 index 000000000000..67e686516baa --- /dev/null +++ b/assets/images/credit-card-exclamation.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/assets/images/crosshair.svg b/assets/images/crosshair.svg new file mode 100644 index 000000000000..357faab49178 --- /dev/null +++ b/assets/images/crosshair.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/assets/images/emptystate__routepending.svg b/assets/images/emptystate__routepending.svg index 90f3296d37d6..aba08554d02f 100644 --- a/assets/images/emptystate__routepending.svg +++ b/assets/images/emptystate__routepending.svg @@ -1 +1,18 @@ - \ No newline at end of file + + + + + + + + + diff --git a/assets/images/inbox.svg b/assets/images/inbox.svg new file mode 100644 index 000000000000..f9059e78ec5a --- /dev/null +++ b/assets/images/inbox.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/assets/images/money-search.svg b/assets/images/money-search.svg new file mode 100644 index 000000000000..90dedae0a2fb --- /dev/null +++ b/assets/images/money-search.svg @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/plane.svg b/assets/images/plane.svg new file mode 100644 index 000000000000..bf4d56875239 --- /dev/null +++ b/assets/images/plane.svg @@ -0,0 +1 @@ + diff --git a/assets/images/product-illustrations/emptystate__travel.svg b/assets/images/product-illustrations/emptystate__travel.svg new file mode 100644 index 000000000000..416b27eb5bee --- /dev/null +++ b/assets/images/product-illustrations/emptystate__travel.svg @@ -0,0 +1,575 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/receipt-slash.svg b/assets/images/receipt-slash.svg new file mode 100644 index 000000000000..2af3fcbc60e6 --- /dev/null +++ b/assets/images/receipt-slash.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg new file mode 100644 index 000000000000..a96a7e5dc0af --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__checkmarkcircle.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__sendmoney.svg b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg new file mode 100644 index 000000000000..80393e3c30cf --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__sendmoney.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg new file mode 100644 index 000000000000..e158bc5588cb --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__subscription-annual.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg new file mode 100644 index 000000000000..d70d2d1ef552 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/assets/images/subscription-details__approvedlogo--light.svg b/assets/images/subscription-details__approvedlogo--light.svg new file mode 100644 index 000000000000..580ee60c597c --- /dev/null +++ b/assets/images/subscription-details__approvedlogo--light.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/subscription-details__approvedlogo.svg b/assets/images/subscription-details__approvedlogo.svg new file mode 100644 index 000000000000..7722e2526657 --- /dev/null +++ b/assets/images/subscription-details__approvedlogo.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/README.md b/docs/README.md index 224abe0554a5..235334c95732 100644 --- a/docs/README.md +++ b/docs/README.md @@ -186,9 +186,18 @@ Just update the content for each variable accordingly or remove it if the inform Assume that we want to rename the article `The Free Plan` to `Freemium Features` for the hub `billing and plan types` in New Expensify platform. 1. Go to `docs/articles/new-expensify/billing-and-plan-types` 2. Rename `The-Free-Plan.md` to `Freemium-Features.md`. Use dashes for spaces in the file name. +3. Add an entry in redirects.csv for the old article pointing to the new article. Note: It is important that the file has `.md` extension. +# How to hide an article temporarily +Video demo available [here 🧵](https://expensify.slack.com/archives/C02NK2DQWUX/p1717772272605829?thread_ts=1717523271.137469&cid=C02NK2DQWUX). +1. Open github's in built code editor by pressing `.` on your keyboard. ([instructions here](https://docs.github.com/en/codespaces/the-githubdev-web-based-editor#opening-the-githubdev-editor)) +2. Go to the article that you want to hide `docs/articles/...`. +3. Drag and drop the article inside the hidden folder `docs/Hidden/`. +4. Add a redirect for it in `docs/redirects.csv` to ensure that we don't have broken links. You can choose to point it to the home page or the article's hub. +5. Commit the changes and raise a PR. + # How the site is deployed This site is hosted on Cloudflare pages. Whenever code is merged to main, the github action `deployExpensifyHelp` will run. diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ae19775d75df..eb59388159bf 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -941,8 +941,8 @@ button { } #platform-tabs > .active { - color: var(--color-button-text); - background-color: var(--color-button-success-background); + color: var(--color-text); + background-color: var(--color-button-background); } .hidden { diff --git a/docs/articles/expensify-classic/reports/Add-custom-fields-to-reports-and-invoices.md b/docs/articles/expensify-classic/reports/Add-custom-fields-to-reports-and-invoices.md new file mode 100644 index 000000000000..ce0f60d3be56 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Add-custom-fields-to-reports-and-invoices.md @@ -0,0 +1,27 @@ +--- +title: Add custom fields to reports and invoices +description: Customize the fields that appear on a report or an invoice +--- +
+ +Workspace Admins can add additional required fields to a report to include selections for project names, locations, trip information, and more. + +{% include info.html %} +You cannot create these report fields directly in Expensify if you are connected to an accounting integration (QuickBooks Online, QuickBooks Desktop, Intacct, Xero, or NetSuite). Please refer to the relevant article for instructions on creating fields within that system. +{% include end-info.html %} + +To create a custom field for a report, + +1. Hover over Settings and select **Workspaces**. +2. Select the desired workspace. +3. Click the **Reports** tab on the left. +4. Scroll down to the Report and Invoice Fields section. +5. Under Add New Field, enter a Field Title. +6. Click the dropdown for the Type field and select the desired selection method: + - **Text**: Provides a text box to type in the requested information. + - **Dropdown**: Provides a dropdown of options to choose from. + - **Date**: Opens a calendar to select a date. +7. Select the report type: **Expense Report** or **Invoice**. +8. Click **Add**. + +
diff --git a/docs/articles/expensify-classic/reports/Set-default-report-title.md b/docs/articles/expensify-classic/reports/Set-default-report-title.md new file mode 100644 index 000000000000..a103ad8d5e5a --- /dev/null +++ b/docs/articles/expensify-classic/reports/Set-default-report-title.md @@ -0,0 +1,17 @@ +--- +title: Set default report title +description: Set an automatic title for all reports +--- +
+ +Workspace Admins can set a default report title for all reports created under a specific workspace. If desired, these titles can also be enforced to prevent employees from changing them. + +1. Hover over Settings and select **Workspaces**. +2. Select the desired workspace. +3. Click the **Reports** tab on the left. +4. Scroll down to the Default Report Title section. +5. Configure the formula. You can use the example provided on the page as a guide or choose from more [report formula options](https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates). + - Some formulas will automatically update the report title as changes are made to the report. For example, any formula related to dates, total amounts, workspace name, would adjust the title before the report is submitted for approval. Changes will not retroactively update report titles for reports which have been Approved or Reimbursed. +6. If desired, enable the Enforce Default Report Title toggle. This will prevent employees from editing the default title. + +
diff --git a/docs/articles/expensify-classic/travel/Approve-travel-expenses.md b/docs/articles/expensify-classic/travel/Approve-travel-expenses.md new file mode 100644 index 000000000000..ae0feb4efaa6 --- /dev/null +++ b/docs/articles/expensify-classic/travel/Approve-travel-expenses.md @@ -0,0 +1,37 @@ +--- +title: Approve travel expenses +description: Determine how travel expenses are approved +--- +
+ +Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. + +- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. +- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. +- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. + +# Set approval method + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under General, select approval methods for Flights, Hotels, Cars and Rail. + +
+ +
+ +Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. + +- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. +- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. +- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. + +# Set approval method + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under General, select approval methods for Flights, Hotels, Cars and Rail. + +
diff --git a/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md b/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md new file mode 100644 index 000000000000..5d25670ac5ab --- /dev/null +++ b/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md @@ -0,0 +1,89 @@ +--- +title: Book with Expensify Travel +description: Book flights, hotels, cars, trains, and more with Expensify Travel +--- +
+ +Expensify Travel allows members to search and book flights, hotels, cars, and trains globally at the most competitive rates available. + +With Expensify Travel, you can: +- Search and book travel arrangements all in one place +- Book travel for yourself or for someone else +- Get real-time support by chat or phone +- Manage all your T&E expenses in Expensify +- Create specific rules for booking travel +- Enable approvals for out-of-policy trips +- Book with any credit card on the market +- Book with the Expensify Card to get cash back and automatically reconcile transactions + +There is a flat fee of $15 per trip booked. A single trip can include multiple bookings, such as a flight, a hotel, and a car rental. + +# Book travel + +To book travel from the Expensify web app, + +1. Click the **Travel** tab. +2. Click **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 travel arrangement selected (for example, the destination, dates of travel, etc.). +5. Select all the details for the arrangement you want to book. +6. Review the booking details and click **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. + +{% include info.html %} +The travel itinerary is also emailed to the traveler’s [copilots](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot), if applicable. +{% include end-info.html %} + +
+ +
+Expensify Travel allows members to search and book flights, hotels, cars, and trains globally at the most competitive rates available. + +With Expensify Travel, you can: +- Search and book travel arrangements all in one place +- Book travel for yourself or for someone else +- Get real-time support by chat or phone +- Manage all your T&E expenses in Expensify +- Create specific rules for booking travel +- Enable approvals for out-of-policy trips +- Book with any credit card on the market +- Book with the Expensify Card to get cash back and automatically reconcile transactions + +There is a flat fee of $15 per trip booked. A single trip can include multiple bookings, such as a flight, a hotel, and a car rental. + +# Book travel + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Agree to the terms and conditions and click **Continue**. +4. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +5. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). +6. Select all the details for the arrangement you want to book. +7. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom menu and select **Book travel**. +2. Tap **Book or manage travel**. +3. Agree to the terms and conditions and tap **Continue**. +4. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. +5. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). +6. Select all the details for the arrangement you want to book. +7. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. +{% include end-option.html %} + +{% include end-selector.html %} + +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. + +
diff --git a/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md b/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md new file mode 100644 index 000000000000..2e17af06773c --- /dev/null +++ b/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md @@ -0,0 +1,65 @@ +--- +title: Configure travel policy and preferences +description: Set and update travel policies and preferences for your Expensify Workspace +--- +
+ +As a Workspace Admin, you can set travel policies for all travel booked under your workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. + +# Create a travel policy + +When using Expensify Travel for the first time, you will need to create a new Travel Policy. + +To create a travel policy from the Expensify web app, + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. +5. Use the **Edit members** section to select the group of employees that belong to this policy. A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. +6. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +7. Click the paperclip icon next to each setting to de-couple it from your default policy. +8. Update the desired settings. + +# Update travel policy preferences + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Select the appropriate policy in the left menu. +5. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +6. Click the paperclip icon next to each setting to de-couple it from your default policy. +7. Update the desired policies. + +
+ +
+ +As a Workspace Admin, you can set travel policies for all travel booked under your workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. + +# Create a travel policy + +When using Expensify Travel for the first time, you will need to create a new Travel Policy. + +To create a travel policy from the Expensify web app, + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. +5. Use the **Edit members** section to select the group of employees that belong to this policy. A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. +6. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +7. Click the paperclip icon next to each setting to de-couple it from your default policy. +8. Update the desired settings. + +# Update travel policy preferences + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Select the appropriate policy in the left menu. +5. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +6. Click the paperclip icon next to each setting to de-couple it from your default policy. +7. Update the desired policies. + +
diff --git a/docs/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements.md b/docs/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements.md new file mode 100644 index 000000000000..7dc71c3220ca --- /dev/null +++ b/docs/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements.md @@ -0,0 +1,28 @@ +--- +title: Edit or cancel travel arrangements +description: Modify travel arrangements booked with Expensify Travel +--- +
+ +Click **Get Support** on your emailed travel itinerary for real-time help with the booking. Any modifications, exchanges, or voidings made to a trip via support will incur a $25 booking change fee. + +
+ +
+ +You can review your travel arrangements any time by opening the Trip chat in your inbox. For example, if you booked a flight to San Francisco, a “Trip to San Francisco” chat will be automatically added to your chat inbox. + +To edit or cancel a travel arrangement, +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace the travel is booked under. +4. Tap into the booking to see more details. +5. Click **Trip Support**. + +If there is an unexpected change to the itinerary (for example, a flight cancellation), Expensify’s travel partner **Spotnana** will reach out to the traveler to provide updates on those changes. + +{% include info.html %} +You can click **Get Support** on your emailed travel itinerary for real-time help with the booking. Any modifications, exchanges, or voidings made to a trip via support will incur a $25 booking change fee. +{% include end-info.html %} + +
diff --git a/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md deleted file mode 100644 index e79e30ce42c9..000000000000 --- a/docs/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Report Fields & Titles -description: This article is about managing Report Fields and Report Titles in Expensify ---- -# Overview - -In this article, we'll go over how to use Report Titles and Report Fields. - -## How to use Report Titles - -Default report titles enable group workspace admins or individual workspace users to establish a standardized title format for reports associated with a specific workspace. Additionally, admins have the choice to enforce these report titles, preventing employees from altering them. This ensures uniformity in report naming conventions during the review process, eliminating the need for employees to manually input titles for each report they generate. - -- Group workspace admins can set the Default Report Title from **Settings > Workspaces > Group > *[Workspace Name]* > Reports**. -- Individual users can set the Default Report Title from **Settings > Workspaces > Individual > *[Workspace Name]* > Reports**. - -You can configure the title by using the formulas that we provide to populate the Report Title. Take a look at the help article on Custom Formulas to find all eligible formulas for your Report Titles. - -## Deep Dive on Report Titles - -Some formulas will automatically update the report title as changes are made to the report. For example, any formula related to dates, total amounts, workspace name, would adjust the title before the report is submitted for approval. Changes will not retroactively update report titles for reports which have been Approved or Reimbursed. - -To prevent report title editing by employees, simply enable "Enforce Default Report Title." - -## How to use Report Fields - -Report fields let you specify header-level details, distinct from tags which pertain to expenses on individual line items. These details can encompass specific project names, business trip information, locations, and more. Customize them according to your workspace's requirements. - -To set up Report Fields, follow these steps: -- Workspace Admins can create report fields for group workspaces from **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Report and Invoice Fields**. For individual workspaces, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Report and Invoice Fields**. -- Under "Add New Field," enter the desired field name in the "Field Title" to describe the type of information to be selected. -- Choose the appropriate input method under "Type": - - Text: Provides users with a free-text box to enter the requested information. - - Dropdown: Creates a selection of options for users to choose from. - - Date: Displays a clickable box that opens a calendar for users to select a date. - -## Deep Dive on Report Fields - -You cannot create these report fields directly in Expensify if you are connected to an accounting integration (QuickBooks Online, QuickBooks Desktop, Intacct, Xero, or NetSuite). Please refer to the relevant article for instructions on creating fields within that system. - -When report fields are configured on a workspace, they become mandatory information for associated reports. Leaving a report field empty or unselected will trigger a report violation, potentially blocking report submission or export. - -Report fields are "sticky," which means that any changes made by an employee will persist and be reflected in subsequent reports they create. - diff --git a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md deleted file mode 100644 index 18ad693a1c56..000000000000 --- a/docs/articles/expensify-classic/workspaces/reports/Scheduled-Submit.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Scheduled Submit -description: How to use the Scheduled Submit feature ---- -# Overview - -Scheduled Submit reduces the delay between the time an employee creates an expense to when it is submitted to the admin. This gives admins significantly faster visibility into employee spend. Without Scheduled Submit enabled, expenses can be left Unreported giving workspace admins no visibility into employee spend. - -The biggest delay in expense management is the time it takes for an employee to actually submit the expense after it is incurred. Scheduled Submit allows you to automatically collect employee expenses on a schedule of your choosing without delaying the process while you wait for employees to submit them. - -It works like this: Employee expenses are automatically gathered onto a report. If there is not an existing report, a new one will be created. This report is submitted automatically at the cadence you choose (daily, weekly, monthly, twice month, by trip). - -# How to enable Scheduled Submit - -**For workspace admins**: To enable Scheduled Submit on your group workspace, follow **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. -For individuals or employees: To enable Scheduled Submit on your individual workspace, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. - -## Scheduled Submit frequency options - -**Daily**: Each night, expenses without violations will be submitted. Expenses with violations will remain on an open report until the violations are corrected, after which they will be submitted in the evening (PDT). - -**Weekly**: Expenses that are free of violations will be submitted on a weekly basis. However, expenses with violations will be held in a new open report and combined with any new expenses. They will then be submitted at the end of the following weekly cycle, specifically on Sunday evening (PDT). - -**Twice a month**: Expenses that are violation-free will be submitted on both the 15th and the last day of each month, in the evening (PDT). Expenses with violations will not be submitted, but moved on to a new open report so the employee can resolve the violations and then will be submitted at the conclusion of the next cycle. - -**Monthly**: Expenses that are free from violations will be submitted on a monthly basis. Expenses with violations will be held back and moved to a new Open report so the violations can be resolved, and they will be submitted on the evening (PDT) of the specified date. - -**By trip**: Expenses are grouped by trip. This is calculated by grouping all expenses together that occur in a similar time frame. If two full days pass without any new expenses being created, the trip report will be submitted on the evening of the second day. Any expenses generated after this submission will initiate a new trip report. Please note that the "2-day" period refers to a date-based interval, not a 48-hour time frame. - -**Manually**: An open report will be created, and expenses will be added to it automatically. However, it's important to note that the report will not be submitted automatically; manual submission of reports will be required.This is a great option for automatically gathering all an employee’s expenses on a report for the employee’s convenience, but they will still need to review and submit the report. - -**Instantly**: Expenses are automatically added to a report in the Processing state, and all expenses will continue to accumulate on one report until it is Approved or Reimbursed. This removes the need to submit expenses, and Processing reports can either be Reimbursed right away (if Submit and Close is enabled), or Approved and then Reimbursed (if Submit and Approve is enabled) by a workspace admin. - -# Deep Dive - -## Schedule Submit Override -If Scheduled Submit is disabled at the group workspace level or configured the frequency as "Manually," the individual workspace settings of a user will take precedence and be applied. This means an employee can still set up Scheduled Submit for themselves even if the admin has not enabled it. We highly recommend Scheduled Submit as it helps put your expenses on auto-pilot! - -## Personal Card Transactions -Personal card transactions are handled differently compared to other expenses. If a user has added a card through Settings > Account > Credit Card Import, they need to make sure it is set as non-reimbursable and transactions must be automatically merged with a SmartScanned receipt. If transactions are set to come in as reimbursable or they aren’t merged with a SmartScanned receipt, Scheduled Submit settings will not apply. - -## A note on Instantly -Setting Scheduled Submit frequency to Instantly will limit some employee actions on reports, such as the ability to retract or self-close reports, or create multiple reports. When Instantly is selected, expenses are automatically added to a Processing expense report, and new expenses will continue to accumulate on a single report until the report is Closed or Reimbursed by a workspace admin. diff --git a/docs/articles/new-expensify/expenses/Set-up-your-wallet.md b/docs/articles/new-expensify/expenses/Set-up-your-wallet.md new file mode 100644 index 000000000000..de1ee61066b0 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Set-up-your-wallet.md @@ -0,0 +1,52 @@ +--- +title: Set up your wallet +description: Send and receive payments by adding your payment account +--- +
+To send and receive money using Expensify, you’ll first need to set up your Expensify Wallet by adding your payment account. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Wallet** in the left menu. +3. Click **Enable wallet**. +4. If you haven’t already added your bank account, click **Continue** and follow the prompts to add your bank account details with Plaid. If you have already connected your bank account, you’ll skip to the next step. + +{% include info.html %} +Plaid is an encrypted third-party financial data platform that Expensify uses to securely verify your banking information. +{% include end-info.html %} + +{:start="5"} +5. Enter your personal details (including your name, address, date of birth, phone number, and the last 4 digits of your social security number). +6. Click **Save & continue**. +7. Review the Onfido terms and click **Accept**. +8. Use the prompts to continue the next steps on your mobile device where you will select which option you want to use to verify your device: a QR code, a link, or a text message. +9. Follow the prompts on your mobile device to submit your ID with Onfido. + +When your ID is uploaded successfully, Onfido closes automatically. You can return to your Expensify Wallet to verify that it is now enabled. Once enabled, you are ready to send and receive payments. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Wallet**. +3. Tap **Enable wallet**. +4. If you haven’t already added your bank account, tap **Continue** and follow the prompts to add your bank account details with Plaid. If you have already connected your bank account, you’ll skip to the next step. + +{% include info.html %} +Plaid is an encrypted third-party financial data platform that Expensify uses to securely verify your banking information. +{% include end-info.html %} + +{:start="5"} +5. Enter your personal details (including your name, address, date of birth, phone number, and the last 4 digits of your social security number). +6. Tap **Save & continue**. +7. Review the Onfido terms and tap **Accept**. +8. Follow the prompts to submit your ID with Onfido. When your ID is uploaded successfully, Onfido closes automatically. +9. Tap **Enable wallet** again to enable payments for the wallet. + +Once enabled, you are ready to send and receive payments. +{% include end-option.html %} + +{% include end-selector.html %} + +
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 24f178db9f12..56e456eb1256 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 @@ -31,7 +31,7 @@ Before completing this process, you’ll want to: New cards will have the same limit as the existing cards. Each cardholder’s current physical and virtual cards will remain active until a Domain Admin or the cardholder deactivates it. {% include info.html %} -Cards won’t be issued to any employees who don’t currently have them. In this case, you’ll need to issue a new card. +Cards won’t be issued to any employees who don’t currently have them. In this case, 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) {% include end-info.html %} {% include faq-begin.md %} diff --git a/docs/articles/new-expensify/travel/Approve-travel-expenses.md b/docs/articles/new-expensify/travel/Approve-travel-expenses.md new file mode 100644 index 000000000000..ae0feb4efaa6 --- /dev/null +++ b/docs/articles/new-expensify/travel/Approve-travel-expenses.md @@ -0,0 +1,37 @@ +--- +title: Approve travel expenses +description: Determine how travel expenses are approved +--- +
+ +Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. + +- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. +- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. +- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. + +# Set approval method + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under General, select approval methods for Flights, Hotels, Cars and Rail. + +
+ +
+ +Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. + +- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. +- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. +- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. + +# Set approval method + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under General, select approval methods for Flights, Hotels, Cars and Rail. + +
diff --git a/docs/articles/new-expensify/travel/Coming-Soon.md b/docs/articles/new-expensify/travel/Coming-Soon.md deleted file mode 100644 index 4d32487a14b5..000000000000 --- a/docs/articles/new-expensify/travel/Coming-Soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Coming soon -description: Coming soon ---- - -# Coming soon \ No newline at end of file diff --git a/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md new file mode 100644 index 000000000000..2e17af06773c --- /dev/null +++ b/docs/articles/new-expensify/travel/Configure-travel-policy-and-preferences.md @@ -0,0 +1,65 @@ +--- +title: Configure travel policy and preferences +description: Set and update travel policies and preferences for your Expensify Workspace +--- +
+ +As a Workspace Admin, you can set travel policies for all travel booked under your workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. + +# Create a travel policy + +When using Expensify Travel for the first time, you will need to create a new Travel Policy. + +To create a travel policy from the Expensify web app, + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. +5. Use the **Edit members** section to select the group of employees that belong to this policy. A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. +6. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +7. Click the paperclip icon next to each setting to de-couple it from your default policy. +8. Update the desired settings. + +# Update travel policy preferences + +1. Click the **Travel** tab. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Select the appropriate policy in the left menu. +5. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +6. Click the paperclip icon next to each setting to de-couple it from your default policy. +7. Update the desired policies. + +
+ +
+ +As a Workspace Admin, you can set travel policies for all travel booked under your workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. + +# Create a travel policy + +When using Expensify Travel for the first time, you will need to create a new Travel Policy. + +To create a travel policy from the Expensify web app, + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. +5. Use the **Edit members** section to select the group of employees that belong to this policy. A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. +6. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +7. Click the paperclip icon next to each setting to de-couple it from your default policy. +8. Update the desired settings. + +# Update travel policy preferences + +1. Click the + icon in the bottom left menu and select **Book travel**. +2. Click **Book or manage travel**. +3. Click the **Program** tab at the top and select **Policies**. +4. Select the appropriate policy in the left menu. +5. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. +6. Click the paperclip icon next to each setting to de-couple it from your default policy. +7. Update the desired policies. + +
diff --git a/docs/articles/new-expensify/travel/Edit-or-cancel-travel-arrangements.md b/docs/articles/new-expensify/travel/Edit-or-cancel-travel-arrangements.md new file mode 100644 index 000000000000..7dc71c3220ca --- /dev/null +++ b/docs/articles/new-expensify/travel/Edit-or-cancel-travel-arrangements.md @@ -0,0 +1,28 @@ +--- +title: Edit or cancel travel arrangements +description: Modify travel arrangements booked with Expensify Travel +--- +
+ +Click **Get Support** on your emailed travel itinerary for real-time help with the booking. Any modifications, exchanges, or voidings made to a trip via support will incur a $25 booking change fee. + +
+ +
+ +You can review your travel arrangements any time by opening the Trip chat in your inbox. For example, if you booked a flight to San Francisco, a “Trip to San Francisco” chat will be automatically added to your chat inbox. + +To edit or cancel a travel arrangement, +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace the travel is booked under. +4. Tap into the booking to see more details. +5. Click **Trip Support**. + +If there is an unexpected change to the itinerary (for example, a flight cancellation), Expensify’s travel partner **Spotnana** will reach out to the traveler to provide updates on those changes. + +{% include info.html %} +You can click **Get Support** on your emailed travel itinerary for real-time help with the booking. Any modifications, exchanges, or voidings made to a trip via support will incur a $25 booking change fee. +{% include end-info.html %} + +
diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js index 6b3390148ff0..9e4880780e91 100644 --- a/docs/assets/js/main.js +++ b/docs/assets/js/main.js @@ -196,6 +196,35 @@ const tocbotOptions = { scrollContainer: 'content-area', }; +function selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent) { + newExpensifyTab.classList.add('active'); + newExpensifyContent.classList.remove('hidden'); + + if (expensifyClassicTab && expensifyClassicContent) { + expensifyClassicTab.classList.remove('active'); + expensifyClassicContent.classList.add('hidden'); + } + window.tocbot.refresh({ + ...tocbotOptions, + contentSelector: '#new-expensify', + }); +} + +function selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent) { + expensifyClassicTab.classList.add('active'); + expensifyClassicContent.classList.remove('hidden'); + + if (newExpensifyTab && newExpensifyContent) { + newExpensifyTab.classList.remove('active'); + newExpensifyContent.classList.add('hidden'); + } + + window.tocbot.refresh({ + ...tocbotOptions, + contentSelector: '#expensify-classic', + }); +} + window.addEventListener('DOMContentLoaded', () => { injectFooterCopywrite(); @@ -219,8 +248,10 @@ window.addEventListener('DOMContentLoaded', () => { let contentSelector = '.article-toc-content'; if (expensifyClassicContent) { contentSelector = '#expensify-classic'; + selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); } else if (newExpensifyContent) { contentSelector = '#new-expensify'; + selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); } if (window.tocbot) { @@ -232,28 +263,12 @@ window.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line es/no-optional-chaining expensifyClassicTab?.addEventListener('click', () => { - expensifyClassicTab.classList.add('active'); - expensifyClassicContent.classList.remove('hidden'); - - newExpensifyTab.classList.remove('active'); - newExpensifyContent.classList.add('hidden'); - window.tocbot.refresh({ - ...tocbotOptions, - contentSelector: '#expensify-classic', - }); + selectExpensifyClassic(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); }); // eslint-disable-next-line es/no-optional-chaining newExpensifyTab?.addEventListener('click', () => { - newExpensifyTab.classList.add('active'); - newExpensifyContent.classList.remove('hidden'); - - expensifyClassicTab.classList.remove('active'); - expensifyClassicContent.classList.add('hidden'); - window.tocbot.refresh({ - ...tocbotOptions, - contentSelector: '#new-expensify', - }); + selectNewExpensify(newExpensifyTab, newExpensifyContent, expensifyClassicTab, expensifyClassicContent); }); document.getElementById('header-button').addEventListener('click', toggleHeaderMenu); diff --git a/docs/redirects.csv b/docs/redirects.csv index 3042dc79085c..13463327d06d 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -197,5 +197,7 @@ https://help.expensify.com/articles/new-expensify/workspaces/The-Free-Plan,https https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account https://help.expensify.com/articles/new-expensify/settings/Security,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency +https://help.expensify.com/articles/expensify-classic/workspaces/reports/Report-Fields-And-Titles,https://help.expensify.com/expensify-classic/hubs/workspaces/ +https://help.expensify.com/articles/expensify-classic/workspaces/reports/Scheduled-Submit,https://help.expensify.com/articles/expensify-classic/reports/Automatically-submit-employee-reports https://help.expensify.com/articles/new-expensify/chat/Expensify-Chat-For-Admins,https://help.expensify.com/new-expensify/hubs/chat/ https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account.html,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 8337041077be..ad1d743d778d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.78 + 1.4.80 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.78.3 + 1.4.80.16 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7b076baeb35a..23bdf74a3648 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.78 + 1.4.80 CFBundleSignature ???? CFBundleVersion - 1.4.78.3 + 1.4.80.16 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 014ab9966e82..9f8cc7b1612e 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.78 + 1.4.80 CFBundleVersion - 1.4.78.3 + 1.4.80.16 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1ea6b65a58b7..c138d1b27f61 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1852,7 +1852,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.70): + - RNLiveMarkdown (0.1.82): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1870,9 +1870,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.70) + - RNLiveMarkdown/common (= 0.1.82) - Yoga - - RNLiveMarkdown/common (0.1.70): + - RNLiveMarkdown/common (0.1.82): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1942,7 +1942,7 @@ PODS: - React-Codegen - React-Core - ReactCommon/turbomodule/core - - RNReanimated (3.7.2): + - RNReanimated (3.8.1): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2589,12 +2589,12 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 23250f3d64c9d5f82ff36c4733c03544af0222d2 + RNLiveMarkdown: d160a948e52282067439585c89a3962582c082ce RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 - RNReanimated: 51db0fff543694d931bd3b7cab1a3b36bd86c738 + RNReanimated: 323436b1a5364dca3b5f8b1a13458455e0de9efe RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 diff --git a/package-lock.json b/package-lock.json index 2829937478da..aaef29c6fad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.78-3", + "version": "1.4.80-16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.78-3", + "version": "1.4.80-16", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.70", + "@expensify/react-native-live-markdown": "0.1.82", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -59,7 +59,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", + "expensify-common": "^2.0.10", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -207,7 +207,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.49", + "eslint-config-expensify": "^2.0.50", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -250,8 +250,8 @@ "yaml": "^2.2.1" }, "engines": { - "node": "20.13.0", - "npm": "10.5.2" + "node": "20.14.0", + "npm": "10.7.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3558,9 +3558,14 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.70.tgz", - "integrity": "sha512-HyqBtZyvuJFB4gIUECKIMxWCnTPlPj+GPWmw80VzMBRFV9QiFRKUKRWefNEJ1cXV5hl8a6oOWDQla+dCnjCzOQ==", + "version": "0.1.82", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.82.tgz", + "integrity": "sha512-w/K2+0d1sAYvyLVpPv1ufDOTaj4y96Z362N3JDN+SDUmPQN2MvVGwsTL0ltzdw78yd62azFcQl6th7P6l62THQ==", + "workspaces": [ + "parser", + "example", + "WebExample" + ], "engines": { "node": ">= 18.0.0" }, @@ -19365,9 +19370,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.49", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.49.tgz", - "integrity": "sha512-3yGQuOsjvtWh/jYSJKIJgmwULhrVMCiYkWGzLOKpm/wCzdiP4l0T/gJMWOkvGhTtyqxsP7ZUTwPODgcE3extxA==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.50.tgz", + "integrity": "sha512-I+OMkEprqEWlSCZGJBJxpt2Wg4HQ41/QqpKVfcADiQ3xJ76bZ1mBueqz6DR4jfph1xC6XVRl4dqGNlwbeU/2Rg==", "dev": true, "dependencies": { "@lwc/eslint-plugin-lwc": "^1.7.2", @@ -19842,8 +19847,9 @@ }, "node_modules/eslint-plugin-you-dont-need-lodash-underscore": { "version": "6.12.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.12.0.tgz", + "integrity": "sha512-WF4mNp+k2532iswT6iUd1BX6qjd3AV4cFy/09VC82GY9SsRtvkxhUIx7JNGSe0/bLyd57oTr4inPFiIaENXhGw==", "dev": true, - "license": "MIT", "dependencies": { "kebab-case": "^1.0.0" }, @@ -20335,11 +20341,11 @@ } }, "node_modules/expensify-common": { - "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", - "integrity": "sha512-uy1+axUTTuPKwAR06xNG/tGIJ+uaavmSQgKiNU7pQVR94ibNzDD2WESn2E7OEP9/QrHa61lfFlluTjFvvz5I8Q==", - "license": "MIT", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.10.tgz", + "integrity": "sha512-+8LCtnR+VxmCjKKkfeR6XGAhVxvwZtQAw3386c1EDGNK1C0bvz3I1kLVMFbulSeibZv6/G33aO6SiW/kwum6Nw==", "dependencies": { + "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", "clipboard": "2.0.11", "html-entities": "^2.5.2", diff --git a/package.json b/package.json index 13a2f5eabf03..ee1ad32a0067 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.78-3", + "version": "1.4.80-16", "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.", @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.70", + "@expensify/react-native-live-markdown": "0.1.82", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -111,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", + "expensify-common": "^2.0.10", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -259,7 +259,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.49", + "eslint-config-expensify": "^2.0.50", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -329,7 +329,7 @@ ] }, "engines": { - "node": "20.13.0", - "npm": "10.5.2" + "node": "20.14.0", + "npm": "10.7.0" } } diff --git a/patches/@react-navigation+native+6.1.12.patch b/patches/@react-navigation+native+6.1.12.patch index d451d89d687c..d53f8677d225 100644 --- a/patches/@react-navigation+native+6.1.12.patch +++ b/patches/@react-navigation+native+6.1.12.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js -index 16fdbef..bc2c96a 100644 +index 16fdbef..e660dd6 100644 --- a/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js +++ b/node_modules/@react-navigation/native/lib/module/createMemoryHistory.js @@ -1,8 +1,23 @@ @@ -63,6 +63,15 @@ index 16fdbef..bc2c96a 100644 replace(_ref3) { var _window$history$state2; let { +@@ -80,7 +101,7 @@ export default function createMemoryHistory() { + + // Need to keep the hash part of the path if there was no previous history entry + // or the previous history entry had the same path +- let pathWithHash = path; ++ let pathWithHash = path.replace(/(\/{2,})|(\/$)/g, (match, p1) => (p1 ? '/' : '')); + if (!items.length || items.findIndex(item => item.id === id) < 0) { + // There are two scenarios for creating an array with only one history record: + // - When loaded id not found in the items array, this function by default will replace @@ -108,7 +129,9 @@ export default function createMemoryHistory() { window.history.replaceState({ id diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index 848e6d238254..fa44f2ee7d3a 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -102,4 +102,18 @@ get_abs_path() { abs_path=${abs_path/#\/\//\/} echo "$abs_path" -} \ No newline at end of file +} + +# Function to read lines from standard input into an array using a temporary file. +# This is a bash 3 polyfill for readarray. +# Arguments: +# $1: Name of the array variable to store the lines +# Usage: +# read_lines_into_array array_name +read_lines_into_array() { + local array_name="$1" + local line + while IFS= read -r line || [ -n "$line" ]; do + eval "$array_name+=(\"$line\")" + done +} diff --git a/src/CONST.ts b/src/CONST.ts index de4e3305eddc..b0e3ab8c3af4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -50,6 +50,7 @@ const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; const chatTypes = { POLICY_ANNOUNCE: 'policyAnnounce', POLICY_ADMINS: 'policyAdmins', + TRIP_ROOM: 'tripRoom', GROUP: 'group', DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', @@ -574,6 +575,7 @@ const CONST = { ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'), MANAGE_CARDS_URL: 'domain_companycards', FEES_URL: `${USE_EXPENSIFY_URL}/fees`, + SAVE_WITH_EXPENSIFY_URL: `${USE_EXPENSIFY_URL}/savings-calculator`, CFPB_PREPAID_URL: 'https://cfpb.gov/prepaid', STAGING_NEW_EXPENSIFY_URL: 'https://staging.new.expensify.com', NEWHELP_URL: 'https://help.expensify.com', @@ -697,6 +699,7 @@ const CONST = { TASK_COMPLETED: 'TASKCOMPLETED', TASK_EDITED: 'TASKEDITED', TASK_REOPENED: 'TASKREOPENED', + TRIPPREVIEW: 'TRIPPREVIEW', UNAPPROVED: 'UNAPPROVED', // OldDot Action UNHOLD: 'UNHOLD', UNSHARE: 'UNSHARE', // OldDot Action @@ -937,7 +940,7 @@ const CONST = { TO: 'to', CATEGORY: 'category', TAG: 'tag', - TOTAL: 'total', + TOTAL_AMOUNT: 'amount', TYPE: 'type', ACTION: 'action', TAX_AMOUNT: 'taxAmount', @@ -1577,7 +1580,7 @@ const CONST = { APPROVE: 'approve', TRACK: 'track', }, - AMOUNT_MAX_LENGTH: 10, + AMOUNT_MAX_LENGTH: 8, RECEIPT_STATE: { SCANREADY: 'SCANREADY', OPEN: 'OPEN', @@ -1814,6 +1817,7 @@ const CONST = { XERO_CHECK_CONNECTION: 'xeroCheckConnection', XERO_SYNC_TITLE: 'xeroSyncTitle', }, + SYNC_STAGE_TIMEOUT_MINUTES: 20, }, ACCESS_VARIANTS: { PAID: 'paid', @@ -1897,6 +1901,12 @@ const CONST = { COMPACT: 'compact', DEFAULT: 'default', }, + SUBSCRIPTION: { + TYPE: { + ANNUAL: 'yearly2018', + PAYPERUSE: 'monthly2018', + }, + }, REGEX: { SPECIAL_CHARS_WITHOUT_NEWLINE: /((?!\n)[()-\s\t])/g, DIGITS_AND_PLUS: /^\+?[0-9]*$/, @@ -3296,6 +3306,7 @@ const CONST = { }, CONCIERGE_TRAVEL_URL: 'https://community.expensify.com/discussion/7066/introducing-concierge-travel', + BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', SCREEN_READER_STATES: { ALL: 'all', ACTIVE: 'active', @@ -3486,6 +3497,9 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', + SHARED: 'shared', + DRAFTS: 'drafts', + FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, @@ -3518,10 +3532,12 @@ const CONST = { COLON: ':', MAPBOX: { PADDING: 50, - DEFAULT_ZOOM: 10, + DEFAULT_ZOOM: 15, SINGLE_MARKER_ZOOM: 15, DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', + ANIMATION_DURATION_ON_CENTER_ME: 1000, + CENTER_BUTTON_FADE_DURATION: 300, }, ONYX_UPDATE_TYPES: { HTTPS: 'https', @@ -3857,10 +3873,10 @@ const CONST = { type: 'meetGuide', autoCompleted: false, title: 'Meet your setup specialist', - description: ({adminsRoomLink, guideCalendarLink}: {adminsRoomLink: string; guideCalendarLink: string}) => + description: ({adminsRoomLink}: {adminsRoomLink: string}) => `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + '\n' + - `Chat with the specialist in your [#admins room](${adminsRoomLink}) or [schedule a call](${guideCalendarLink}) today.`, + `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, }, { type: 'setupCategories', @@ -4739,6 +4755,12 @@ const CONST = { INITIAL_URL: 'INITIAL_URL', }, + RESERVATION_TYPE: { + CAR: 'car', + HOTEL: 'hotel', + FLIGHT: 'flight', + }, + DOT_SEPARATOR: '•', DEFAULT_TAX: { @@ -4791,6 +4813,30 @@ const CONST = { ASC: 'asc', DESC: 'desc', }, + + SUBSCRIPTION_SIZE_LIMIT: 20000, + SUBSCRIPTION_POSSIBLE_COST_SAVINGS: { + COLLECT_PLAN: 10, + CONTROL_PLAN: 18, + }, + FEEDBACK_SURVEY_OPTIONS: { + TOO_LIMITED: { + ID: 'tooLimited', + TRANSLATION_KEY: 'feedbackSurvey.tooLimited', + }, + TOO_EXPENSIVE: { + ID: 'tooExpensive', + TRANSLATION_KEY: 'feedbackSurvey.tooExpensive', + }, + INADEQUATE_SUPPORT: { + ID: 'inadequateSupport', + TRANSLATION_KEY: 'feedbackSurvey.inadequateSupport', + }, + BUSINESS_CLOSING: { + ID: 'businessClosing', + TRANSLATION_KEY: 'feedbackSurvey.businessClosing', + }, + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 86a4a0a31716..0d22d3714fe6 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -154,6 +154,9 @@ const ONYXKEYS = { /** Whether the user has been shown the hold educational interstitial yet */ NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', + /** Store the state of the subscription */ + NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store preferred skintone for emoji */ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', @@ -394,6 +397,8 @@ const ONYXKEYS = { POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm', + POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM: 'policyDistanceRateTaxReclaimableOnEditForm', + POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM_DRAFT: 'policyDistanceRateTaxReclaimableOnEditFormDraft', POLICY_DISTANCE_RATE_EDIT_FORM_DRAFT: 'policyDistanceRateEditFormDraft', CLOSE_ACCOUNT_FORM: 'closeAccount', CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft', @@ -479,6 +484,8 @@ const ONYXKEYS = { WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft', NEW_CHAT_NAME_FORM: 'newChatNameForm', NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', + SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', + SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', }, } as const; @@ -533,9 +540,11 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm; [ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm; [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm; + [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT_FORM]: FormTypes.PolicyDistanceRateTaxReclaimableOnEditForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm; [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; + [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; }; type OnyxFormDraftValuesMapping = { @@ -621,6 +630,8 @@ type OnyxValuesMapping = { [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; + + // The value of this nvp is a string representation of the date when the block expires, or an empty string if the user is not blocked [ONYXKEYS.NVP_BLOCKED_FROM_CHAT]: string; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; @@ -640,6 +651,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; + [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 82acc26e3100..ed20d388bb87 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,13 +60,13 @@ const ROUTES = { getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const, }, CHAT_FINDER: 'chat-finder', - DETAILS: { - route: 'details', - getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` as const, - }, PROFILE: { route: 'a/:accountID', - getRoute: (accountID: string | number, backTo?: string) => getUrlWithBackToParam(`a/${accountID}`, backTo), + getRoute: (accountID?: string | number, backTo?: string, login?: string) => { + const baseRoute = getUrlWithBackToParam(`a/${accountID}`, backTo); + const loginParam = login ? `?login=${encodeURIComponent(login)}` : ''; + return `${baseRoute}${loginParam}` as const; + }, }, PROFILE_AVATAR: { route: 'a/:accountID/avatar', @@ -102,6 +102,9 @@ const ROUTES = { SETTINGS_PRONOUNS: 'settings/profile/pronouns', SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_SUBSCRIPTION: 'settings/subscription', + SETTINGS_SUBSCRIPTION_SIZE: 'settings/subscription/subscription-size', + SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD: 'settings/subscription/add-payment-card', + SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', @@ -795,6 +798,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/distance-rates/:rateID/edit', getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/edit` as const, }, + WORKSPACE_DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: { + route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-reclaimable/edit', + getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-reclaimable/edit` as const, + }, + WORKSPACE_DISTANCE_RATE_TAX_RATE_EDIT: { + 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, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1ec1462da32c..9f7277a0ad0f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -106,6 +106,9 @@ const SCREENS = { SUBSCRIPTION: { ROOT: 'Settings_Subscription', + SIZE: 'Settings_Subscription_Size', + ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', + DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', }, }, SAVE_THE_WORLD: { @@ -320,6 +323,8 @@ const SCREENS = { DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings', DISTANCE_RATE_DETAILS: 'Distance_Rate_Details', DISTANCE_RATE_EDIT: 'Distance_Rate_Edit', + DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit', + DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit', }, EDIT_REQUEST: { diff --git a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx similarity index 82% rename from src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx rename to src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx index fcbbbbd4af3f..60fa838b0577 100644 --- a/src/pages/workspace/members/WorkspaceOwnerPaymentCardCurrencyModal.tsx +++ b/src/components/AddPaymentCard/PaymentCardCurrencyModal.tsx @@ -6,9 +6,10 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; -type WorkspaceOwnerPaymentCardCurrencyModalProps = { +type PaymentCardCurrencyModalProps = { /** Whether the modal is visible */ isVisible: boolean; @@ -25,7 +26,8 @@ type WorkspaceOwnerPaymentCardCurrencyModalProps = { onClose?: () => void; }; -function WorkspaceOwnerPaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: WorkspaceOwnerPaymentCardCurrencyModalProps) { +function PaymentCardCurrencyModal({isVisible, currencies, currentCurrency = CONST.CURRENCY.USD, onCurrencyChange, onClose}: PaymentCardCurrencyModalProps) { + const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {sections} = useMemo( @@ -51,13 +53,14 @@ function WorkspaceOwnerPaymentCardCurrencyModal({isVisible, currencies, currentC onClose={() => onClose?.()} onModalHide={onClose} hideModalContentWhileAnimating + innerContainerStyle={styles.RHPNavigatorContainer(isSmallScreenWidth)} useNativeDriver > , currency?: ValueOf) => void; + submitButtonText: string; + /** Custom content to display in the footer after card form */ + footerContent?: ReactNode; + /** Custom content to display in the header before card form */ + headerContent?: ReactNode; +}; + +function IAcceptTheLabel() { + const {translate} = useLocalize(); + + return ( + + {`${translate('common.iAcceptThe')}`} + {`${translate('common.addCardTermsOfService')}`} {`${translate('common.and')}`} + {` ${translate('common.privacyPolicy')} `} + + ); +} + +const REQUIRED_FIELDS = [ + INPUT_IDS.NAME_ON_CARD, + INPUT_IDS.CARD_NUMBER, + INPUT_IDS.EXPIRATION_DATE, + INPUT_IDS.ADDRESS_STREET, + INPUT_IDS.SECURITY_CODE, + INPUT_IDS.ADDRESS_ZIP_CODE, + INPUT_IDS.ADDRESS_STATE, +]; + +const CARD_TYPES = { + DEBIT_CARD: 'debit', + PAYMENT_CARD: 'payment', +}; + +const CARD_TYPE_SECTIONS = { + DEFAULTS: 'defaults', + ERROR: 'error', +}; +type CartTypesMap = (typeof CARD_TYPES)[keyof typeof CARD_TYPES]; +type CartTypeSectionsMap = (typeof CARD_TYPE_SECTIONS)[keyof typeof CARD_TYPE_SECTIONS]; + +type CardLabels = Record>>; + +const CARD_LABELS: CardLabels = { + [CARD_TYPES.DEBIT_CARD]: { + [CARD_TYPE_SECTIONS.DEFAULTS]: { + cardNumber: 'addDebitCardPage.debitCardNumber', + nameOnCard: 'addDebitCardPage.nameOnCard', + expirationDate: 'addDebitCardPage.expirationDate', + expiration: 'addDebitCardPage.expiration', + securityCode: 'addDebitCardPage.cvv', + billingAddress: 'addDebitCardPage.billingAddress', + }, + [CARD_TYPE_SECTIONS.ERROR]: { + nameOnCard: 'addDebitCardPage.error.invalidName', + cardNumber: 'addDebitCardPage.error.debitCardNumber', + expirationDate: 'addDebitCardPage.error.expirationDate', + securityCode: 'addDebitCardPage.error.securityCode', + addressStreet: 'addDebitCardPage.error.addressStreet', + addressZipCode: 'addDebitCardPage.error.addressZipCode', + }, + }, + [CARD_TYPES.PAYMENT_CARD]: { + defaults: { + cardNumber: 'addPaymentCardPage.paymentCardNumber', + nameOnCard: 'addPaymentCardPage.nameOnCard', + expirationDate: 'addPaymentCardPage.expirationDate', + expiration: 'addPaymentCardPage.expiration', + securityCode: 'addPaymentCardPage.cvv', + billingAddress: 'addPaymentCardPage.billingAddress', + }, + error: { + nameOnCard: 'addPaymentCardPage.error.invalidName', + cardNumber: 'addPaymentCardPage.error.paymentCardNumber', + expirationDate: 'addPaymentCardPage.error.expirationDate', + securityCode: 'addPaymentCardPage.error.securityCode', + addressStreet: 'addPaymentCardPage.error.addressStreet', + addressZipCode: 'addPaymentCardPage.error.addressZipCode', + }, + }, +}; + +function PaymentCardForm({ + shouldShowPaymentCardForm, + addPaymentCard, + showAcceptTerms, + showAddressField, + showCurrencyField, + isDebitCard, + submitButtonText, + showStateSelector, + footerContent, + headerContent, +}: PaymentCardFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const route = useRoute(); + const label = CARD_LABELS[isDebitCard ? CARD_TYPES.DEBIT_CARD : CARD_TYPES.PAYMENT_CARD]; + + const cardNumberRef = useRef(null); + + const [isCurrencyModalVisible, setIsCurrencyModalVisible] = useState(false); + const [currency, setCurrency] = useState(CONST.CURRENCY.USD); + + const validate = (formValues: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(formValues, REQUIRED_FIELDS); + + if (formValues.nameOnCard && !ValidationUtils.isValidLegalName(formValues.nameOnCard)) { + errors.nameOnCard = label.error.nameOnCard; + } + + if (formValues.cardNumber && !ValidationUtils.isValidDebitCard(formValues.cardNumber.replace(/ /g, ''))) { + errors.cardNumber = label.error.cardNumber; + } + + if (formValues.expirationDate && !ValidationUtils.isValidExpirationDate(formValues.expirationDate)) { + errors.expirationDate = label.error.expirationDate; + } + + if (formValues.securityCode && !ValidationUtils.isValidSecurityCode(formValues.securityCode)) { + errors.securityCode = label.error.securityCode; + } + + if (formValues.addressStreet && !ValidationUtils.isValidAddress(formValues.addressStreet)) { + errors.addressStreet = label.error.addressStreet; + } + + if (formValues.addressZipCode && !ValidationUtils.isValidZipCode(formValues.addressZipCode)) { + errors.addressZipCode = label.error.addressZipCode; + } + + if (!formValues.acceptTerms) { + errors.acceptTerms = 'common.error.acceptTerms'; + } + + return errors; + }; + + const showCurrenciesModal = useCallback(() => { + setIsCurrencyModalVisible(true); + }, []); + + const changeCurrency = useCallback((newCurrency: keyof typeof CONST.CURRENCY) => { + setCurrency(newCurrency); + setIsCurrencyModalVisible(false); + }, []); + + if (!shouldShowPaymentCardForm) { + return null; + } + + return ( + <> + {headerContent} + addPaymentCard(formData, currency)} + submitButtonText={submitButtonText} + scrollContextEnabled + style={[styles.mh5, styles.flexGrow1]} + > + + + + + + + + + + + {!!showAddressField && ( + + + + )} + + {!!showStateSelector && ( + + + + )} + {!!showCurrencyField && ( + + {(isHovered) => ( + + )} + + )} + {!!showAcceptTerms && ( + + + + )} + + } + currentCurrency={currency} + onCurrencyChange={changeCurrency} + onClose={() => setIsCurrencyModalVisible(false)} + /> + {footerContent} + + + ); +} + +PaymentCardForm.displayName = 'PaymentCardForm'; + +export default PaymentCardForm; diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 2212e7460a2a..4c470858292c 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {useEffect, useRef} from 'react'; // eslint-disable-next-line no-restricted-imports import type {Text as RNText} from 'react-native'; diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 9713e40136a2..173f84d392e5 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -64,7 +64,7 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} return ( = { includeBase64: false, saveToPhotos: false, selectionLimit: 1, includeExtra: false, + assetRepresentationMode: 'current', }; /** @@ -212,7 +213,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s * An attachment error dialog when user selected malformed images */ const showImageCorruptionAlert = useCallback(() => { - Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedImage')); + Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); }, [translate]); /** diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index fcec07a327a0..f2325eda532d 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -37,7 +37,7 @@ function extractAttachments( const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/'); attachments.unshift({ source: tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]), - isAuthTokenRequired: Boolean(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]), + isAuthTokenRequired: !!attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE], file: {name: splittedUrl[splittedUrl.length - 1]}, duration: Number(attribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]), isReceipt: false, @@ -69,7 +69,7 @@ function extractAttachments( attachments.unshift({ reportActionID: attribs['data-id'], source, - isAuthTokenRequired: Boolean(expensifySource), + isAuthTokenRequired: !!expensifySource, file: {name: fileName}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index cee2264894a7..a7409e57f846 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {memo, useEffect, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; diff --git a/src/components/AutoEmailLink.tsx b/src/components/AutoEmailLink.tsx index e1a9bdd2794b..d64c665a020f 100644 --- a/src/components/AutoEmailLink.tsx +++ b/src/components/AutoEmailLink.tsx @@ -1,4 +1,4 @@ -import {CONST} from 'expensify-common/lib/CONST'; +import {CONST as COMMON_CONST} from 'expensify-common'; import React from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,8 +20,8 @@ function AutoEmailLink({text, style}: AutoEmailLinkProps) { const styles = useThemeStyles(); return ( - {text.split(CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => { - if (CONST.REG_EXP.EMAIL.test(str)) { + {text.split(COMMON_CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => { + if (COMMON_CONST.REG_EXP.EMAIL.test(str)) { return ( ; + /** Link message below the subtitle */ linkKey?: TranslationPaths; @@ -79,6 +82,7 @@ function BlockingView({ iconColor, title, subtitle = '', + subtitleStyle, linkKey = 'notFound.goBackHome', shouldShowLink = false, iconWidth = variables.iconSizeSuperLarge, @@ -98,7 +102,7 @@ function BlockingView({ () => ( <> {shouldShowLink ? ( @@ -111,7 +115,7 @@ function BlockingView({ ) : null} ), - [styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate], + [styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate, subtitleStyle], ); const subtitleContent = useMemo(() => { diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index b72cda0de011..88ae8d48a871 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -227,7 +227,7 @@ function Button( large && styles.buttonLargeText, success && styles.buttonSuccessText, danger && styles.buttonDangerText, - Boolean(icon) && styles.textAlignLeft, + !!icon && styles.textAlignLeft, textStyles, ]} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} @@ -329,7 +329,7 @@ function Button( ]} style={[ styles.button, - StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, Boolean(icon), Boolean(text?.length > 0), shouldShowRightIcon), + StyleUtils.getButtonStyleWithIcon(styles, small, medium, large, !!icon, !!(text?.length > 0), shouldShowRightIcon), success ? styles.buttonSuccess : undefined, danger ? styles.buttonDanger : undefined, isDisabled ? styles.buttonOpacityDisabled : undefined, diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index 51612078a88c..1403f6a7ed42 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import ConfirmContent from './ConfirmContent'; import Modal from './Modal'; +import type BaseModalProps from './Modal/types'; type ConfirmModalProps = { /** Title of the modal */ @@ -74,6 +75,9 @@ type ConfirmModalProps = { * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. * */ shouldEnableNewFocusManagement?: boolean; + + /** How to re-focus after the modal is dismissed */ + restoreFocusType?: BaseModalProps['restoreFocusType']; }; function ConfirmModal({ @@ -98,8 +102,9 @@ function ConfirmModal({ onConfirm, image, shouldEnableNewFocusManagement, + restoreFocusType, }: ConfirmModalProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); return ( @@ -109,9 +114,10 @@ function ConfirmModal({ isVisible={isVisible} shouldSetModalVisibility={shouldSetModalVisibility} onModalHide={onModalHide} - type={shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} + type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} innerContainerStyle={image ? styles.pt0 : {}} shouldEnableNewFocusManagement={shouldEnableNewFocusManagement} + restoreFocusType={restoreFocusType} > ; + /** Whether the size of the route pending icon is smaller. */ + isSmallerIcon?: boolean; + + /** Whether it should have border radius */ + shouldHaveBorderRadius?: boolean; + + /** Whether it should display the Mapbox map only when the route/coordinates exist otherwise + * it will display pending map icon */ + requireRouteToDisplayMap?: boolean; + /** Whether the map is interactable or not */ interactive?: boolean; }; -function ConfirmedRoute({mapboxAccessToken, transaction, interactive}: ConfirmedRouteProps) { +function ConfirmedRoute({mapboxAccessToken, transaction, isSmallerIcon, shouldHaveBorderRadius = true, requireRouteToDisplayMap = false, interactive}: ConfirmedRouteProps) { const {isOffline} = useNetwork(); const {route0: route} = transaction?.routes ?? {}; const waypoints = transaction?.comment?.waypoints ?? {}; const coordinates = route?.geometry?.coordinates ?? []; const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const getMarkerComponent = useCallback( (icon: IconAsset): ReactNode => ( @@ -90,7 +102,9 @@ function ConfirmedRoute({mapboxAccessToken, transaction, interactive}: Confirmed return MapboxToken.stop; }, []); - return !isOffline && Boolean(mapboxAccessToken?.token) ? ( + const shouldDisplayMap = !requireRouteToDisplayMap || !!coordinates.length; + + return !isOffline && !!mapboxAccessToken?.token && shouldDisplayMap ? ( } - style={[styles.mapView, styles.br4]} + style={[styles.mapView, shouldHaveBorderRadius && styles.br4]} waypoints={waypointMarkers} styleURL={CONST.MAPBOX.STYLE_URL} /> ) : ( - + ); } diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 02ed11afa7db..bfe4578afd0f 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; diff --git a/src/components/DatePicker/CalendarPicker/index.tsx b/src/components/DatePicker/CalendarPicker/index.tsx index 1b290aacd30d..533586d4bdbf 100644 --- a/src/components/DatePicker/CalendarPicker/index.tsx +++ b/src/components/DatePicker/CalendarPicker/index.tsx @@ -1,5 +1,5 @@ import {addMonths, endOfDay, endOfMonth, format, getYear, isSameDay, parseISO, setDate, setYear, startOfDay, startOfMonth, subMonths} from 'date-fns'; -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {View} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -235,7 +235,7 @@ function CalendarPicker({ style={themeStyles.calendarDayRoot} accessibilityLabel={day?.toString() ?? ''} tabIndex={day ? 0 : -1} - accessible={Boolean(day)} + accessible={!!day} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} > {({hovered, pressed}) => ( diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx index 765fbab03876..649e66ccefa8 100644 --- a/src/components/DeeplinkWrapper/index.website.tsx +++ b/src/components/DeeplinkWrapper/index.website.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import {useEffect, useRef, useState} from 'react'; import * as Browser from '@libs/Browser'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx index 91b8b0fc4483..0b0c3ddf27ca 100644 --- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx @@ -74,7 +74,7 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit ))} {renderAdditionalText?.()} - {Boolean(isEllipsisActive) && ( + {!!isEllipsisActive && ( {/* There is some Gap for real ellipsis so we are adding 4 `.` to cover */} diff --git a/src/components/DistanceMapView/index.android.tsx b/src/components/DistanceMapView/index.android.tsx index 168a480c6100..629b05d7bccf 100644 --- a/src/components/DistanceMapView/index.android.tsx +++ b/src/components/DistanceMapView/index.android.tsx @@ -5,6 +5,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MapView from '@components/MapView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type DistanceMapViewProps from './types'; @@ -13,6 +14,7 @@ function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { const [isMapReady, setIsMapReady] = useState(false); const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const theme = useTheme(); return ( <> @@ -33,6 +35,7 @@ function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { title={translate('distance.mapPending.title')} subtitle={isOffline ? translate('distance.mapPending.subtitle') : translate('distance.mapPending.onlineSubtitle')} shouldShowLink={false} + iconColor={theme.border} /> )} diff --git a/src/components/DragAndDrop/Provider/index.tsx b/src/components/DragAndDrop/Provider/index.tsx index dc02eea2b12c..1011fa161312 100644 --- a/src/components/DragAndDrop/Provider/index.tsx +++ b/src/components/DragAndDrop/Provider/index.tsx @@ -1,5 +1,5 @@ import {PortalHost} from '@gorhom/portal'; -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import useDragAndDrop from '@hooks/useDragAndDrop'; diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx index 40f5d242d005..bfb59dc748ab 100644 --- a/src/components/EReceipt.tsx +++ b/src/components/EReceipt.tsx @@ -36,13 +36,13 @@ function EReceipt({transaction, transactionID}: EReceiptProps) { const { amount: transactionAmount, - currency: transactionCurrency = '', + currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate, cardID: transactionCardID, } = ReportUtils.getTransactionDetails(transaction, CONST.DATE.MONTH_DAY_YEAR_FORMAT) ?? {}; const formattedAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const currency = CurrencyUtils.getCurrencySymbol(transactionCurrency); + const currency = CurrencyUtils.getCurrencySymbol(transactionCurrency ?? ''); const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount; const cardDescription = transactionCardID ? CardUtils.getCardDescription(transactionCardID) : ''; diff --git a/src/components/FeatureList.tsx b/src/components/FeatureList.tsx index 5e4ab89cf150..37b798dcd66c 100644 --- a/src/components/FeatureList.tsx +++ b/src/components/FeatureList.tsx @@ -32,11 +32,20 @@ type FeatureListProps = { /** Action to call on cta button press */ onCtaPress?: () => void; + /** Text of the secondary button button */ + secondaryButtonText?: string; + + /** Accessibility label for the secondary button */ + secondaryButtonAccessibilityLabel?: string; + + /** Action to call on secondary button press */ + onSecondaryButtonPress?: () => void; + /** A list of menuItems representing the feature list. */ menuItems: FeatureListItem[]; - /** The illustration to display in the header. Can be a JSON object representing a Lottie animation. */ - illustration: DotLottieAnimation; + /** The illustration to display in the header. Can be an image or a JSON object representing a Lottie animation. */ + illustration: DotLottieAnimation | IconAsset; /** The style passed to the illustration */ illustrationStyle?: StyleProp; @@ -57,6 +66,9 @@ function FeatureList({ ctaText = '', ctaAccessibilityLabel = '', onCtaPress, + secondaryButtonText = '', + secondaryButtonAccessibilityLabel = '', + onSecondaryButtonPress, menuItems, illustration, illustrationStyle, @@ -99,6 +111,15 @@ function FeatureList({ ))} + {secondaryButtonText && ( +