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/.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/.nvmrc b/.nvmrc index 62d44807d084..48b14e6b2b56 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.13.0 +20.14.0 diff --git a/README.md b/README.md index 29a9e9b8ffdc..6544e0e95486 100644 --- a/README.md +++ b/README.md @@ -663,7 +663,38 @@ Sometimes it might be beneficial to generate a local production version instead In order to generate a production web build, run `npm run build`, this will generate a production javascript build in the `dist/` folder. #### Local production build of the MacOS desktop app -In order to compile a production desktop build, run `npm run desktop-build`, this will generate a production app in the `dist/Mac` folder named `Chat.app`. +The commands used to compile a production or staging desktop build are `npm run desktop-build` and `npm run desktop-build-staging`, respectively. These will product an app in the `dist/Mac` folder named NewExpensify.dmg that you can install like a normal app. + +HOWEVER, by default those commands will try to notarize the build (signing it as Expensify) and publish it to the S3 bucket where it's hosted for users. In most cases you won't actually need or want to do that for your local testing. To get around that and disable those behaviors for your local build, apply the following diff: + +```diff +diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js +index e4ed685f65..4c7c1b3667 100644 +--- a/config/electronBuilder.config.js ++++ b/config/electronBuilder.config.js +@@ -42,9 +42,6 @@ module.exports = { + entitlements: 'desktop/entitlements.mac.plist', + entitlementsInherit: 'desktop/entitlements.mac.plist', + type: 'distribution', +- notarize: { +- teamId: '368M544MTT', +- }, + }, + dmg: { + title: 'New Expensify', +diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh +index 791f59d733..526306eec1 100755 +--- a/scripts/build-desktop.sh ++++ b/scripts/build-desktop.sh +@@ -35,4 +35,4 @@ npx webpack --config config/webpack/webpack.desktop.ts --env file=$ENV_FILE + title "Building Desktop App Archive Using Electron" + info "" + shift 1 +-npx electron-builder --config config/electronBuilder.config.js --publish always "$@" ++npx electron-builder --config config/electronBuilder.config.js --publish never "$@" +``` + +There may be some cases where you need to test a signed and published build, such as when testing the update flows. Instructions on setting that up can be found in [Testing Electron Auto-Update](https://github.com/Expensify/App/blob/main/desktop/README.md#testing-electron-auto-update). Good luck 🙃 #### Local production build the iOS app In order to compile a production iOS build, run `npm run ios-build`, this will generate a `Chat.ipa` in the root directory of this project. diff --git a/__mocks__/react-native-permissions.ts b/__mocks__/react-native-permissions.ts index 67b7db830d94..d98b7f32a611 100644 --- a/__mocks__/react-native-permissions.ts +++ b/__mocks__/react-native-permissions.ts @@ -35,30 +35,30 @@ const requestNotifications: jest.Mock = jest.fn((options: Record notificationOptions.includes(option)) - .reduce((acc: NotificationSettings, option: string) => ({...acc, [option]: true}), { - lockScreen: true, - notificationCenter: true, - }), + .reduce( + (acc: NotificationSettings, option: string) => { + acc[option] = true; + return acc; + }, + { + lockScreen: true, + notificationCenter: true, + }, + ), })); const checkMultiple: jest.Mock = jest.fn((permissions: string[]) => - permissions.reduce( - (acc: ResultsCollection, permission: string) => ({ - ...acc, - [permission]: RESULTS.GRANTED, - }), - {}, - ), + permissions.reduce((acc: ResultsCollection, permission: string) => { + acc[permission] = RESULTS.GRANTED; + return acc; + }, {}), ); const requestMultiple: jest.Mock = jest.fn((permissions: string[]) => - permissions.reduce( - (acc: ResultsCollection, permission: string) => ({ - ...acc, - [permission]: RESULTS.GRANTED, - }), - {}, - ), + permissions.reduce((acc: ResultsCollection, permission: string) => { + acc[permission] = RESULTS.GRANTED; + return acc; + }, {}), ); export { diff --git a/android/app/build.gradle b/android/app/build.gradle index 5fb10a8173b6..ab15f3190354 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 1001047703 - versionName "1.4.77-3" + versionCode 1001048009 + versionName "1.4.80-9" // 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/arrow-down-long.svg b/assets/images/arrow-down-long.svg new file mode 100644 index 000000000000..cbf6e7e5ad2f --- /dev/null +++ b/assets/images/arrow-down-long.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/arrow-up-long.svg b/assets/images/arrow-up-long.svg new file mode 100644 index 000000000000..13d7a0c2d67e --- /dev/null +++ b/assets/images/arrow-up-long.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/babel.config.js b/babel.config.js index 3023d37df7e0..060bc0313950 100644 --- a/babel.config.js +++ b/babel.config.js @@ -10,6 +10,9 @@ const defaultPlugins = [ '@babel/transform-runtime', '@babel/plugin-proposal-class-properties', + // This will serve to map the classes correctly in FullStory + '@fullstory/babel-plugin-annotate-react', + // We use `transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. 'transform-class-properties', diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 08a444a6b8e4..cc3e256be399 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -96,6 +96,79 @@ These steps are covered in more detail in the "testing" section below. Due to some technical constraints, Apple and Google sign-in may require additional configuration to be able to work in the development environment as expected. This document describes any additional steps for each platform. +## Show Apple / Google SSO buttons development environment + +The Apple/Google Sign In button renders differently in development mode. To prevent confusion +for developers about a possible regression, we decided to not render third party buttons in +development mode. + +To re-enable the SSO buttons in development mode, remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components: + +```diff +diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx +index 4286a26033..850f8944ca 100644 +--- a/src/pages/signin/LoginForm/BaseLoginForm.tsx ++++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx +@@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false + // for developers about possible regressions, we won't render buttons in development mode. + // For more information about these differences and how to test in development mode, + // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` +- CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( ++ ( + + `Swift Default Apps` => `URI Schemes` => `new-expensify` and select `New Expensify.app` +4. Note that a dev build of the desktop app will not work. You'll create and install a local staging build: + 1. Update `build-desktop.sh` replacing `--publish always` with `--publish never`. + 2. Run `npm run desktop-build-staging` and install the locally-generated desktop app to test. +5. (Google only) apply the following diff: + + ```diff + diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx + index 765fbab038..4318528b4c 100644 + --- a/src/components/DeeplinkWrapper/index.website.tsx + +++ b/src/components/DeeplinkWrapper/index.website.tsx + @@ -63,14 +63,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra + const isUnsupportedDeeplinkRoute = routeRegex.test(window.location.pathname); + + // Making a few checks to exit early before checking authentication status + - if ( + - !isMacOSWeb() || + - isUnsupportedDeeplinkRoute || + - hasShownPrompt || + - CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV || + - autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || + - Session.isAnonymousUser() + - ) { + + if (!isMacOSWeb() || isUnsupportedDeeplinkRoute || hasShownPrompt || autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || Session.isAnonymousUser()) { + return; + } + // We want to show the prompt immediately if the user is already authenticated. + diff --git a/src/libs/Navigation/linkingConfig/prefixes.ts b/src/libs/Navigation/linkingConfig/prefixes.ts + index ca2da6f56b..2c191598f0 100644 + --- a/src/libs/Navigation/linkingConfig/prefixes.ts + +++ b/src/libs/Navigation/linkingConfig/prefixes.ts + @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [ + 'https://www.expensify.cash', + 'https://staging.expensify.cash', + 'https://dev.new.expensify.com', + + 'http://localhost', + CONST.NEW_EXPENSIFY_URL, + CONST.STAGING_NEW_EXPENSIFY_URL, + ]; + ``` + +6. Run `npm run web` + ## Apple #### Port requirements @@ -193,57 +266,11 @@ This is required because the desktop app needs to know the address of the web ap Note that changing this value to a domain that isn't configured for use with Expensify will cause Android to break, as it is still using the real client ID, but now has an incorrect value for `redirectURI`. -#### Set Environment to something other than "Development" - -The `DeepLinkWrapper` component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". - -Within the `.env` file, set `envName` to something other than "Development", for example: - -``` -envName=Staging -``` - -Alternatively, within the `DeepLinkWrapper/index.website.js` file, you can set the `CONFIG.ENVIRONMENT` to something other than "Development". +## Google -#### Handle deep links in dev on MacOS +Unlike with Apple, to test Google Sign-In we don't need to set up any http/ssh tunnels. We can just use `localhost`. But we need to set up the web and desktop environments to use `localhost` instead of `dev.new.expensify.com` -If developing on MacOS, the development desktop app can't handle deeplinks correctly. To be able to test deeplinking back to the app, follow these steps: - -1. Create a "real" build of the desktop app, which can handle deep links, open the build folder, and install the dmg there: - -```shell -npm run desktop-build -open desktop-build -# Then double-click "NewExpensify.dmg" in Finder window -``` - -2. Even with this build, the deep link may not be handled by the correct app, as the development Electron config seems to intercept it sometimes. To manage this, install [SwiftDefaultApps](https://github.com/Lord-Kamina/SwiftDefaultApps), which adds a preference pane that can be used to configure which app should handle deep links. - -### Test the Apple / Google SSO buttons in development environment - -The Apple/Google Sign In button renders differently in development mode. To prevent confusion -for developers about a possible regression, we decided to not render third party buttons in -development mode. - -Here's how you can re-enable the SSO buttons in development mode: - -- Remove this [condition](https://github.com/Expensify/App/blob/c2a718c9100e704c89ad9564301348bc53a49777/src/pages/signin/LoginForm/BaseLoginForm.tsx#L300) so that we always render the SSO button components - ```diff - diff --git a/src/pages/signin/LoginForm/BaseLoginForm.tsx b/src/pages/signin/LoginForm/BaseLoginForm.tsx - index 4286a26033..850f8944ca 100644 - --- a/src/pages/signin/LoginForm/BaseLoginForm.tsx - +++ b/src/pages/signin/LoginForm/BaseLoginForm.tsx - @@ -288,7 +288,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false - // for developers about possible regressions, we won't render buttons in development mode. - // For more information about these differences and how to test in development mode, - // see`Expensify/App/contributingGuides/APPLE_GOOGLE_SIGNIN.md` - - CONFIG.ENVIRONMENT !== CONST.ENVIRONMENT.DEV && ( - + ( - - { + @@ -246,7 +246,7 @@ const mainWindow = (): Promise => { + let deeplinkUrl: string; + let browserWindow: BrowserWindow; + + - const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); + + const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`http://localhost:${port}`) : serve({directory: `${__dirname}/www`}); + + // Prod and staging set the icon in the electron-builder config, so only update it here for dev + if (__DEV__) { + ``` -The DeepLinkWrapper component will not handle deep links in the development environment. To be able to test deep linking, you must set the environment to something other than "Development". diff --git a/docs/Hidden/Instructions b/docs/Hidden/Instructions new file mode 100644 index 000000000000..940c7ab60d10 --- /dev/null +++ b/docs/Hidden/Instructions @@ -0,0 +1 @@ +This folder is used to house articles that should not be live articles on the helpsite. 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/chat/Expensify-Chat-For-Admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md deleted file mode 100644 index 5128484adc9d..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Admins.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Expensify Chat for Admins -description: Best Practices for Admins settings up Expensify Chat ---- - -# Overview -Expensify Chat is an incredible way to build a community and foster long-term relationships between event producers and attendees, or attendees with each other. Admins are a huge factor in the success of the connections built in Expensify Chat during the events, as they are generally the drivers of the conference schedule, and help ensure safety and respect is upheld by all attendees both on and offline. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your session attendees: -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Admin Best Practices -In order to get the most out of Expensify Chat, we created a list of best practices for admins to review in order to use the tool to its fullest capabilities. - -**During the conference:** -- At a minimum, send 3 announcements throughout the day to create awareness of any sessions, activations, contests, or parties you want to promote. -- Communicate with the Expensify Team in the #admins room if you see anything you have questions about or are unsure of to make sure we’re resolving issues together ASAP. -- As an admin, It’s up to you to help keep your conference community safe and respectful. [Flag any content for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) that does not fit your culture and values to keep chatrooms a positive experience for everyone involved. - -**After the conference:** -- The rooms will all stay open after the conference ends, so encourage speakers to keep engaging as long as the conversation is going in their session room. -- Continue sharing photos and videos from the event or anything fun in #social as part of a wrap up for everyone. -- Use the #announce room to give attendees a sneak preview of your next event. -- \ No newline at end of file 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/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 9602b864b1ac..6e35b9282d07 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.77 + 1.4.80 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.77.3 + 1.4.80.9 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c123880655b2..b4c1313c26c9 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.77 + 1.4.80 CFBundleSignature ???? CFBundleVersion - 1.4.77.3 + 1.4.80.9 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 6cc928676c80..11caa243baf5 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.77 + 1.4.80 CFBundleVersion - 1.4.77.3 + 1.4.80.9 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1ea6b65a58b7..de363c211cb3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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) @@ -2594,7 +2594,7 @@ SPEC CHECKSUMS: 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 a552305e84b4..d972d33f7c27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.80-9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.80-9", "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.76", + "@expensify/react-native-live-markdown": "0.1.70", "@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#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", + "expensify-common": "^2.0.10", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -82,7 +82,7 @@ "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", - "react-native": "0.73.5", + "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.1", @@ -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.47", + "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,14 +3558,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.76", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.76.tgz", - "integrity": "sha512-JUXiLg0Y2FJiVOfZKRgoOP1no8ThPaJ6MBc122UsW6SG53OvS12MTHfgfKHjXRH1nIGro/p9ekYb8GAzpp+kdw==", - "workspaces": [ - "parser", - "example", - "WebExample" - ], + "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==", "engines": { "node": ">= 18.0.0" }, @@ -7730,19 +7725,19 @@ } }, "node_modules/@react-native-community/cli": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.6.tgz", - "integrity": "sha512-647OSi6xBb8FbwFqX9zsJxOzu685AWtrOUWHfOkbKD+5LOpGORw+GQo0F9rWZnB68rLQyfKUZWJeaD00pGv5fw==", - "dependencies": { - "@react-native-community/cli-clean": "12.3.6", - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-doctor": "12.3.6", - "@react-native-community/cli-hermes": "12.3.6", - "@react-native-community/cli-plugin-metro": "12.3.6", - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native-community/cli-types": "12.3.6", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.2.tgz", + "integrity": "sha512-WgoUWwLDcf/G1Su2COUUVs3RzAwnV/vUTdISSpAUGgSc57mPabaAoUctKTnfYEhCnE3j02k3VtaVPwCAFRO3TQ==", + "dependencies": { + "@react-native-community/cli-clean": "12.3.2", + "@react-native-community/cli-config": "12.3.2", + "@react-native-community/cli-debugger-ui": "12.3.2", + "@react-native-community/cli-doctor": "12.3.2", + "@react-native-community/cli-hermes": "12.3.2", + "@react-native-community/cli-plugin-metro": "12.3.2", + "@react-native-community/cli-server-api": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", + "@react-native-community/cli-types": "12.3.2", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", @@ -7761,11 +7756,11 @@ } }, "node_modules/@react-native-community/cli-clean": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.6.tgz", - "integrity": "sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz", + "integrity": "sha512-90k2hCX0ddSFPT7EN7h5SZj0XZPXP0+y/++v262hssoey3nhurwF57NGWN0XAR0o9BSW7+mBfeInfabzDraO6A==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0" } @@ -7835,11 +7830,11 @@ } }, "node_modules/@react-native-community/cli-config": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.6.tgz", - "integrity": "sha512-JGWSYQ9EAK6m2v0abXwFLEfsqJ1zkhzZ4CV261QZF9MoUNB6h57a274h1MLQR9mG6Tsh38wBUuNfEPUvS1vYew==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-config/-/cli-config-12.3.2.tgz", + "integrity": "sha512-UUCzDjQgvAVL/57rL7eOuFUhd+d+6qfM7V8uOegQFeFEmSmvUUDLYoXpBa5vAK9JgQtSqMBJ1Shmwao+/oElxQ==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", @@ -7958,28 +7953,29 @@ } }, "node_modules/@react-native-community/cli-debugger-ui": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.6.tgz", - "integrity": "sha512-SjUKKsx5FmcK9G6Pb6UBFT0s9JexVStK5WInmANw75Hm7YokVvHEgtprQDz2Uvy5znX5g2ujzrkIU//T15KQzA==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-12.3.2.tgz", + "integrity": "sha512-nSWQUL+51J682DlfcC1bjkUbQbGvHCC25jpqTwHIjmmVjYCX1uHuhPSqQKgPNdvtfOkrkACxczd7kVMmetxY2Q==", "dependencies": { "serve-static": "^1.13.1" } }, "node_modules/@react-native-community/cli-doctor": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.6.tgz", - "integrity": "sha512-fvBDv2lTthfw4WOQKkdTop2PlE9GtfrlNnpjB818MhcdEnPjfQw5YaTUcnNEGsvGomdCs1MVRMgYXXwPSN6OvQ==", - "dependencies": { - "@react-native-community/cli-config": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-doctor/-/cli-doctor-12.3.2.tgz", + "integrity": "sha512-GrAabdY4qtBX49knHFvEAdLtCjkmndjTeqhYO6BhsbAeKOtspcLT/0WRgdLIaKODRa61ADNB3K5Zm4dU0QrZOg==", + "dependencies": { + "@react-native-community/cli-config": "12.3.2", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-platform-ios": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.10.0", "execa": "^5.0.0", "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", @@ -8041,6 +8037,11 @@ "node": ">=8" } }, + "node_modules/@react-native-community/cli-doctor/node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -8064,14 +8065,15 @@ } }, "node_modules/@react-native-community/cli-hermes": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.6.tgz", - "integrity": "sha512-sNGwfOCl8OAIjWCkwuLpP8NZbuO0dhDI/2W7NeOGDzIBsf4/c4MptTrULWtGIH9okVPLSPX0NnRyGQ+mSwWyuQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-hermes/-/cli-hermes-12.3.2.tgz", + "integrity": "sha512-SL6F9O8ghp4ESBFH2YAPLtIN39jdnvGBKnK4FGKpDCjtB3DnUmDsGFlH46S+GGt5M6VzfG2eeKEOKf3pZ6jUzA==", "dependencies": { - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", - "hermes-profile-transformer": "^0.0.6" + "hermes-profile-transformer": "^0.0.6", + "ip": "^1.1.5" } }, "node_modules/@react-native-community/cli-hermes/node_modules/ansi-styles": { @@ -8127,6 +8129,11 @@ "node": ">=8" } }, + "node_modules/@react-native-community/cli-hermes/node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + }, "node_modules/@react-native-community/cli-hermes/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8139,11 +8146,11 @@ } }, "node_modules/@react-native-community/cli-platform-android": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.6.tgz", - "integrity": "sha512-DeDDAB8lHpuGIAPXeeD9Qu2+/wDTFPo99c8uSW49L0hkmZJixzvvvffbGQAYk32H0TmaI7rzvzH+qzu7z3891g==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-12.3.2.tgz", + "integrity": "sha512-MZ5nO8yi/N+Fj2i9BJcJ9C/ez+9/Ir7lQt49DWRo9YDmzye66mYLr/P2l/qxsixllbbDi7BXrlLpxaEhMrDopg==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.2.4", @@ -8216,11 +8223,11 @@ } }, "node_modules/@react-native-community/cli-platform-ios": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.6.tgz", - "integrity": "sha512-3eZ0jMCkKUO58wzPWlvAPRqezVKm9EPZyaPyHbRPWU8qw7JqkvnRlWIaYDGpjCJgVW4k2hKsEursLtYKb188tg==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-ios/-/cli-platform-ios-12.3.2.tgz", + "integrity": "sha512-OcWEAbkev1IL6SUiQnM6DQdsvfsKZhRZtoBNSj9MfdmwotVZSOEZJ+IjZ1FR9ChvMWayO9ns/o8LgoQxr1ZXeg==", "dependencies": { - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-tools": "12.3.2", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", @@ -8293,17 +8300,17 @@ } }, "node_modules/@react-native-community/cli-plugin-metro": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.6.tgz", - "integrity": "sha512-3jxSBQt4fkS+KtHCPSyB5auIT+KKIrPCv9Dk14FbvOaEh9erUWEm/5PZWmtboW1z7CYeNbFMeXm9fM2xwtVOpg==" + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-12.3.2.tgz", + "integrity": "sha512-FpFBwu+d2E7KRhYPTkKvQsWb2/JKsJv+t1tcqgQkn+oByhp+qGyXBobFB8/R3yYvRRDCSDhS+atWTJzk9TjM8g==" }, "node_modules/@react-native-community/cli-server-api": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.6.tgz", - "integrity": "sha512-80NIMzo8b2W+PL0Jd7NjiJW9mgaT8Y8wsIT/lh6mAvYH7mK0ecDJUYUTAAv79Tbo1iCGPAr3T295DlVtS8s4yQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-server-api/-/cli-server-api-12.3.2.tgz", + "integrity": "sha512-iwa7EO9XFA/OjI5pPLLpI/6mFVqv8L73kNck3CNOJIUCCveGXBKK0VMyOkXaf/BYnihgQrXh+x5cxbDbggr7+Q==", "dependencies": { - "@react-native-community/cli-debugger-ui": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", + "@react-native-community/cli-debugger-ui": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", @@ -8448,9 +8455,9 @@ } }, "node_modules/@react-native-community/cli-tools": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.6.tgz", - "integrity": "sha512-FPEvZn19UTMMXUp/piwKZSh8cMEfO8G3KDtOwo53O347GTcwNrKjgZGtLSPELBX2gr+YlzEft3CoRv2Qmo83fQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-tools/-/cli-tools-12.3.2.tgz", + "integrity": "sha512-nDH7vuEicHI2TI0jac/DjT3fr977iWXRdgVAqPZFFczlbs7A8GQvEdGnZ1G8dqRUmg+kptw0e4hwczAOG89JzQ==", "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", @@ -8548,9 +8555,9 @@ } }, "node_modules/@react-native-community/cli-types": { - "version": "12.3.6", - "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.6.tgz", - "integrity": "sha512-xPqTgcUtZowQ8WKOkI9TLGBwH2bGggOC4d2FFaIRST3gTcjrEeGRNeR5aXCzJFIgItIft8sd7p2oKEdy90+01Q==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-12.3.2.tgz", + "integrity": "sha512-9D0UEFqLW8JmS16mjHJxUJWX8E+zJddrHILSH8AJHZ0NNHv4u2DXKdb0wFLMobFxGNxPT+VSOjc60fGvXzWHog==", "dependencies": { "joi": "^17.2.1" } @@ -9002,13 +9009,13 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.73.17", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.17.tgz", - "integrity": "sha512-F3PXZkcHg+1ARIr6FRQCQiB7ZAA+MQXGmq051metRscoLvgYJwj7dgC8pvgy0kexzUkHu5BNKrZeySzUft3xuQ==", + "version": "0.73.16", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.73.16.tgz", + "integrity": "sha512-eNH3v3qJJF6f0n/Dck90qfC9gVOR4coAXMTdYECO33GfgjTi+73vf/SBqlXw9HICH/RNZYGPM3wca4FRF7TYeQ==", "dependencies": { - "@react-native-community/cli-server-api": "12.3.6", - "@react-native-community/cli-tools": "12.3.6", - "@react-native/dev-middleware": "0.73.8", + "@react-native-community/cli-server-api": "12.3.2", + "@react-native-community/cli-tools": "12.3.2", + "@react-native/dev-middleware": "0.73.7", "@react-native/metro-babel-transformer": "0.73.15", "chalk": "^4.0.0", "execa": "^5.1.1", @@ -9094,9 +9101,8 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.73.8", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.73.8.tgz", - "integrity": "sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg==", + "version": "0.73.7", + "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.73.3", @@ -9107,8 +9113,7 @@ "node-fetch": "^2.2.0", "open": "^7.0.3", "serve-static": "^1.13.1", - "temp-dir": "^2.0.0", - "ws": "^6.2.2" + "temp-dir": "^2.0.0" }, "engines": { "node": ">=18" @@ -9139,14 +9144,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, "node_modules/@react-native/gradle-plugin": { "version": "0.73.4", "license": "MIT", @@ -16797,8 +16794,7 @@ }, "node_modules/colorette": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -17866,9 +17862,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debounce": { "version": "1.2.1", @@ -19369,9 +19365,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.48.tgz", - "integrity": "sha512-PFegJ9Wfsiu5tgevhjA1toCxsZ8Etfk6pIjtXAnwpmVj7q4CtB3QDRusJoUDyJ3HrZr8AsFKViz7CU/CBTfwOw==", + "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", @@ -19846,8 +19842,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" }, @@ -20339,11 +20336,11 @@ } }, "node_modules/expensify-common": { - "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", - "integrity": "sha512-AbeXop0pAVnkOJ7uVShqF7q9xwOYADW1mit0kK73ADkNuuQuHCYTqQSsQDuLaG80c5N96h+NZF/9LvcrhU2aFw==", - "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", @@ -20946,9 +20943,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz", + "integrity": "sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==", "funding": [ { "type": "github", @@ -26588,9 +26585,9 @@ } }, "node_modules/joi": { - "version": "17.13.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", - "integrity": "sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg==", + "version": "17.12.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.3.tgz", + "integrity": "sha512-2RRziagf555owrm9IRVtdKynOBeITiDpuZqIpgwqXShPncPKNiRQoiGsl/T8SQdq+8ugRzH2LqY67irr2y/d+g==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -27355,6 +27352,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/logkitty/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, "node_modules/logkitty/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -27471,6 +27476,18 @@ "node": ">=8" } }, + "node_modules/logkitty/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/longest": { "version": "1.0.1", "license": "MIT", @@ -30590,6 +30607,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/qrcode/node_modules/camelcase": { + "version": "5.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", "license": "ISC", @@ -30696,6 +30720,17 @@ "node": ">=8" } }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.10.3", "license": "BSD-3-Clause", @@ -31115,17 +31150,17 @@ } }, "node_modules/react-native": { - "version": "0.73.5", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.5.tgz", - "integrity": "sha512-iHgDArmF4CrhL0qTj+Rn+CBN5pZWUL9lUGl8ub+V9Hwu/vnzQQh8rTMVSwVd2sV6N76KjpE5a4TfIAHkpIHhKg==", + "version": "0.73.4", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.73.4.tgz", + "integrity": "sha512-VtS+Yr6OOTIuJGDECIYWzNU8QpJjASQYvMtfa/Hvm/2/h5GdB6W9H9TOmh13x07Lj4AOhNMx3XSsz6TdrO4jIg==", "dependencies": { "@jest/create-cache-key-function": "^29.6.3", - "@react-native-community/cli": "12.3.6", - "@react-native-community/cli-platform-android": "12.3.6", - "@react-native-community/cli-platform-ios": "12.3.6", + "@react-native-community/cli": "12.3.2", + "@react-native-community/cli-platform-android": "12.3.2", + "@react-native-community/cli-platform-ios": "12.3.2", "@react-native/assets-registry": "0.73.1", "@react-native/codegen": "0.73.3", - "@react-native/community-cli-plugin": "0.73.17", + "@react-native/community-cli-plugin": "0.73.16", "@react-native/gradle-plugin": "0.73.4", "@react-native/js-polyfills": "0.73.1", "@react-native/normalize-colors": "0.73.2", @@ -37908,26 +37943,6 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/yargs/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5d1244a40dab..bfd035211038 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.77-3", + "version": "1.4.80-9", "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.76", + "@expensify/react-native-live-markdown": "0.1.70", "@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#18fa764be9d68f72b48d238dcc20f2b0ca8f1147", + "expensify-common": "^2.0.10", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -134,7 +134,7 @@ "react-error-boundary": "^4.0.11", "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", - "react-native": "0.73.5", + "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.1", @@ -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.47", + "eslint-config-expensify": "^2.0.50", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -302,7 +302,7 @@ "yaml": "^2.2.1" }, "overrides": { - "react-native": "0.73.5", + "react-native": "0.73.4", "expo": "$expo", "react-native-svg": "$react-native-svg" }, @@ -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 a8101a7c64db..1ad570b4444e 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', @@ -661,9 +663,9 @@ const CONST = { DELETED_ACCOUNT: 'DELETEDACCOUNT', // OldDot Action DISMISSED_VIOLATION: 'DISMISSEDVIOLATION', DONATION: 'DONATION', // OldDot Action - EXPORTED_TO_CSV: 'EXPORTEDTOCSV', // OldDot Action - EXPORTED_TO_INTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action - EXPORTED_TO_QUICK_BOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + EXPORTED_TO_CSV: 'EXPORTCSV', // OldDot Action + EXPORTED_TO_INTEGRATION: 'EXPORTINTEGRATION', // OldDot Action + EXPORTED_TO_QUICK_BOOKS: 'EXPORTED', // OldDot Action FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', @@ -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 @@ -932,11 +935,12 @@ const CONST = { RECEIPT: 'receipt', DATE: 'date', MERCHANT: 'merchant', + DESCRIPTION: 'description', FROM: 'from', TO: 'to', CATEGORY: 'category', TAG: 'tag', - TOTAL: 'total', + TOTAL_AMOUNT: 'amount', TYPE: 'type', ACTION: 'action', TAX_AMOUNT: 'taxAmount', @@ -1185,6 +1189,10 @@ const CONST = { WEBP: 'image/webp', JPEG: 'image/jpeg', }, + ATTACHMENT_TYPE: { + REPORT: 'r', + NOTE: 'n', + }, IMAGE_OBJECT_POSITION: { TOP: 'top', @@ -1304,12 +1312,13 @@ const CONST = { SYNC: 'sync', ENABLE_NEW_CATEGORIES: 'enableNewCategories', EXPORT: 'export', + TENANT_ID: 'tenantID', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', INVOICE_STATUS: { - AWAITING_PAYMENT: 'AWT_PAYMENT', DRAFT: 'DRAFT', AWAITING_APPROVAL: 'AWT_APPROVAL', + AWAITING_PAYMENT: 'AWT_PAYMENT', }, IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', @@ -1594,6 +1603,9 @@ const CONST = { ACCOUNTANT: 'accountant', }, }, + ACCESS_VARIANTS: { + CREATE: 'create', + }, }, GROWL: { @@ -1774,7 +1786,8 @@ const CONST = { XERO: 'xero', }, SYNC_STAGE_NAME: { - STARTING_IMPORT: 'startingImport', + STARTING_IMPORT_QBO: 'startingImportQBO', + STARTING_IMPORT_XERO: 'startingImportXero', QBO_IMPORT_MAIN: 'quickbooksOnlineImportMain', QBO_IMPORT_CUSTOMERS: 'quickbooksOnlineImportCustomers', QBO_IMPORT_EMPLOYEES: 'quickbooksOnlineImportEmployees', @@ -1804,6 +1817,7 @@ const CONST = { XERO_CHECK_CONNECTION: 'xeroCheckConnection', XERO_SYNC_TITLE: 'xeroSyncTitle', }, + SYNC_STAGE_TIMEOUT_MINUTES: 20, }, ACCESS_VARIANTS: { PAID: 'paid', @@ -1887,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]*$/, @@ -1920,8 +1940,8 @@ const CONST = { // Extract attachment's source from the data's html string ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g, - EMOJI_NAME: /:[\w+-]+:/g, - EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, + EMOJI_NAME: /:[\p{L}0-9_+-]+:/gu, + EMOJI_SUGGESTIONS: /:[\p{L}0-9_+-]{1,40}$/u, AFTER_FIRST_LINE_BREAK: /\n.*/g, LINE_BREAK: /\r\n|\r|\n/g, CODE_2FA: /^\d{6}$/, @@ -2084,7 +2104,6 @@ const CONST = { INFO: 'info', }, REPORT_DETAILS_MENU_ITEM: { - SHARE_CODE: 'shareCode', MEMBERS: 'member', INVITE: 'invite', SETTINGS: 'settings', @@ -3287,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', @@ -3477,6 +3497,9 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', + SHARED: 'shared', + DRAFTS: 'drafts', + FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, @@ -3509,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', @@ -4730,6 +4755,12 @@ const CONST = { INITIAL_URL: 'INITIAL_URL', }, + RESERVATION_TYPE: { + CAR: 'car', + HOTEL: 'hotel', + FLIGHT: 'flight', + }, + DOT_SEPARATOR: '•', DEFAULT_TAX: { @@ -4777,6 +4808,13 @@ const CONST = { REFERRER: { NOTIFICATION: 'notification', }, + + SORT_ORDER: { + ASC: 'asc', + DESC: 'desc', + }, + + SUBSCRIPTION_SIZE_LIMIT: 20000, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 7a6203a44068..ddc4b5f88a69 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -269,6 +269,8 @@ function Expensify({ ); } +Expensify.displayName = 'Expensify'; + export default withOnyx({ isCheckingPublicRoom: { key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a027a8493b41..85744329d487 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -133,9 +133,6 @@ const ONYXKEYS = { /** This NVP holds to most recent waypoints that a person has used when creating a distance expense */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', - /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ - NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel', - /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'nvp_introSelected', @@ -157,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', @@ -397,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', @@ -482,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; @@ -536,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 = { @@ -589,7 +595,10 @@ type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; + + // NVP_ONBOARDING is an array for old users. [ONYXKEYS.NVP_ONBOARDING]: Onboarding | []; + [ONYXKEYS.ACTIVE_CLIENTS]: string[]; [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; @@ -628,7 +637,6 @@ type OnyxValuesMapping = { [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; - [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; @@ -641,6 +649,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 0302faf7e92b..fd03e3c19e2d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; +import type {CentralPaneNavigatorParamList} from './libs/Navigation/types'; +import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; // This is a file containing constants for all the routes we want to be able to go to @@ -35,7 +37,15 @@ const ROUTES = { SEARCH: { route: '/search/:query', - getRoute: (query: string) => `search/${query}` as const, + getRoute: (searchQuery: SearchQuery, queryParams?: CentralPaneNavigatorParamList['Search_Central_Pane']) => { + const {sortBy, sortOrder} = queryParams ?? {}; + + if (!sortBy && !sortOrder) { + return `search/${searchQuery}` as const; + } + + return `search/${searchQuery}?sortBy=${sortBy}&sortOrder=${sortOrder}` as const; + }, }, SEARCH_REPORT: { @@ -50,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', @@ -92,6 +102,8 @@ 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_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', @@ -186,7 +198,10 @@ const ROUTES = { SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', - SETTINGS_CONSOLE: 'settings/troubleshoot/console', + SETTINGS_CONSOLE: { + route: 'settings/troubleshoot/console', + getRoute: (backTo?: string) => getUrlWithBackToParam(`settings/troubleshoot/console`, backTo), + }, SETTINGS_SHARE_LOG: { route: 'settings/troubleshoot/console/share-log', getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const, @@ -238,9 +253,10 @@ const ROUTES = { route: 'r/:reportID/details/shareCode', getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, }, - REPORT_ATTACHMENTS: { - route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const, + ATTACHMENTS: { + route: 'attachment', + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number) => + `attachment?source=${encodeURIComponent(url)}&type=${type}${reportID ? `&reportID=${reportID}` : ''}${accountID ? `&accountID=${accountID}` : ''}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', @@ -266,13 +282,9 @@ const ROUTES = { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings` as const, }, - REPORT_SETTINGS_ROOM_NAME: { - route: 'r/:reportID/settings/room-name', - getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const, - }, - REPORT_SETTINGS_GROUP_NAME: { - route: 'r/:reportID/settings/group-name', - getRoute: (reportID: string) => `r/${reportID}/settings/group-name` as const, + REPORT_SETTINGS_NAME: { + route: 'r/:reportID/settings/name', + getRoute: (reportID: string) => `r/${reportID}/settings/name` as const, }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', @@ -357,6 +369,27 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, + SETTINGS_CATEGORIES_ROOT: { + route: 'settings/:policyID/categories', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories`, backTo), + }, + SETTINGS_CATEGORY_SETTINGS: { + route: 'settings/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/${encodeURIComponent(categoryName)}`, backTo), + }, + SETTINGS_CATEGORIES_SETTINGS: { + route: 'settings/:policyID/categories/settings', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/settings`, backTo), + }, + SETTINGS_CATEGORY_CREATE: { + route: 'settings/:policyID/categories/new', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/new`, backTo), + }, + SETTINGS_CATEGORY_EDIT: { + route: 'settings/:policyID/categories/:categoryName/edit', + getRoute: (policyID: string, categoryName: string, backTo = '') => + getUrlWithBackToParam(`settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit`, backTo), + }, MONEY_REQUEST_STEP_CURRENCY: { route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, pageIndex = '', currency = '', backTo = '') => @@ -647,10 +680,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, }, - WORKSPACE_MORE_FEATURES: { - route: 'settings/workspaces/:policyID/more-features', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, - }, WORKSPACE_CATEGORY_CREATE: { route: 'settings/workspaces/:policyID/categories/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const, @@ -659,6 +688,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/edit', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const, }, + WORKSPACE_MORE_FEATURES: { + route: 'settings/workspaces/:policyID/more-features', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, + }, WORKSPACE_TAGS: { route: 'settings/workspaces/:policyID/tags', getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const, @@ -676,12 +709,12 @@ const ROUTES = { getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, }, WORKSPACE_TAG_EDIT: { - route: 'settings/workspace/:policyID/tag/:tagName/edit', - getRoute: (policyID: string, tagName: string) => `settings/workspace/${policyID}/tag/${encodeURIComponent(tagName)}/edit` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/edit', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, }, WORKSPACE_TAG_SETTINGS: { - route: 'settings/workspaces/:policyID/tag/:tagName', - getRoute: (policyID: string, tagName: string) => `settings/workspaces/${policyID}/tag/${encodeURIComponent(tagName)}` as const, + route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName', + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', @@ -764,6 +797,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', @@ -799,17 +840,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories` as const, }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_COST_CENTERS: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/cost-centers', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/cost-centers` as const, - }, - POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP_REGION: { - route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/region', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/region` as const, + POLICY_ACCOUNTING_XERO_TRACKING_CATEGORIES_MAP: { + route: 'settings/workspaces/:policyID/accounting/xero/import/tracking-categories/mapping/:categoryId/:categoryName', + getRoute: (policyID: string, categoryId: string, categoryName: string) => + `settings/workspaces/${policyID}/accounting/xero/import/tracking-categories/mapping/${categoryId}/${encodeURIComponent(categoryName)}` as const, }, POLICY_ACCOUNTING_XERO_CUSTOMER: { - route: '/settings/workspaces/:policyID/accounting/xero/import/customers', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/accounting/xero/import/customers` as const, + route: 'settings/workspaces/:policyID/accounting/xero/import/customers', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/customers` as const, }, POLICY_ACCOUNTING_XERO_TAXES: { route: 'settings/workspaces/:policyID/accounting/xero/import/taxes', @@ -820,8 +858,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, }, POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: { - route: '/settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', - getRoute: (policyID: string) => `/settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, + route: 'settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, }, POLICY_ACCOUNTING_XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: { route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-date-select', @@ -835,6 +873,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, }, + POLICY_ACCOUNTING_XERO_BILL_STATUS_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-status-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/purchase-bill-status-selector` as const, + }, POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR: { route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4e7243d0eb2c..fd7418aee1c5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -7,7 +7,7 @@ import type DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', CONCIERGE: 'Concierge', - REPORT_ATTACHMENTS: 'ReportAttachments', + ATTACHMENTS: 'Attachments', } as const; const SCREENS = { @@ -106,6 +106,8 @@ const SCREENS = { SUBSCRIPTION: { ROOT: 'Settings_Subscription', + SIZE: 'Settings_Subscription_Size', + ADD_PAYMENT_CARD: 'Settings_Subscription_Add_Payment_Card', }, }, SAVE_THE_WORLD: { @@ -142,6 +144,7 @@ const SCREENS = { PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', + SETTINGS_CATEGORIES: 'SettingsCategories', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -186,10 +189,17 @@ const SCREENS = { ENABLE_PAYMENTS: 'IOU_Send_Enable_Payments', }, + SETTINGS_CATEGORIES: { + SETTINGS_CATEGORY_SETTINGS: 'Settings_Category_Settings', + SETTINGS_CATEGORIES_SETTINGS: 'Settings_Categories_Settings', + SETTINGS_CATEGORY_CREATE: 'Settings_Category_Create', + SETTINGS_CATEGORY_EDIT: 'Settings_Category_Edit', + SETTINGS_CATEGORIES_ROOT: 'Settings_Categories', + }, + REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', - ROOM_NAME: 'Report_Settings_Room_Name', - GROUP_NAME: 'Report_Settings_Group_Name', + NAME: 'Report_Settings_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', VISIBILITY: 'Report_Settings_Visibility', @@ -248,11 +258,11 @@ const SCREENS = { XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', - XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', - XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', + XERO_MAP_TRACKING_CATEGORY: 'Policy_Accounting_Xero_Map_Tracking_Category', XERO_EXPORT: 'Policy_Accounting_Xero_Export', XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: 'Policy_Accounting_Xero_Export_Purchase_Bill_Date_Select', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', + XERO_BILL_STATUS_SELECTOR: 'Policy_Accounting_Xero_Export_Bill_Status_Selector', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', @@ -312,6 +322,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/AttachmentContext.ts b/src/components/AttachmentContext.ts new file mode 100644 index 000000000000..4ed6bdc9084f --- /dev/null +++ b/src/components/AttachmentContext.ts @@ -0,0 +1,22 @@ +import {createContext} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type AttachmentContextProps = { + type?: ValueOf; + reportID?: string; + accountID?: number; +}; + +const AttachmentContext = createContext({ + type: undefined, + reportID: undefined, + accountID: undefined, +}); + +AttachmentContext.displayName = 'AttachmentContext'; + +export { + // eslint-disable-next-line import/prefer-default-export + AttachmentContext, +}; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index af7f482198bb..54a073e30567 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -1,10 +1,11 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {memo, useCallback, useEffect, useMemo, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import {useSharedValue} from 'react-native-reanimated'; +import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -101,6 +102,12 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { /** The report that has this attachment */ report?: OnyxEntry | EmptyObject; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** Optional callback to fire when we want to do something after modal show. */ onModalShow?: () => void; @@ -156,6 +163,8 @@ function AttachmentModal({ onModalClose = () => {}, isLoading = false, shouldShowNotFoundPage = false, + type = undefined, + accountID = undefined, }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -453,7 +462,7 @@ function AttachmentModal({ let shouldShowThreeDotsButton = false; if (!isEmptyObject(report)) { headerTitleNew = translate(isReceiptAttachment ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !isReceiptAttachment && !isOffline; + shouldShowDownloadButton = allowDownload && isDownloadButtonReadyToBeShown && !shouldShowNotFoundPage && !isReceiptAttachment && !isOffline; shouldShowThreeDotsButton = isReceiptAttachment && isModalOpen && threeDotsMenuItems.length !== 0; } const context = useMemo( @@ -517,35 +526,37 @@ function AttachmentModal({ onLinkPress={() => Navigation.dismissModal()} /> )} - {!isEmptyObject(report) && !isReceiptAttachment ? ( - - ) : ( - !!sourceForAttachmentView && - shouldLoadAttachment && - !isLoading && - !shouldShowNotFoundPage && ( - - - - ) - )} + {!shouldShowNotFoundPage && + (!isEmptyObject(report) && !isReceiptAttachment ? ( + + ) : ( + !!sourceForAttachmentView && + shouldLoadAttachment && + !isLoading && ( + + + + ) + ))}
{/* If we have an onConfirm method show a confirmation button */} {!!onConfirm && ( diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index f6730f4b81d9..154fcf838c86 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Alert, View} from 'react-native'; import RNFetchBlob from 'react-native-blob-util'; @@ -212,7 +212,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/extractAttachmentsFromReport.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts similarity index 82% rename from src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts rename to src/components/Attachments/AttachmentCarousel/extractAttachments.ts index d1185f88ccd5..f2325eda532d 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -1,8 +1,10 @@ import {Parser as HtmlParser} from 'htmlparser2'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import {getReport} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; @@ -10,8 +12,13 @@ import type {ReportAction, ReportActions} from '@src/types/onyx'; /** * Constructs the initial component state from report actions */ -function extractAttachmentsFromReport(parentReportAction?: OnyxEntry, reportActions?: OnyxEntry) { - const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; +function extractAttachments( + type: ValueOf, + {reportID, accountID, parentReportAction, reportActions}: {reportID?: string; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, +) { + const report = getReport(reportID); + const privateNotes = report?.privateNotes; + const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate @@ -30,7 +37,7 @@ function extractAttachmentsFromReport(parentReportAction?: OnyxEntry { if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { return; @@ -86,4 +101,4 @@ function extractAttachmentsFromReport(parentReportAction?: OnyxEntry(null); @@ -30,16 +31,21 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); + let targetAttachments: Attachment[] = []; + if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + } else { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions}); + } - const initialPage = attachmentsFromReport.findIndex(compareImage); + const initialPage = targetAttachments.findIndex(compareImage); // Dismiss the modal when deleting an attachment during its display in preview. if (initialPage === -1 && attachments.find(compareImage)) { Navigation.dismissModal(); } else { setPage(initialPage); - setAttachments(attachmentsFromReport); + setAttachments(targetAttachments); // Update the download button visibility in the parent modal if (setDownloadButtonVisibility) { @@ -47,8 +53,8 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } // Update the parent modal's state with the source and name from the mapped attachments - if (attachmentsFromReport[initialPage] !== undefined && onNavigate) { - onNavigate(attachmentsFromReport[initialPage]); + if (targetAttachments[initialPage] !== undefined && onNavigate) { + onNavigate(targetAttachments[initialPage]); } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index 23b285faf10e..947569538d32 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -21,7 +21,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import CarouselActions from './CarouselActions'; import CarouselButtons from './CarouselButtons'; import CarouselItem from './CarouselItem'; -import extractAttachmentsFromReport from './extractAttachmentsFromReport'; +import extractAttachments from './extractAttachments'; import type {AttachmentCaraouselOnyxProps, AttachmentCarouselProps, UpdatePageProps} from './types'; import useCarouselArrows from './useCarouselArrows'; @@ -33,7 +33,7 @@ const viewabilityConfig = { const MIN_FLING_VELOCITY = 500; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility}: AttachmentCarouselProps) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, type, accountID}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); @@ -57,9 +57,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions ?? undefined); + let targetAttachments: Attachment[] = []; + if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {reportID: report.reportID, accountID}); + } else { + targetAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined}); + } - if (isEqual(attachments, attachmentsFromReport)) { + if (isEqual(attachments, targetAttachments)) { if (attachments.length === 0) { setPage(-1); setDownloadButtonVisibility?.(false); @@ -67,14 +72,14 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, return; } - const initialPage = attachmentsFromReport.findIndex(compareImage); + const initialPage = targetAttachments.findIndex(compareImage); // Dismiss the modal when deleting an attachment during its display in preview. if (initialPage === -1 && attachments.find(compareImage)) { Navigation.dismissModal(); } else { setPage(initialPage); - setAttachments(attachmentsFromReport); + setAttachments(targetAttachments); // Update the download button visibility in the parent modal if (setDownloadButtonVisibility) { @@ -82,11 +87,11 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, } // Update the parent modal's state with the source and name from the mapped attachments - if (attachmentsFromReport[initialPage] !== undefined && onNavigate) { - onNavigate(attachmentsFromReport[initialPage]); + if (targetAttachments[initialPage] !== undefined && onNavigate) { + onNavigate(targetAttachments[initialPage]); } } - }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate]); + }, [reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, report.reportID, type]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts index 8ba3489a5fcf..d31ebbd328cd 100644 --- a/src/components/Attachments/AttachmentCarousel/types.ts +++ b/src/components/Attachments/AttachmentCarousel/types.ts @@ -1,6 +1,8 @@ import type {ViewToken} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; +import type CONST from '@src/CONST'; import type {Report, ReportActions} from '@src/types/onyx'; type UpdatePageProps = { @@ -28,6 +30,12 @@ type AttachmentCarouselProps = AttachmentCaraouselOnyxProps & { /** The report currently being looked at */ report: Report; + /** The type of the attachment */ + type?: ValueOf; + + /** If the attachment originates from a note, the accountID will represent the author of that note. */ + accountID?: number; + /** A callback that is called when swipe-down-to-close gesture happens */ onClose: () => void; }; 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 ( - + + + {Object.keys(parentNavigationSubtitleData).length > 0 && ( ; + /** Link message below the subtitle */ linkKey?: TranslationPaths; @@ -36,6 +40,12 @@ type BaseBlockingViewProps = { /** Render custom subtitle */ CustomSubtitle?: React.ReactElement; + + /** Determines how the image should be resized to fit its container */ + contentFitImage?: ImageContentFit; + + /** Additional styles to apply to the container */ + containerStyle?: StyleProp; }; type BlockingViewIconProps = { @@ -72,6 +82,7 @@ function BlockingView({ iconColor, title, subtitle = '', + subtitleStyle, linkKey = 'notFound.goBackHome', shouldShowLink = false, iconWidth = variables.iconSizeSuperLarge, @@ -81,6 +92,8 @@ function BlockingView({ animationStyles = [], animationWebStyle = {}, CustomSubtitle, + contentFitImage, + containerStyle, }: BlockingViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -89,7 +102,7 @@ function BlockingView({ () => ( <> {shouldShowLink ? ( @@ -102,7 +115,7 @@ function BlockingView({ ) : null} ), - [styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate], + [styles, subtitle, shouldShowLink, linkKey, onLinkPress, translate, subtitleStyle], ); const subtitleContent = useMemo(() => { @@ -117,7 +130,7 @@ function BlockingView({ }, [styles, subtitleText, shouldEmbedLinkWithSubtitle, CustomSubtitle]); return ( - + {animation && ( )} 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/CaretWrapper.tsx b/src/components/CaretWrapper.tsx new file mode 100644 index 000000000000..d34b17397670 --- /dev/null +++ b/src/components/CaretWrapper.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import {View} from 'react-native'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; + +type CaretWrapperProps = ChildrenProps; + +function CaretWrapper({children}: CaretWrapperProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + + return ( + + {children} + + + ); +} + +export default CaretWrapper; diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index e11ea53bfcf7..23a08b4306e3 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -68,6 +68,12 @@ type ConfirmModalProps = { /** Image to display with content */ image?: IconAsset; + + /** + * Whether the modal should enable the new focus manager. + * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. + * */ + shouldEnableNewFocusManagement?: boolean; }; function ConfirmModal({ @@ -91,8 +97,9 @@ function ConfirmModal({ isVisible, onConfirm, image, + shouldEnableNewFocusManagement, }: ConfirmModalProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); return ( @@ -102,8 +109,9 @@ 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} > ; + /** 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/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 7e1d81cc4071..1776a0401403 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -12,7 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; +import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session} from '@src/types/onyx'; diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx index 37fea2b957a2..10c358ad79c0 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx @@ -6,8 +6,9 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; -import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; +import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline'; import * as Link from '@userActions/Link'; +import * as PolicyAction from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import type {ConnectToQuickbooksOnlineButtonProps} from './types'; @@ -27,6 +28,8 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB setIsDisconnectModalOpen(true); return; } + // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO + PolicyAction.enablePolicyTaxes(policyID, false); Link.openLink(getQuickBooksOnlineSetupLink(policyID), environmentURL); }} isDisabled={isOffline} diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 8abe0e5759fc..bcb2a0833086 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -1,13 +1,15 @@ +import {isEmpty} from 'lodash'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {TranslationPaths} from '@src/languages/types'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import type {ConnectionName, PolicyFeatureName} from '@src/types/onyx/Policy'; import HeaderWithBackButton from './HeaderWithBackButton'; import ScreenWrapper from './ScreenWrapper'; import ScrollView from './ScrollView'; @@ -17,8 +19,8 @@ type ConnectionLayoutProps = { /** Used to set the testID for tests */ displayName: string; - /** Header title for the connection */ - headerTitle: TranslationPaths; + /** Header title to be translated for the connection component */ + headerTitle?: TranslationPaths; /** The subtitle to show in the header */ headerSubtitle?: string; @@ -26,14 +28,14 @@ type ConnectionLayoutProps = { /** React nodes that will be shown */ children?: React.ReactNode; - /** Title of the connection component */ + /** Title to be translated for the connection component */ title?: TranslationPaths; /** The current policyID */ policyID: string; /** Defines which types of access should be verified */ - accessVariants?: PolicyAccessVariant[]; + accessVariants?: AccessVariant[]; /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; @@ -44,18 +46,30 @@ type ConnectionLayoutProps = { /** Style of the title text */ titleStyle?: StyleProp | undefined; + /** Whether to include safe area padding bottom or not */ + shouldIncludeSafeAreaPaddingBottom?: boolean; + /** Whether to use ScrollView or not */ shouldUseScrollView?: boolean; + + /** Used for dynamic header title translation with parameters */ + headerTitleAlreadyTranslated?: string; + + /** Used for dynamic title translation with parameters */ + titleAlreadyTranslated?: string; + + /** Name of the current connection */ + connectionName: ConnectionName; }; -type ConnectionLayoutContentProps = Pick; +type ConnectionLayoutContentProps = Pick; -function ConnectionLayoutContent({title, titleStyle, children}: ConnectionLayoutContentProps) { +function ConnectionLayoutContent({title, titleStyle, children, titleAlreadyTranslated}: ConnectionLayoutContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); return ( <> - {title && {translate(title)}} + {title && {titleAlreadyTranslated ?? translate(title)}} {children} ); @@ -72,20 +86,28 @@ function ConnectionLayout({ featureName, contentContainerStyle, titleStyle, + shouldIncludeSafeAreaPaddingBottom, + connectionName, shouldUseScrollView = true, + headerTitleAlreadyTranslated, + titleAlreadyTranslated, }: ConnectionLayoutProps) { const {translate} = useLocalize(); + const policy = PolicyUtils.getPolicy(policyID ?? ''); + const isConnectionEmpty = isEmpty(policy.connections?.[connectionName]); + const renderSelectionContent = useMemo( () => ( {children} ), - [title, titleStyle, children], + [title, titleStyle, children, titleAlreadyTranslated], ); return ( @@ -93,14 +115,15 @@ function ConnectionLayout({ policyID={policyID} accessVariants={accessVariants} featureName={featureName} + shouldBeBlocked={isConnectionEmpty} > Navigation.goBack()} /> 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 && ( + - )} - {/** + + {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} + + + )} + {/** These are the actionable buttons that appear at the bottom of a Concierge message for example: Invite a user mentioned but not a member of the room https://github.com/Expensify/App/issues/32741 */} - {actionableItemButtons.length > 0 && ( - - )} - - ) : ( - - )} + {actionableItemButtons.length > 0 && ( + + )} + + ) : ( + + )} + ); } @@ -772,6 +784,19 @@ function ReportActionItem({ return {content}; }; + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.TRIPPREVIEW) { + if (ReportUtils.isTripRoom(report)) { + return ( + + + + ); + } + } + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { if (ReportActionsUtils.isTransactionThread(parentReportAction)) { const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction); @@ -838,6 +863,7 @@ function ReportActionItem({
); } + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { return ( @@ -939,6 +965,7 @@ function ReportActionItem({ accessible > @@ -1044,11 +1071,15 @@ export default withOnyx({ key: ({action}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${(action as OnyxTypes.OriginalMessageIOU)?.originalMessage?.IOUTransactionID ?? 0}`, selector: (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null, }, + modal: { + key: ONYXKEYS.MODAL, + }, })( memo(ReportActionItem, (prevProps, nextProps) => { const prevParentReportAction = prevProps.parentReportAction; const nextParentReportAction = nextProps.parentReportAction; return ( + prevProps.modal?.willAlertModalBecomeVisible === nextProps.modal?.willAlertModalBecomeVisible && prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && @@ -1079,7 +1110,8 @@ export default withOnyx({ lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && lodashIsEqual(prevProps.transaction, nextProps.transaction) && lodashIsEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) && - lodashIsEqual(prevParentReportAction, nextParentReportAction) + lodashIsEqual(prevParentReportAction, nextParentReportAction) && + prevProps.modal?.willAlertModalBecomeVisible === nextProps.modal?.willAlertModalBecomeVisible ); }), ); diff --git a/src/pages/home/report/ReportActionItemBasicMessage.tsx b/src/pages/home/report/ReportActionItemBasicMessage.tsx index a28f2af24448..20098933b02e 100644 --- a/src/pages/home/report/ReportActionItemBasicMessage.tsx +++ b/src/pages/home/report/ReportActionItemBasicMessage.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 6cb03e8dae05..d22ef2167c4f 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -1,8 +1,8 @@ -import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import {ExpensiMark} from 'expensify-common'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +import {InteractionManager, Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; @@ -107,11 +107,7 @@ function ReportActionItemMessageEdit( useEffect(() => { const parser = new ExpensiMark(); const originalMessage = parser.htmlToMarkdown(action.message?.[0]?.html ?? ''); - if ( - ReportActionsUtils.isDeletedAction(action) || - Boolean(action.message && draftMessage === originalMessage) || - Boolean(prevDraftMessage === draftMessage || isCommentPendingSaved.current) - ) { + if (ReportActionsUtils.isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) { return; } setDraft(draftMessage); @@ -406,7 +402,11 @@ function ReportActionItemMessageEdit( style={[styles.textInputCompose, styles.flex1, styles.bgTransparent]} onFocus={() => { setIsFocused(true); - reportScrollManager.scrollToIndex(index, true); + InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { + reportScrollManager.scrollToIndex(index, true); + }); + }); setShouldShowComposeInputKeyboardAware(false); // Clear active report action when another action gets focused diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index e97c51377476..dd4e35510ed9 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -101,7 +101,7 @@ function ReportActionItemParentAction({ {allAncestors.map((ancestor) => ( ))} - {Boolean(hasEmojiStatus) && ( + {!!hasEmojiStatus && ( Boolean(action.pendingAction)).length > 0) { + if (moneyRequestActions.filter((action) => !!action.pendingAction).length > 0) { createdAction.pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; } diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 82b49d1e260c..c537fedfe994 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -1,33 +1,40 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import type {Attachment} from '@components/Attachments/types'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type ReportAttachmentsProps = StackScreenProps; +type ReportAttachmentsProps = StackScreenProps; function ReportAttachments({route}: ReportAttachmentsProps) { const reportID = route.params.reportID; + const type = route.params.type; + const accountID = route.params.accountID; const report = ReportUtils.getReport(reportID); + const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); // In native the imported images sources are of type number. Ref: https://reactnative.dev/docs/image#imagesource const source = Number(route.params.source) || route.params.source; const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.REPORT_ATTACHMENTS.getRoute(reportID, String(attachment.source)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); Navigation.navigate(routeToNavigate); }, - [reportID], + [reportID, accountID, type], ); return ( ); } diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index ac56fe916bc9..decd8f1b8d56 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -40,6 +40,8 @@ type ReportFooterProps = ReportFooterOnyxProps & { /** Report object for the current report */ report?: OnyxTypes.Report; + reportMetadata?: OnyxEntry; + reportNameValuePairs?: OnyxEntry; /** The last report action */ @@ -72,6 +74,7 @@ function ReportFooter({ pendingAction, session, report = {reportID: '0'}, + reportMetadata, reportNameValuePairs, shouldShowComposeInput = false, isEmptyChat = true, @@ -90,7 +93,10 @@ function ReportFooter({ const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS; const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; - const hideComposer = !ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs) || blockedFromChat; + + // If a user just signed in and is viewing a public report, optimistically show the composer while loading the report, since they will have write access when the response comes back. + const showComposerOptimistically = !isAnonymousUser && ReportUtils.isPublicRoom(report) && reportMetadata?.isLoadingInitialReportActions; + const hideComposer = (!ReportUtils.canUserPerformWriteAction(report, reportNameValuePairs) && !showComposerOptimistically) || blockedFromChat; const canWriteInReport = ReportUtils.canWriteInReport(report); const isSystemChat = ReportUtils.isSystemChat(report); @@ -211,6 +217,7 @@ export default withOnyx({ prevProps.lastReportAction === nextProps.lastReportAction && prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay && - lodashIsEqual(prevProps.session, nextProps.session), + lodashIsEqual(prevProps.session, nextProps.session) && + lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata), ), ); diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index cf1ab6f8aa19..f29acf8d35fc 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; import {isEmpty} from 'lodash'; import React, {memo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.native.ts b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts index 7c5758f8720d..ac68a9e660cf 100644 --- a/src/pages/home/report/comment/shouldRenderAsText/index.native.ts +++ b/src/pages/home/report/comment/shouldRenderAsText/index.native.ts @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; /** * Whether to render the report action as text diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.ts b/src/pages/home/report/comment/shouldRenderAsText/index.ts index f26f43c528eb..7e8200671c1a 100644 --- a/src/pages/home/report/comment/shouldRenderAsText/index.ts +++ b/src/pages/home/report/comment/shouldRenderAsText/index.ts @@ -1,4 +1,4 @@ -import Str from 'expensify-common/lib/str'; +import {Str} from 'expensify-common'; /** * Whether to render the report action as text diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx index 8056d9bcc413..c06b2464f5b1 100644 --- a/src/pages/home/sidebar/AllSettingsScreen.tsx +++ b/src/pages/home/sidebar/AllSettingsScreen.tsx @@ -82,7 +82,7 @@ function AllSettingsScreen({policies}: AllSettingsScreenProps) { iconRight: item.iconRight, onPress: item.action, shouldShowRightIcon: item.shouldShowRightIcon, - shouldBlockSelection: Boolean(item.link), + shouldBlockSelection: !!item.link, wrapperStyle: styles.sectionMenuItem, isPaneMenu: true, focused: item.focused, diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 07dfa5b1b32a..f0fa320c1483 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -40,7 +40,7 @@ function BaseSidebarScreen() { {({insets}) => ( <> diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index 46bd34006550..5bbc9d22a97f 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -64,11 +64,14 @@ type MoneyRequestAmountFormProps = { /** The current tab we have navigated to in the expense modal. String that corresponds to the expense type. */ selectedTab?: SelectedTabRequest; + + /** Whether the user input should be kept or not */ + shouldKeepUserInput?: boolean; }; const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01; const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) => - isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(Math.abs(taxAmount)); + isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount)); const AMOUNT_VIEW_ID = 'amountView'; const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; @@ -88,6 +91,7 @@ function MoneyRequestAmountForm( onCurrencyButtonPress, onSubmitButtonPress, selectedTab = CONST.TAB_REQUEST.MANUAL, + shouldKeepUserInput = false, }: MoneyRequestAmountFormProps, forwardedRef: ForwardedRef, ) { @@ -144,7 +148,7 @@ function MoneyRequestAmountForm( }, [isFocused, wasFocused]); const initializeAmount = useCallback((newAmount: number) => { - const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : ''; + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : ''; moneyRequestAmountInput.current?.changeAmount(frontendAmount); moneyRequestAmountInput.current?.changeSelection({ start: frontendAmount.length, @@ -218,14 +222,9 @@ function MoneyRequestAmountForm( return; } - // Update display amount string post-edit to ensure consistency with backend amount - // Reference: https://github.com/Expensify/App/issues/30505 - const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount)); - initializeAmount(backendAmount); - onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); }, - [taxAmount, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], + [taxAmount, onSubmitButtonPress, currency, formattedTaxAmount], ); const buttonText: string = useMemo(() => { @@ -287,6 +286,7 @@ function MoneyRequestAmountForm( } textInput.current = ref; }} + shouldKeepUserInput={shouldKeepUserInput} moneyRequestAmountInputRef={moneyRequestAmountInput} inputStyle={[styles.iouAmountTextInput]} containerStyle={[styles.iouAmountTextInputContainer]} diff --git a/src/pages/iou/SplitBillDetailsPage.tsx b/src/pages/iou/SplitBillDetailsPage.tsx index 7d0b9bb15b07..130d062d4ed6 100644 --- a/src/pages/iou/SplitBillDetailsPage.tsx +++ b/src/pages/iou/SplitBillDetailsPage.tsx @@ -5,10 +5,13 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; import MoneyRequestHeaderStatusBar from '@components/MoneyRequestHeaderStatusBar'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {SplitDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -16,6 +19,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import withReportAndReportActionOrNotFound from '@pages/home/report/withReportAndReportActionOrNotFound'; import type {WithReportAndReportActionOrNotFoundProps} from '@pages/home/report/withReportAndReportActionOrNotFound'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,6 +61,7 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr const styles = useThemeStyles(); const reportID = report?.reportID ?? ''; const {translate} = useLocalize(); + const theme = useTheme(); const reportAction = useMemo(() => reportActions?.[route.params.reportActionID] ?? ({} as ReportAction), [reportActions, route.params.reportActionID]); const participantAccountIDs = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? reportAction?.originalMessage.participantAccountIDs ?? [] : []; @@ -99,12 +104,20 @@ function SplitBillDetailsPage({personalDetails, report, route, reportActions, tr {isScanning && ( - + + + } + description={translate('iou.receiptScanInProgressDescription')} + shouldStyleFlexGrow={false} + /> + )} {!!participants.length && ( { Navigation.closeRHPFlow(); }; @@ -126,15 +121,21 @@ function IOURequestStartPage({ } return ( - - {({safeAreaPaddingBottomStyle}) => ( - + + {({safeAreaPaddingBottomStyle}) => ( - {() => } + + {() => ( + + )} + {() => } {shouldDisplayDistanceRequest && {() => }} @@ -159,9 +167,9 @@ function IOURequestStartPage({ )} - - )} - + )} + + ); } diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 1d9aec1ea60d..f7507cc90c2d 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -115,7 +115,6 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic undefined, undefined, undefined, - !isCategorizeOrShareAction, isCategorizeOrShareAction ? 0 : undefined, ); @@ -254,7 +253,7 @@ function MoneyRequestParticipantsSelector({participants = [], onFinish, onPartic () => OptionsListUtils.getHeaderMessage( ((newChatOptions as Options)?.personalDetails ?? []).length + ((newChatOptions as Options)?.recentReports ?? []).length !== 0, - Boolean((newChatOptions as Options)?.userToInvite), + !!(newChatOptions as Options)?.userToInvite, debouncedSearchTerm.trim(), participants.some((participant) => participant?.searchText?.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ), diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 18dc951f949b..44e7cc58d10c 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -54,6 +54,9 @@ type IOURequestStepAmountProps = IOURequestStepAmountOnyxProps & WithWritableReportOrNotFoundProps & { /** The transaction object being modified in Onyx */ transaction: OnyxEntry; + + /** Whether the user input should be kept or not */ + shouldKeepUserInput?: boolean; }; function IOURequestStepAmount({ @@ -68,6 +71,7 @@ function IOURequestStepAmount({ splitDraftTransaction, skipConfirmation, draftTransaction, + shouldKeepUserInput = false, }: IOURequestStepAmountProps) { const {translate} = useLocalize(); const textInput = useRef(null); @@ -78,7 +82,8 @@ function IOURequestStepAmount({ const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const isEditingSplitBill = isEditing && isSplitBill; - const {amount: transactionAmount} = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction) ?? {amount: 0}; + const currentTransaction = isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction; + const {amount: transactionAmount} = ReportUtils.getTransactionDetails(currentTransaction) ?? {amount: 0}; const {currency: originalCurrency} = ReportUtils.getTransactionDetails(isEditing ? draftTransaction : transaction) ?? {currency: CONST.CURRENCY.USD}; const currency = CurrencyUtils.isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency; @@ -163,7 +168,11 @@ function IOURequestStepAmount({ const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount)); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - IOU.setMoneyRequestAmount(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD); + IOU.setMoneyRequestAmount(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, shouldKeepUserInput); + + // Initially when we're creating money request, we do not know the participant and hence if the request is with workspace with tax tracking enabled + // So, we reset the taxAmount here and calculate it in the hook in MoneyRequestConfirmationList component + IOU.setMoneyRequestTaxAmount(transactionID, null); if (backTo) { Navigation.goBack(backTo); @@ -269,25 +278,25 @@ function IOURequestStepAmount({ } // If the value hasn't changed, don't request to save changes on the server and just close the modal - const transactionCurrency = TransactionUtils.getCurrency(transaction); - if (newAmount === TransactionUtils.getAmount(transaction) && currency === transactionCurrency) { + const transactionCurrency = TransactionUtils.getCurrency(currentTransaction); + if (newAmount === TransactionUtils.getAmount(currentTransaction) && currency === transactionCurrency) { Navigation.dismissModal(); return; } + // If currency has changed, then we get the default tax rate based on currency, otherwise we use the current tax rate selected in transaction, if we have it. + const transactionTaxCode = ReportUtils.getTransactionDetails(currentTransaction)?.taxCode; + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, currentTransaction, currency) ?? ''; + const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; + const taxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, taxCode) ?? ''; + const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); + if (isSplitBill) { - IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency}); + IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount}); Navigation.goBack(backTo); return; } - // If currency has changed, then we get the default tax rate based on currency, otherwise we use the current tax rate selected in transaction, if we have it. - const transactionTaxCode = transaction?.taxCode ?? ''; - const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction, currency) ?? ''; - const taxCode = (currency !== transactionCurrency ? defaultTaxCode : transactionTaxCode) ?? defaultTaxCode; - const taxPercentage = TransactionUtils.getTaxValue(policy, transaction, taxCode) ?? ''; - const taxAmount = CurrencyUtils.convertToBackendAmount(TransactionUtils.calculateTaxAmount(taxPercentage, newAmount)); - IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode}); Navigation.dismissModal(); }; @@ -309,6 +318,7 @@ function IOURequestStepAmount({ policyID={policy?.id ?? ''} bankAccountRoute={ReportUtils.getBankAccountRoute(report)} ref={(e) => (textInput.current = e)} + shouldKeepUserInput={transaction?.shouldShowOriginalAmount} onCurrencyButtonPress={navigateToCurrencySelectionPage} onSubmitButtonPress={saveAmountAndCurrency} selectedTab={iouRequestType} diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index a38380904851..fd5c60537c38 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -1,17 +1,24 @@ import lodashIsEmpty from 'lodash/isEmpty'; import React, {useEffect} from 'react'; +import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import Button from '@components/Button'; import CategoryPicker from '@components/CategoryPicker'; +import FixedFooter from '@components/FixedFooter'; +import * as Illustrations from '@components/Icon/Illustrations'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -75,6 +82,7 @@ function IOURequestStepCategory({ const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); const isEditing = action === CONST.IOU.ACTION.EDIT; const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; @@ -92,7 +100,7 @@ function IOURequestStepCategory({ const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = !shouldShowCategory || (isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction))); + const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !ReportUtils.canEditMoneyRequest(reportAction)); const fetchData = () => { if (policy && policyCategories) { @@ -101,8 +109,9 @@ function IOURequestStepCategory({ PolicyActions.openDraftWorkspaceRequest(report?.policyID ?? ''); }; - - useNetwork({onReconnect: fetchData}); + const {isOffline} = useNetwork({onReconnect: fetchData}); + const isLoading = !isOffline && policyCategories === undefined; + const shouldShowEmptyState = !isLoading && !shouldShowCategory; useEffect(() => { fetchData(); @@ -150,14 +159,53 @@ function IOURequestStepCategory({ shouldShowWrapper shouldShowNotFoundPage={shouldShowNotFoundPage} testID={IOURequestStepCategory.displayName} - includeSafeAreaPaddingBottom={false} > - {translate('iou.categorySelection')} - + {isLoading && ( + + )} + {shouldShowEmptyState && ( + + + +