diff --git a/.eslintrc.js b/.eslintrc.js index f852c970f85c..281f8269804e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,12 +1,13 @@ const restrictedImportPaths = [ { name: 'react-native', - importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable'], + importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text'], message: [ '', "For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.", "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", "For 'StatusBar', please use 'src/libs/StatusBar' instead.", + "For 'Text', please use '@components/Text' instead.", ].join('\n'), }, { diff --git a/README.md b/README.md index 3b9010695760..f6629af8604d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ In order to have more consistent builds, we use a strict `node` and `npm` versio ## Configuring HTTPS The webpack development server now uses https. If you're using a mac, you can simply run `npm run setup-https`. -If you're using another operating system, you will need to ensure `mkcert` is installed, and then follow the instructions in the repository to generate certificates valid for `new.expesify.com.dev` and `localhost`. The certificate should be named `certificate.pem` and the key should be named `key.pem`. They should be placed in `config/webpack`. +If you're using another operating system, you will need to ensure `mkcert` is installed, and then follow the instructions in the repository to generate certificates valid for `dev.new.expensify.com` and `localhost`. The certificate should be named `certificate.pem` and the key should be named `key.pem`. They should be placed in `config/webpack`. ## Running the web app 🕸 * To run the **development web app**: `npm run web` diff --git a/android/app/build.gradle b/android/app/build.gradle index 162147aeff0c..8bf0bf928e4e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042407 - versionName "1.4.24-7" + versionCode 1001042701 + versionName "1.4.27-1" } flavorDimensions "default" diff --git a/android/app/src/main/res/drawable/ic_launcher_monochrome.png b/android/app/src/main/res/drawable/ic_launcher_monochrome.png index b1a286b6f8dd..0af99b087923 100644 Binary files a/android/app/src/main/res/drawable/ic_launcher_monochrome.png and b/android/app/src/main/res/drawable/ic_launcher_monochrome.png differ diff --git a/assets/animations/Coin.lottie b/assets/animations/Coin.lottie new file mode 100644 index 000000000000..e426f7efdc3c Binary files /dev/null and b/assets/animations/Coin.lottie differ diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg new file mode 100644 index 000000000000..047a43073b3c --- /dev/null +++ b/assets/images/chatbubble-add.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg new file mode 100644 index 000000000000..9da789510276 --- /dev/null +++ b/assets/images/chatbubble-unread.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/assets/images/expensify-logo--adhoc.svg b/assets/images/expensify-logo--adhoc.svg index 273002deca9b..52b381dc4b78 100644 --- a/assets/images/expensify-logo--adhoc.svg +++ b/assets/images/expensify-logo--adhoc.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/expensify-logo--dev.svg b/assets/images/expensify-logo--dev.svg index e8e3fb5033d9..2c9ae142e283 100644 --- a/assets/images/expensify-logo--dev.svg +++ b/assets/images/expensify-logo--dev.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/expensify-logo--staging.svg b/assets/images/expensify-logo--staging.svg index 78dcc1581f99..a1e7482c133b 100644 --- a/assets/images/expensify-logo--staging.svg +++ b/assets/images/expensify-logo--staging.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/assets/images/home-background--mobile-new.svg b/assets/images/home-background--mobile-new.svg index 0da937cae059..d81f2a18cc78 100644 --- a/assets/images/home-background--mobile-new.svg +++ b/assets/images/home-background--mobile-new.svg @@ -1,8835 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg index 38276ecd9385..7bfef1fd38b4 100644 --- a/assets/images/new-expensify.svg +++ b/assets/images/new-expensify.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/product-illustrations/payment-hands.svg b/assets/images/product-illustrations/payment-hands.svg index bf76b528ee76..2dbebd24994b 100644 --- a/assets/images/product-illustrations/payment-hands.svg +++ b/assets/images/product-illustrations/payment-hands.svg @@ -1 +1,140 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/product-illustrations/telescope.svg b/assets/images/product-illustrations/telescope.svg index 95617c801789..1830dff0fe3c 100644 --- a/assets/images/product-illustrations/telescope.svg +++ b/assets/images/product-illustrations/telescope.svg @@ -1,79 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/signIn/google-logo.svg b/assets/images/signIn/google-logo.svg index 4fbdc804a0a2..169ea34b23ee 100644 --- a/assets/images/signIn/google-logo.svg +++ b/assets/images/signIn/google-logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__bigrocket.svg b/assets/images/simple-illustrations/simple-illustration__bigrocket.svg index 1afd5f66b6ea..64d6dc2200f0 100644 --- a/assets/images/simple-illustrations/simple-illustration__bigrocket.svg +++ b/assets/images/simple-illustrations/simple-illustration__bigrocket.svg @@ -1,100 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg index 829d3ee2e3fe..ab9d3ae4db70 100644 --- a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg +++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg @@ -1,22 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__handcard.svg b/assets/images/simple-illustrations/simple-illustration__handcard.svg index 7419b33d425c..a49e0ee5b77f 100644 --- a/assets/images/simple-illustrations/simple-illustration__handcard.svg +++ b/assets/images/simple-illustrations/simple-illustration__handcard.svg @@ -1,41 +1 @@ - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg b/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg index 471b978bb97e..5b5e12a99a9b 100644 --- a/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg +++ b/assets/images/simple-illustrations/simple-illustration__hotdogstand.svg @@ -1,98 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__hourglass.svg b/assets/images/simple-illustrations/simple-illustration__hourglass.svg index 539e1e45b795..683e74a657e8 100644 --- a/assets/images/simple-illustrations/simple-illustration__hourglass.svg +++ b/assets/images/simple-illustrations/simple-illustration__hourglass.svg @@ -1,56 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__mailbox.svg b/assets/images/simple-illustrations/simple-illustration__mailbox.svg index 81b1f508fb52..7af7c71e24f3 100644 --- a/assets/images/simple-illustrations/simple-illustration__mailbox.svg +++ b/assets/images/simple-illustrations/simple-illustration__mailbox.svg @@ -1,71 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__smallrocket.svg b/assets/images/simple-illustrations/simple-illustration__smallrocket.svg index 0f8f166c849f..388bb968a762 100644 --- a/assets/images/simple-illustrations/simple-illustration__smallrocket.svg +++ b/assets/images/simple-illustrations/simple-illustration__smallrocket.svg @@ -1,45 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__trashcan.svg b/assets/images/simple-illustrations/simple-illustration__trashcan.svg index 4e66efa0a67e..66cc9ee27550 100644 --- a/assets/images/simple-illustrations/simple-illustration__trashcan.svg +++ b/assets/images/simple-illustrations/simple-illustration__trashcan.svg @@ -1,52 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/assets/images/thumbs-up.svg b/assets/images/thumbs-up.svg index ef81c88fc854..3e2a4a5125b6 100644 --- a/assets/images/thumbs-up.svg +++ b/assets/images/thumbs-up.svg @@ -1,8 +1 @@ - - - - - + \ No newline at end of file diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 6e02cae677bb..9eb16099f535 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -5,7 +5,7 @@ Welcome! Thanks for checking out the New Expensify app and taking the time to co If you would like to become an Expensify contributor, the first step is to read this document in its **entirety**. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation. #### Test Accounts -You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. +You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do not use Expensify employee or customer accounts for testing. **Notes**: diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md deleted file mode 100644 index e6d8f2fedb73..000000000000 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Free Trial -description: Learn more about your free trial with Expensify. ---- - -# Overview -New customers can take advantage of a seven-day Free Trial on a group Workspace. This trial period allows you to fully explore Expensify's features and capabilities before deciding on a subscription. -During the trial, your organization will have complete access to all the features and functionality offered by the Collect or Control workspace plan. This post provides a step-by-step guide on how to begin, oversee, and successfully conclude your organization's Expensify Free Trial. - -# How to start a Free Trial -1. Sign up for a new Expensify account at expensify.com. -2. After you've signed up for a new Expensify account, you will see a task on your Home page asking if you are using Expensify for your business or as an individual. - a. **Note**: If you select “Individual”, Expensify is free for individuals for up to 25 SmartScans per month. Selecting Individual will **not** start a Free Trial. More details on individual subscriptions can be found [here](https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription). -3. Select the Business option. -4. Select which Expensify features you'd like to set up for your organization. -5. Congratulations, your seven-day Free Trial has started! - -Once you've made these selections, we'll automatically enroll you in a Free Trial and create a Group Workspace, which will trigger new tasks on your Home page to walk you through how to configure Expensify for your organization. If you have any questions about a specific task or need assistance setting up your company, you can speak with your designated Setup Specialist by clicking “Support” on the left-hand navigation menu and selecting their name. This will allow you to message your Setup Specialist, and request a call if you need. - -# How to unlock additional Free Trial weeks -When you begin a Free Trial, you'll have an initial seven-day period before you need to provide your billing information to continue using Expensify. Luckily, Expensify offers the option to extend your Free Trial by an additional five weeks! - -To access these extra free weeks, all you need to do is complete the tasks on your Home page marked with the "Free Week!" banner. Each task completed in this category will automatically add seven more days to your trial. You can easily keep track of the remaining days of your Free Trial by checking the top right-hand corner of your Expensify Home page. - -# How to make the most of your Free Trial -- Complete all of the "Free Week!" tasks right away. These tasks are crucial for establishing your organization's Workspace, and finishing them will give you a clear idea of how much time you have left in your Free Trial. - -- Every Free Trial has dedicated access to a Setup Specialist who can help you set up your account to your preferences. We highly recommend booking a call with your dedicated Setup Specialist as soon as you start your Free Trial. If you ever need assistance with a setup task, your tasks also include demo videos. - -- Invite a few employees to join Expensify as early as possible during the Free Trial. Bringing employees on board and having them submit expenses will allow you to fully experience how all of the features and functionalities of Expensify work together to save time. We provide excellent resources to help employees get started with Expensify. - -- Establish a connection between Expensify and your accounting system from the outset. By doing this early, you can start testing Expensify comprehensively from end to end. - -{% include faq-begin.md %} -## What happens when my Free Trial ends? -If you’ve already added a billing card to Expensify, you will automatically start your organization’s Expensify subscription after your Free Trial ends. At the beginning of the following month, we'll bill the card you have on file for your subscription, adjusting the charge to exclude the Free Trial period. -If your Free Trial concludes without a billing card on file, you will see a notification on your Home page saying, 'Your Free Trial has expired.' -If you still have outstanding 'Free Week!' tasks, completing them will extend your Free Trial by additional days. -If you continue without adding a billing card, you will be granted a five-day grace period after the following billing cycle before all Group Workspace functionality is disabled. To continue using Expensify's Group Workspace features, you will need to input your billing card information and initiate a subscription. - -## How can I downgrade my account after my Free Trial ends? -If you’d like to downgrade to an individual account after your Free Trial has ended, you will need to delete any Group Workspace that you have created. This action will remove the Workspaces, subscription, and any amount owed. You can do this in one of two ways from the Expensify web app: -- Select the “Downgrade” option on the billing card task on your Home page. -- Go to **Settings > Workspaces > [Workspace name]**, then click the gear button next to the Workspace and select Delete. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 73b6c9106e4e..fb84e3484598 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -65,7 +65,7 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s ### How This Works 1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account. 2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement). -3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual credit card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. +3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. ### Example - We have card transactions for the day totaling $100, so we create the following journal entry upon sync: diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index 2dec9ae752b8..c5e8da3fae6a 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -80,7 +80,7 @@ For an efficiency-focused company, we recommend setting up [Scheduled Submit](ht 4. You’ll notice *Scheduled Submit* is located directly under *Report Basics* 5. Choose *Daily* -Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Card or scan their receipt. +Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Visa® Commercial Card or scan their receipt. Scheduled Submit will ensure all expenses are submitted automatically. Any expenses that do not fall within the rules you’ve set up for your policy will be escalated to you for manual review. @@ -155,7 +155,7 @@ The Expensify Card has many benefits for your company. Two in particular are wor ### If you don't have a corporate card, use the Expensify Card Expensify provides a corporate card with the following features: -- Up to 2% cash back (within the US) +- Up to 2% cash back (Applies to USD purchases only) - [SmartLimits](https://community.expensify.com/discussion/4851/deep-dive-what-are-unapproved-expense-limits#latest) - Unlimited Virtual Cards - single-purpose cards with a fixed or monthly limit for specific company purchases - A stable, unbreakable connection (third-party bank feeds can run into connectivity issues) diff --git a/docs/assets/images/send-money.svg b/docs/assets/images/send-money.svg index e858f0d5c327..7abce818f09e 100644 --- a/docs/assets/images/send-money.svg +++ b/docs/assets/images/send-money.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/assets/images/subscription-annual.svg b/docs/assets/images/subscription-annual.svg index a4b99a43b16e..f74ce086b2c7 100644 --- a/docs/assets/images/subscription-annual.svg +++ b/docs/assets/images/subscription-annual.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7081805db569..73ddb84be0e3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.24 + 1.4.27 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.24.7 + 1.4.27.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 20d4ea1a4820..b697d413129d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.24 + 1.4.27 CFBundleSignature ???? CFBundleVersion - 1.4.24.7 + 1.4.27.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index f941edc1100e..0a7aab285d15 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.24 + 1.4.27 CFBundleVersion - 1.4.24.7 + 1.4.27.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index acc8720dafce..f433c4f1e1e2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1176,11 +1176,11 @@ PODS: - React-Core - react-native-key-command (1.0.6): - React-Core - - react-native-netinfo (11.1.0): + - react-native-netinfo (11.2.1): - React-Core - react-native-pager-view (6.2.2): - React-Core - - react-native-pdf (6.7.4): + - react-native-pdf (6.7.3): - React-Core - react-native-performance (5.1.0): - React-Core @@ -1909,9 +1909,9 @@ SPEC CHECKSUMS: react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56 react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 - react-native-netinfo: 3aa5637c18834966e0c932de8ae1ae56fea20a97 + react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631 - react-native-pdf: 79aa75e39a80c1d45ffe58aa500f3cf08f267a2e + react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 @@ -1967,7 +1967,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/package-lock.json b/package-lock.json index 8df6ff2ebf09..c3a48044479a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.27-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.27-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -27,7 +27,7 @@ "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", - "@react-native-community/netinfo": "11.1.0", + "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", "@react-native-firebase/crashlytics": "^12.3.0", @@ -51,7 +51,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#398bf7c6a6d37f229a41d92bd7a4324c0fd32849", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", "expo": "^50.0.0-preview.7", "expo-image": "1.10.1", "fbjs": "^3.0.2", @@ -96,7 +96,7 @@ "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.118", "react-native-pager-view": "6.2.2", - "react-native-pdf": "^6.7.4", + "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", @@ -9608,9 +9608,9 @@ } }, "node_modules/@react-native-community/netinfo": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz", - "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz", + "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==", "peerDependencies": { "react-native": ">=0.59" } @@ -26153,10 +26153,9 @@ } }, "node_modules/clipboard": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", - "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", - "license": "MIT", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", "dependencies": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -28303,8 +28302,7 @@ "node_modules/delegate": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", - "license": "MIT" + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" }, "node_modules/delegates": { "version": "1.0.0", @@ -31594,37 +31592,26 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849", - "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", + "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==", "license": "MIT", "dependencies": { "classnames": "2.3.1", - "clipboard": "2.0.4", - "html-entities": "^2.3.3", + "clipboard": "2.0.11", + "html-entities": "^2.4.0", "jquery": "3.6.0", "localforage": "^1.10.0", "lodash": "4.17.21", - "prop-types": "15.7.2", + "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.3.5", + "semver": "^7.5.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "string.prototype.replaceall": "^1.0.6", + "string.prototype.replaceall": "^1.0.8", "ua-parser-js": "^1.0.35", "underscore": "1.13.6" } }, - "node_modules/expensify-common/node_modules/prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "node_modules/expensify-common/node_modules/react": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", @@ -33539,7 +33526,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", - "license": "MIT", "dependencies": { "delegate": "^3.1.2" } @@ -34284,9 +34270,19 @@ } }, "node_modules/html-entities": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", - "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] }, "node_modules/html-escaper": { "version": "2.0.2", @@ -47083,9 +47079,9 @@ } }, "node_modules/react-native-pdf": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz", - "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz", + "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==", "dependencies": { "crypto-js": "4.2.0", "deprecated-react-native-prop-types": "^2.3.0" @@ -49759,8 +49755,7 @@ "node_modules/select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", - "license": "MIT" + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, "node_modules/select-hose": { "version": "2.0.0", @@ -51347,14 +51342,15 @@ } }, "node_modules/string.prototype.replaceall": { - "version": "1.0.6", - "license": "MIT", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz", + "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", "is-regex": "^1.1.4" }, "funding": { @@ -62634,9 +62630,9 @@ "requires": {} }, "@react-native-community/netinfo": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz", - "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz", + "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==", "requires": {} }, "@react-native-firebase/analytics": { @@ -74710,9 +74706,9 @@ "dev": true }, "clipboard": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", - "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", "requires": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -78671,36 +78667,26 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849", - "integrity": "sha512-H7UrLgWIr8mCoPc1oxbeYW2RwLzUWI6jdjbV6cRnrlp8cDW3IyZISF+BQSPFDj7bMhNAbczQPtEOE1gld21Cvg==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#398bf7c6a6d37f229a41d92bd7a4324c0fd32849", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", + "integrity": "sha512-a/UBkrerB57nB9xbBrFIeJG3IN0lVZV+/JWNbGMfT0FHxtg8/4sGWdC+AHqR3Bm01gwt67dd2csFferlZmTIsg==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", "requires": { "classnames": "2.3.1", - "clipboard": "2.0.4", - "html-entities": "^2.3.3", + "clipboard": "2.0.11", + "html-entities": "^2.4.0", "jquery": "3.6.0", "localforage": "^1.10.0", "lodash": "4.17.21", - "prop-types": "15.7.2", + "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.3.5", + "semver": "^7.5.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "string.prototype.replaceall": "^1.0.6", + "string.prototype.replaceall": "^1.0.8", "ua-parser-js": "^1.0.35", "underscore": "1.13.6" }, "dependencies": { - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, "react": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", @@ -80625,9 +80611,9 @@ } }, "html-entities": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.5.tgz", - "integrity": "sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==" }, "html-escaper": { "version": "2.0.2", @@ -89732,9 +89718,9 @@ "requires": {} }, "react-native-pdf": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.4.tgz", - "integrity": "sha512-sBeNcsrTRnLjmiU9Wx7Uk0K2kPSQtKIIG+FECdrEG16TOdtmQ3iqqEwt0dmy0pJegpg07uES5BXqiKsKkRUIFw==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-6.7.3.tgz", + "integrity": "sha512-bK1fVkj18kBA5YlRFNJ3/vJ1bEX3FDHyAPY6ArtIdVs+vv0HzcK5WH9LSd2bxUsEMIyY9CSjP4j8BcxNXTiQkQ==", "requires": { "crypto-js": "4.2.0", "deprecated-react-native-prop-types": "^2.3.0" @@ -92752,13 +92738,15 @@ } }, "string.prototype.replaceall": { - "version": "1.0.6", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.8.tgz", + "integrity": "sha512-MmCXb9980obcnmbEd3guqVl6lXTxpP28zASfgAlAhlBMw5XehQeSKsdIWlAYtLxp/1GtALwex+2HyoIQtaLQwQ==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", "is-regex": "^1.1.4" } }, diff --git a/package.json b/package.json index 2494716d55f5..da993f136512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.27-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -75,7 +75,7 @@ "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", - "@react-native-community/netinfo": "11.1.0", + "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", "@react-native-firebase/crashlytics": "^12.3.0", @@ -99,7 +99,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#398bf7c6a6d37f229a41d92bd7a4324c0fd32849", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c6bb3cfa56d12af9fa02e2bfc729646f5b64ef44", "expo": "^50.0.0-preview.7", "expo-image": "1.10.1", "fbjs": "^3.0.2", @@ -144,7 +144,7 @@ "react-native-modal": "^13.0.0", "react-native-onyx": "1.0.118", "react-native-pager-view": "6.2.2", - "react-native-pdf": "^6.7.4", + "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", diff --git a/patches/react-native-blob-util+0.17.3.patch b/patches/react-native-blob-util+0.17.3.patch new file mode 100644 index 000000000000..2ade175a7b30 --- /dev/null +++ b/patches/react-native-blob-util+0.17.3.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java +index 4b41402..4f07fc6 100644 +--- a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java ++++ b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java +@@ -279,7 +279,11 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl + DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManagerId = dm.enqueue(req); + androidDownloadManagerTaskTable.put(taskId, Long.valueOf(downloadManagerId)); +- appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); ++ if(Build.VERSION.SDK_INT >= 34 ){ ++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED); ++ }else{ ++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); ++ } + future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { diff --git a/src/CONST.ts b/src/CONST.ts index b1a6b6895de7..0b10e5767328 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -479,7 +479,9 @@ const CONST = { ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', - EXPENSIFY_INBOX_URL: 'https://www.expensify.com/inbox', + OLDDOT_URLS: { + INBOX: 'inbox', + }, SIGN_IN_FORM_WIDTH: 300, @@ -527,6 +529,7 @@ const CONST = { TASKCOMPLETED: 'TASKCOMPLETED', TASKEDITED: 'TASKEDITED', TASKREOPENED: 'TASKREOPENED', + ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER', POLICYCHANGELOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', @@ -600,6 +603,10 @@ const CONST = { }, THREAD_DISABLED: ['CREATED'], }, + ACTIONABLE_MENTION_WHISPER_RESOLUTION: { + INVITE: 'invited', + NOTHING: 'nothing', + }, ARCHIVE_REASON: { DEFAULT: 'default', ACCOUNT_CLOSED: 'accountClosed', @@ -630,18 +637,13 @@ const CONST = { ANNOUNCE: '#announce', ADMINS: '#admins', }, - STATE: { - OPEN: 'OPEN', - SUBMITTED: 'SUBMITTED', - PROCESSING: 'PROCESSING', - }, STATE_NUM: { OPEN: 0, - PROCESSING: 1, - SUBMITTED: 2, + SUBMITTED: 1, + APPROVED: 2, BILLING: 3, }, - STATUS: { + STATUS_NUM: { OPEN: 0, SUBMITTED: 1, CLOSED: 2, @@ -1288,10 +1290,15 @@ const CONST = { TRIP: 'trip', MANUAL: 'manual', }, + AUTO_REPORTING_OFFSET: { + LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth', + LAST_DAY_OF_MONTH: 'lastDayOfMonth', + }, ROOM_PREFIX: '#', CUSTOM_UNIT_RATE_BASE_OFFSET: 100, OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, + ID_FAKE: '_FAKE_', }, CUSTOM_UNITS: { @@ -1440,6 +1447,8 @@ const CONST = { INVISIBLE_CHARACTERS_GROUPS: /[\p{C}\p{Z}]/gu, OTHER_INVISIBLE_CHARACTERS: /[\u3164]/g, + + REPORT_FIELD_TITLE: /{report:([a-zA-Z]+)}/g, }, PRONOUNS: { @@ -2725,7 +2734,7 @@ const CONST = { EXPECTED_OUTPUT: 'FCFA 123,457', }, - PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg'], + PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg', 'docs/index.html'], // Test tool menu parameters TEST_TOOL: { @@ -3060,7 +3069,8 @@ const CONST = { }, /** - * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll. + * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items + * rendered on every scroll. */ MAX_TO_RENDER_PER_BATCH: { DEFAULT: 5, @@ -3072,6 +3082,11 @@ const CONST = { RBR: 'RBR', }, + /** + * Constants for types of violations. + * Defined here because they need to be referenced by the type system to generate the + * ViolationNames type. + */ VIOLATIONS: { ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', @@ -3115,6 +3130,8 @@ const CONST = { EMAIL: 'EMAIL', REPORT: 'REPORT', }, + + MINI_CONTEXT_MENU_MAX_ITEMS: 4, } as const; export default CONST; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 98e3856f4544..40a43d8195de 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -461,6 +461,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7538a16d1a2c..37003a09a0cd 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -286,10 +286,6 @@ const ROUTES = { route: ':iouType/new/merchant/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const, }, - MONEY_REQUEST_WAYPOINT: { - route: ':iouType/new/waypoint/:waypointIndex', - getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` as const, - }, MONEY_REQUEST_RECEIPT: { route: ':iouType/new/receipt/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const, @@ -298,10 +294,6 @@ const ROUTES = { route: ':iouType/new/address/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const, }, - MONEY_REQUEST_EDIT_WAYPOINT: { - route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex', - getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}` as const, - }, MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const, @@ -378,9 +370,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { - route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => - getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), + route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), }, // This URL is used as a redirect to one of the create tabs below. This is so that we can message users with a link // straight to those flows without needing to have optimistic transaction and report IDs. diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.tsx similarity index 72% rename from src/components/AddressSearch/CurrentLocationButton.js rename to src/components/AddressSearch/CurrentLocationButton.tsx index 06541565f567..11bd0a64eba5 100644 --- a/src/components/AddressSearch/CurrentLocationButton.js +++ b/src/components/AddressSearch/CurrentLocationButton.tsx @@ -1,29 +1,16 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import {Text} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import colors from '@styles/theme/colors'; +import type {CurrentLocationButtonProps} from './types'; -const propTypes = { - /** Callback that runs when location button is clicked */ - onPress: PropTypes.func, - - /** Boolean to indicate if the button is clickable */ - isDisabled: PropTypes.bool, -}; - -const defaultProps = { - isDisabled: false, - onPress: () => {}, -}; - -function CurrentLocationButton({onPress, isDisabled}) { +function CurrentLocationButton({onPress, isDisabled = false}: CurrentLocationButtonProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -32,7 +19,7 @@ function CurrentLocationButton({onPress, isDisabled}) { onPress?.()} accessibilityLabel={translate('location.useCurrent')} disabled={isDisabled} onMouseDown={(e) => e.preventDefault()} @@ -48,7 +35,5 @@ function CurrentLocationButton({onPress, isDisabled}) { } CurrentLocationButton.displayName = 'CurrentLocationButton'; -CurrentLocationButton.propTypes = propTypes; -CurrentLocationButton.defaultProps = defaultProps; export default CurrentLocationButton; diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.tsx similarity index 63% rename from src/components/AddressSearch/index.js rename to src/components/AddressSearch/index.tsx index 357f5af8cb58..89e87eeebe54 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.tsx @@ -1,184 +1,79 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; -import _ from 'underscore'; +import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import LocationErrorMessage from '@components/LocationErrorMessage'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; +import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ApiUtils from '@libs/ApiUtils'; -import compose from '@libs/compose'; import getCurrentPosition from '@libs/getCurrentPosition'; +import type {GeolocationErrorCodeType} from '@libs/getCurrentPosition/getCurrentPosition.types'; import * as GooglePlacesUtils from '@libs/GooglePlacesUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import CurrentLocationButton from './CurrentLocationButton'; import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer'; +import type {AddressSearchProps, RenamedInputKeysProps} from './types'; // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the // VirtualizedList component with a VirtualizedList-backed instead LogBox.ignoreLogs(['VirtualizedLists should never be nested']); -const propTypes = { - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Hint text to display */ - hint: PropTypes.string, - - /** The label to display for the field */ - label: PropTypes.string.isRequired, - - /** The value to set the field to initially */ - value: PropTypes.string, - - /** The value to set the field to initially */ - defaultValue: PropTypes.string, - - /** A callback function when the value of this field has changed */ - onInputChange: PropTypes.func.isRequired, - - /** A callback function when an address has been auto-selected */ - onPress: PropTypes.func, - - /** Customize the TextInput container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Should address search be limited to results in the USA */ - isLimitedToUSA: PropTypes.bool, - - /** Shows a current location button in suggestion list */ - canUseCurrentLocation: PropTypes.bool, - - /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces: PropTypes.arrayOf( - PropTypes.shape({ - /** A description of the location (usually the address) */ - description: PropTypes.string, - - /** The name of the location */ - name: PropTypes.string, - - /** Data required by the google auto complete plugin to know where to put the markers on the map */ - geometry: PropTypes.shape({ - /** Data about the location */ - location: PropTypes.shape({ - /** Lattitude of the location */ - lat: PropTypes.number, - - /** Longitude of the location */ - lng: PropTypes.number, - }), - }), - }), - ), - - /** A map of inputID key names */ - renamedInputKeys: PropTypes.shape({ - street: PropTypes.string, - street2: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - lat: PropTypes.string, - lng: PropTypes.string, - zipCode: PropTypes.string, - }), - - /** Maximum number of characters allowed in search input */ - maxInputLength: PropTypes.number, - - /** The result types to return from the Google Places Autocomplete request */ - resultTypes: PropTypes.string, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Location bias for querying search results. */ - locationBias: PropTypes.string, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - inputID: undefined, - shouldSaveDraft: false, - onBlur: () => {}, - onPress: () => {}, - errorText: '', - hint: '', - value: undefined, - defaultValue: undefined, - containerStyles: [], - isLimitedToUSA: false, - canUseCurrentLocation: false, - renamedInputKeys: { - street: 'addressStreet', - street2: 'addressStreet2', - city: 'addressCity', - state: 'addressState', - zipCode: 'addressZipCode', - lat: 'addressLat', - lng: 'addressLng', - }, - maxInputLength: undefined, - predefinedPlaces: [], - resultTypes: 'address', - locationBias: undefined, -}; - -function AddressSearch({ - canUseCurrentLocation, - containerStyles, - defaultValue, - errorText, - hint, - innerRef, - inputID, - isLimitedToUSA, - label, - maxInputLength, - network, - onBlur, - onInputChange, - onPress, - predefinedPlaces, - preferredLocale, - renamedInputKeys, - resultTypes, - shouldSaveDraft, - translate, - value, - locationBias, -}) { +function AddressSearch( + { + canUseCurrentLocation = false, + containerStyles, + defaultValue, + errorText = '', + hint = '', + inputID, + isLimitedToUSA = false, + label, + maxInputLength, + onBlur, + onInputChange, + onPress, + predefinedPlaces = [], + preferredLocale, + renamedInputKeys = { + street: 'addressStreet', + street2: 'addressStreet2', + city: 'addressCity', + state: 'addressState', + zipCode: 'addressZipCode', + lat: 'addressLat', + lng: 'addressLng', + }, + resultTypes = 'address', + shouldSaveDraft = false, + value, + locationBias, + }: AddressSearchProps, + ref: ForwardedRef, +) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const [displayListViewBorder, setDisplayListViewBorder] = useState(false); const [isTyping, setIsTyping] = useState(false); const [isFocused, setIsFocused] = useState(false); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [searchValue, setSearchValue] = useState(value || defaultValue || ''); - const [locationErrorCode, setLocationErrorCode] = useState(null); + const [locationErrorCode, setLocationErrorCode] = useState(null); const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false); const shouldTriggerGeolocationCallbacks = useRef(true); - const containerRef = useRef(); + const containerRef = useRef(null); const query = useMemo( () => ({ language: preferredLocale, @@ -189,18 +84,18 @@ function AddressSearch({ [preferredLocale, resultTypes, isLimitedToUSA, locationBias], ); const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; - const saveLocationDetails = (autocompleteData, details) => { - const addressComponents = details.address_components; + const saveLocationDetails = (autocompleteData: GooglePlaceData, details: GooglePlaceDetail | null) => { + const addressComponents = details?.address_components; if (!addressComponents) { // When there are details, but no address_components, this indicates that some predefined options have been passed // to this component which don't match the usual properties coming from auto-complete. In that case, only a limited // amount of data massaging needs to happen for what the parent expects to get from this function. - if (_.size(details)) { - onPress({ - address: autocompleteData.description || lodashGet(details, 'description', ''), - lat: lodashGet(details, 'geometry.location.lat', 0), - lng: lodashGet(details, 'geometry.location.lng', 0), - name: lodashGet(details, 'name'), + if (details) { + onPress?.({ + address: autocompleteData.description ?? '', + lat: details.geometry.location.lat ?? 0, + lng: details.geometry.location.lng ?? 0, + name: details.name, }); } return; @@ -219,14 +114,19 @@ function AddressSearch({ administrative_area_level_2: stateFallback, country: countryPrimary, } = GooglePlacesUtils.getAddressComponents(addressComponents, { + // eslint-disable-next-line @typescript-eslint/naming-convention street_number: 'long_name', route: 'long_name', subpremise: 'long_name', locality: 'long_name', sublocality: 'long_name', + // eslint-disable-next-line @typescript-eslint/naming-convention postal_town: 'long_name', + // eslint-disable-next-line @typescript-eslint/naming-convention postal_code: 'long_name', + // eslint-disable-next-line @typescript-eslint/naming-convention administrative_area_level_1: 'short_name', + // eslint-disable-next-line @typescript-eslint/naming-convention administrative_area_level_2: 'long_name', country: 'short_name', }); @@ -234,6 +134,7 @@ function AddressSearch({ // The state's iso code (short_name) is needed for the StatePicker component but we also // need the state's full name (long_name) when we render the state in a TextInput. const {administrative_area_level_1: longStateName} = GooglePlacesUtils.getAddressComponents(addressComponents, { + // eslint-disable-next-line @typescript-eslint/naming-convention administrative_area_level_1: 'long_name', }); @@ -243,15 +144,16 @@ function AddressSearch({ country: countryFallbackLongName = '', state: stateAutoCompleteFallback = '', city: cityAutocompleteFallback = '', - } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData.terms); + } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); - const countryFallback = _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryFallbackLongName); + const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName); - const country = countryPrimary || countryFallback; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const country = countryPrimary || countryFallback || ''; const values = { street: `${streetNumber} ${streetName}`.trim(), - name: lodashGet(details, 'name', ''), + name: details.name ?? '', // Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise. street2: subpremise, // Make sure country is updated first, since city and state will be reset if the country changes @@ -264,9 +166,9 @@ function AddressSearch({ city: locality || postalTown || sublocality || cityAutocompleteFallback, zipCode, - lat: lodashGet(details, 'geometry.location.lat', 0), - lng: lodashGet(details, 'geometry.location.lng', 0), - address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), + lat: details.geometry.location.lat ?? 0, + lng: details.geometry.location.lng ?? 0, + address: autocompleteData.description || details.formatted_address || '', }; // If the address is not in the US, use the full length state name since we're displaying the address's @@ -282,7 +184,7 @@ function AddressSearch({ } // Set the state to be the same as the city in case the state is empty. - if (_.isEmpty(values.state)) { + if (!values.state) { values.state = values.city; } @@ -290,8 +192,8 @@ function AddressSearch({ // We are setting up a fallback to ensure "values.street" is populated with a relevant value if (!values.street && details.adr_address) { const streetAddressRegex = /([^<]*)<\/span>/; - const adr_address = details.adr_address.match(streetAddressRegex); - const streetAddressFallback = lodashGet(adr_address, [1], null); + const adrAddress = details.adr_address.match(streetAddressRegex); + const streetAddressFallback = adrAddress ? adrAddress?.[1] : null; if (streetAddressFallback) { values.street = streetAddressFallback; } @@ -299,28 +201,28 @@ function AddressSearch({ // Not all pages define the Address Line 2 field, so in that case we append any additional address details // (e.g. Apt #) to Address Line 1 - if (subpremise && typeof renamedInputKeys.street2 === 'undefined') { + if (subpremise && typeof renamedInputKeys?.street2 === 'undefined') { values.street += `, ${subpremise}`; } - const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country); + const isValidCountryCode = !!Object.keys(CONST.ALL_COUNTRIES).find((foundCountry) => foundCountry === country); if (isValidCountryCode) { values.country = country; } if (inputID) { - _.each(values, (inputValue, key) => { - const inputKey = lodashGet(renamedInputKeys, key, key); + Object.entries(values).forEach(([key, inputValue]) => { + const inputKey = renamedInputKeys?.[key as keyof RenamedInputKeysProps] ?? key; if (!inputKey) { return; } - onInputChange(inputValue, inputKey); + onInputChange?.(inputValue, inputKey); }); } else { - onInputChange(values); + onInputChange?.(values); } - onPress(values); + onPress?.(values); }; /** Gets the user's current location and registers success/error callbacks */ @@ -351,7 +253,7 @@ function AddressSearch({ address: CONST.YOUR_LOCATION_TEXT, name: CONST.YOUR_LOCATION_TEXT, }; - onPress(location); + onPress?.(location); }, (errorData) => { if (!shouldTriggerGeolocationCallbacks.current) { @@ -368,19 +270,22 @@ function AddressSearch({ ); }; - const renderHeaderComponent = () => - predefinedPlaces.length > 0 && ( - <> - {/* This will show current location button in list if there are some recent destinations */} - {shouldShowCurrentLocationButton && ( - - )} - {!value && {translate('common.recentDestinations')}} - - ); + const renderHeaderComponent = () => ( + <> + {predefinedPlaces.length > 0 && ( + <> + {/* This will show current location button in list if there are some recent destinations */} + {shouldShowCurrentLocationButton && ( + + )} + {!value && {translate('common.recentDestinations')}} + + )} + + ); // eslint-disable-next-line arrow-body-style useEffect(() => { @@ -392,10 +297,8 @@ function AddressSearch({ const listEmptyComponent = useCallback( () => - network.isOffline || !isTyping ? null : ( - {translate('common.noResultsFound')} - ), - [network.isOffline, isTyping, styles, translate], + !!isOffline || !isTyping ? null : {translate('common.noResultsFound')}, + [isOffline, isTyping, styles, translate], ); const listLoader = useCallback( @@ -464,27 +367,15 @@ function AddressSearch({ query={query} requestUrl={{ useOnPlatform: 'all', - url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), + url: isOffline ? '' : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}), }} textInputProps={{ InputComp: TextInput, - ref: (node) => { - if (!innerRef) { - return; - } - - if (_.isFunction(innerRef)) { - innerRef(node); - return; - } - - // eslint-disable-next-line no-param-reassign - innerRef.current = node; - }, + ref, label, containerStyles, errorText, - hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint, + hint: displayListViewBorder || (predefinedPlaces?.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint, value, defaultValue, inputID, @@ -498,20 +389,19 @@ function AddressSearch({ setIsFocused(false); setIsTyping(false); } - onBlur(); + onBlur?.(); }, autoComplete: 'off', - onInputChange: (text) => { + onInputChange: (text: string) => { setSearchValue(text); setIsTyping(true); if (inputID) { - onInputChange(text); + onInputChange?.(text); } else { onInputChange({street: text}); } - // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering - if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) { + if (!text && !predefinedPlaces.length) { setDisplayListViewBorder(false); } }, @@ -530,22 +420,21 @@ function AddressSearch({ isRowScrollable={false} listHoverColor={theme.border} listUnderlayColor={theme.buttonPressedBG} - onLayout={(event) => { + onLayout={(event: LayoutChangeEvent) => { // We use the height of the element to determine if we should hide the border of the listView dropdown // to prevent a lingering border when there are no address suggestions. setDisplayListViewBorder(event.nativeEvent.layout.height > variables.googleEmptyListViewHeight); }} inbetweenCompo={ // We want to show the current location button even if there are no recent destinations - predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? ( + predefinedPlaces?.length === 0 && + shouldShowCurrentLocationButton && ( - ) : ( - <> ) } placeholder="" @@ -561,18 +450,6 @@ function AddressSearch({ ); } -AddressSearch.propTypes = propTypes; -AddressSearch.defaultProps = defaultProps; -AddressSearch.displayName = 'AddressSearch'; - -const AddressSearchWithRef = React.forwardRef((props, ref) => ( - -)); - -AddressSearchWithRef.displayName = 'AddressSearchWithRef'; +AddressSearch.displayName = 'AddressSearchWithRef'; -export default compose(withNetwork(), withLocalize)(AddressSearchWithRef); +export default forwardRef(AddressSearch); diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.js deleted file mode 100644 index 18bfc10a8dcb..000000000000 --- a/src/components/AddressSearch/isCurrentTargetInsideContainer.js +++ /dev/null @@ -1,8 +0,0 @@ -function isCurrentTargetInsideContainer(event, containerRef) { - // The related target check is required here - // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false - // it will make the auto complete component re-render before onPress is called making selecting an option not working. - return containerRef.current && event.target && containerRef.current.contains(event.relatedTarget); -} - -export default isCurrentTargetInsideContainer; diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js deleted file mode 100644 index dbf0004b08d9..000000000000 --- a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js +++ /dev/null @@ -1,6 +0,0 @@ -function isCurrentTargetInsideContainer() { - // The related target check is not required here because in native there is no race condition rendering like on the web - return false; -} - -export default isCurrentTargetInsideContainer; diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts new file mode 100644 index 000000000000..b53b9e3ddec0 --- /dev/null +++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts @@ -0,0 +1,6 @@ +import type {IsCurrentTargetInsideContainerType} from './types'; + +// The related target check is not required here because in native there is no race condition rendering like on the web +const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = () => false; + +export default isCurrentTargetInsideContainer; diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts new file mode 100644 index 000000000000..a50eb747b400 --- /dev/null +++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts @@ -0,0 +1,14 @@ +import type {IsCurrentTargetInsideContainerType} from './types'; + +const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = (event, containerRef) => { + // The related target check is required here + // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false + // it will make the auto complete component re-render before onPress is called making selecting an option not working. + if (!containerRef.current || !event.target || !('relatedTarget' in event) || !('contains' in containerRef.current)) { + return false; + } + + return !!containerRef.current.contains(event.relatedTarget as Node); +}; + +export default isCurrentTargetInsideContainer; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts new file mode 100644 index 000000000000..8016f1b2ea39 --- /dev/null +++ b/src/components/AddressSearch/types.ts @@ -0,0 +1,96 @@ +import type {RefObject} from 'react'; +import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; +import type {Place} from 'react-native-google-places-autocomplete'; +import type Locale from '@src/types/onyx/Locale'; + +type CurrentLocationButtonProps = { + /** Callback that is called when the button is clicked */ + onPress?: () => void; + + /** Boolean to indicate if the button is clickable */ + isDisabled?: boolean; +}; + +type RenamedInputKeysProps = { + street: string; + street2: string; + city: string; + state: string; + lat: string; + lng: string; + zipCode: string; +}; + +type OnPressProps = { + address: string; + lat: number; + lng: number; + name: string; +}; + +type StreetValue = { + street: string; +}; + +type AddressSearchProps = { + /** The ID used to uniquely identify the input in a Form */ + inputID?: string; + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft?: boolean; + + /** Callback that is called when the text input is blurred */ + onBlur?: () => void; + + /** Error text to display */ + errorText?: string; + + /** Hint text to display */ + hint?: string; + + /** The label to display for the field */ + label: string; + + /** The value to set the field to initially */ + value?: string; + + /** The value to set the field to initially */ + defaultValue?: string; + + /** A callback function when the value of this field has changed */ + onInputChange: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void; + + /** A callback function when an address has been auto-selected */ + onPress?: (props: OnPressProps) => void; + + /** Customize the TextInput container */ + containerStyles?: StyleProp; + + /** Should address search be limited to results in the USA */ + isLimitedToUSA?: boolean; + + /** Shows a current location button in suggestion list */ + canUseCurrentLocation?: boolean; + + /** A list of predefined places that can be shown when the user isn't searching for something */ + predefinedPlaces?: Place[]; + + /** A map of inputID key names */ + renamedInputKeys: RenamedInputKeysProps; + + /** Maximum number of characters allowed in search input */ + maxInputLength?: number; + + /** The result types to return from the Google Places Autocomplete request */ + resultTypes?: string; + + /** Location bias for querying search results. */ + locationBias?: string; + + /** The user's preferred locale e.g. 'en', 'es-ES' */ + preferredLocale?: Locale; +}; + +type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; + +export type {CurrentLocationButtonProps, AddressSearchProps, RenamedInputKeysProps, IsCurrentTargetInsideContainerType}; diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx similarity index 52% rename from src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js rename to src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 6161ba140726..df8a0a30b129 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -1,6 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import AttachmentView from '@components/Attachments/AttachmentView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; @@ -10,59 +10,52 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as Download from '@userActions/Download'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps as anchorForAttachmentsOnlyDefaultProps, propTypes as anchorForAttachmentsOnlyPropTypes} from './anchorForAttachmentsOnlyPropTypes'; - -const propTypes = { - /** Press in handler for the link */ - onPressIn: PropTypes.func, - - /** Press out handler for the link */ - onPressOut: PropTypes.func, +import type {Download as OnyxDownload} from '@src/types/onyx'; +import type AnchorForAttachmentsOnlyProps from './types'; +type BaseAnchorForAttachmentsOnlyOnyxProps = { /** If a file download is happening */ - download: PropTypes.shape({ - isDownloading: PropTypes.bool.isRequired, - }), - - ...anchorForAttachmentsOnlyPropTypes, + download: OnyxEntry; }; -const defaultProps = { - onPressIn: undefined, - onPressOut: undefined, - download: {isDownloading: false}, - ...anchorForAttachmentsOnlyDefaultProps, -}; +type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & + BaseAnchorForAttachmentsOnlyOnyxProps & { + /** Press in handler for the link */ + onPressIn?: () => void; + + /** Press out handler for the link */ + onPressOut?: () => void; + }; -function BaseAnchorForAttachmentsOnly(props) { - const sourceURL = props.source; - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL); - const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1]; - const fileName = props.displayName; +function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', download, onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { + const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); + const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - const isDownloading = props.download && props.download.isDownloading; + const isDownloading = download?.isDownloading ?? false; return ( {({anchor, report, action, checkIfContextMenuActive}) => ( { if (isDownloading) { return; } Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, fileName).then(() => Download.setDownload(sourceID, false)); + fileDownload(sourceURLWithAuth, displayName).then(() => Download.setDownload(sourceID, false)); }} - onPressIn={props.onPressIn} - onPressOut={props.onPressOut} + onPressIn={onPressIn} + onPressOut={onPressOut} + // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript. onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} - accessibilityLabel={fileName} + accessibilityLabel={displayName} role={CONST.ROLE.BUTTON} > @@ -73,13 +66,11 @@ function BaseAnchorForAttachmentsOnly(props) { } BaseAnchorForAttachmentsOnly.displayName = 'BaseAnchorForAttachmentsOnly'; -BaseAnchorForAttachmentsOnly.propTypes = propTypes; -BaseAnchorForAttachmentsOnly.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ download: { key: ({source}) => { - const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) || [])[1]; + const sourceID = (source?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; return `${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`; }, }, diff --git a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js b/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js deleted file mode 100644 index 9452e615d31c..000000000000 --- a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import stylePropTypes from '@styles/stylePropTypes'; - -const propTypes = { - /** The URL of the attachment */ - source: PropTypes.string, - - /** Filename for attachments. */ - displayName: PropTypes.string, - - /** Any additional styles to apply */ - style: stylePropTypes, -}; - -const defaultProps = { - source: '', - style: {}, - displayName: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/components/AnchorForAttachmentsOnly/index.native.js b/src/components/AnchorForAttachmentsOnly/index.native.tsx similarity index 62% rename from src/components/AnchorForAttachmentsOnly/index.native.js rename to src/components/AnchorForAttachmentsOnly/index.native.tsx index 3277d51ec058..2e0e94bc0b88 100644 --- a/src/components/AnchorForAttachmentsOnly/index.native.js +++ b/src/components/AnchorForAttachmentsOnly/index.native.tsx @@ -1,9 +1,9 @@ import React from 'react'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly'; +import type AnchorForAttachmentsOnlyProps from './types'; -function AnchorForAttachmentsOnly(props) { +function AnchorForAttachmentsOnly(props: AnchorForAttachmentsOnlyProps) { const styles = useThemeStyles(); return ( ; +}; + +export default AnchorForAttachmentsOnlyProps; diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index bb3792f59d9f..99a0ee3bf683 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useEffect, useRef} from 'react'; +// eslint-disable-next-line no-restricted-imports import type {Text as RNText} from 'react-native'; import {StyleSheet} from 'react-native'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index ad79e316baf3..04e8a5f8d55b 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Text, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; import useLocalize from '@hooks/useLocalize'; @@ -9,6 +9,7 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; +import Text from './Text'; type AnonymousReportFooterProps = { /** The report currently being looked at */ diff --git a/src/components/AutoEmailLink.js b/src/components/AutoEmailLink.tsx similarity index 68% rename from src/components/AutoEmailLink.js rename to src/components/AutoEmailLink.tsx index af581525ab69..e1a9bdd2794b 100644 --- a/src/components/AutoEmailLink.js +++ b/src/components/AutoEmailLink.tsx @@ -1,18 +1,13 @@ import {CONST} from 'expensify-common/lib/CONST'; -import PropTypes from 'prop-types'; import React from 'react'; -import _ from 'underscore'; +import type {StyleProp, TextStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Text from './Text'; import TextLink from './TextLink'; -const propTypes = { - text: PropTypes.string.isRequired, - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - style: [], +type AutoEmailLinkProps = { + text: string; + style?: StyleProp; }; /* @@ -21,14 +16,15 @@ const defaultProps = { * - Else just render it inside `Text` component */ -function AutoEmailLink(props) { +function AutoEmailLink({text, style}: AutoEmailLinkProps) { const styles = useThemeStyles(); return ( - - {_.map(props.text.split(CONST.REG_EXP.EXTRACT_EMAIL), (str, index) => { + + {text.split(CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => { if (CONST.REG_EXP.EMAIL.test(str)) { return ( {str} @@ -52,6 +49,5 @@ function AutoEmailLink(props) { } AutoEmailLink.displayName = 'AutoEmailLink'; -AutoEmailLink.propTypes = propTypes; -AutoEmailLink.defaultProps = defaultProps; + export default AutoEmailLink; diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js index 92cbe3a4da04..f69fe7eb5ecb 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.js @@ -6,7 +6,6 @@ import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import gestureHandlerPropTypes from './gestureHandlerPropTypes'; @@ -51,7 +50,6 @@ const defaultProps = { }; function ImageCropView(props) { - const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); @@ -90,7 +88,8 @@ function ImageCropView(props) { diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 5993e60861f5..807029addf5e 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -33,10 +33,10 @@ type FullPageNotFoundViewProps = { linkKey?: TranslationPaths; /** Method to trigger when pressing the back button of the header */ - onBackButtonPress: () => void; + onBackButtonPress?: () => void; /** Function to call when pressing the navigation link */ - onLinkPress: () => void; + onLinkPress?: () => void; }; // eslint-disable-next-line rulesdir/no-negated-variables diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.tsx similarity index 56% rename from src/components/ButtonWithDropdownMenu.js rename to src/components/ButtonWithDropdownMenu.tsx index 4d3ec8796a31..466c68229a32 100644 --- a/src/components/ButtonWithDropdownMenu.js +++ b/src/components/ButtonWithDropdownMenu.tsx @@ -1,89 +1,89 @@ -import PropTypes from 'prop-types'; +import type {RefObject} from 'react'; import React, {useEffect, useRef, useState} from 'react'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; -import sourcePropTypes from './Image/sourcePropTypes'; +import type {AnchorAlignment} from './Popover/types'; import PopoverMenu from './PopoverMenu'; -const propTypes = { +type DropdownOption = { + value: string; + text: string; + icon: IconAsset; + iconWidth?: number; + iconHeight?: number; + iconDescription?: string; +}; + +type ButtonWithDropdownMenuProps = { /** Text to display for the menu header */ - menuHeaderText: PropTypes.string, + menuHeaderText?: string; /** Callback to execute when the main button is pressed */ - onPress: PropTypes.func.isRequired, + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: string) => void; /** Call the onPress function on main button when Enter key is pressed */ - pressOnEnter: PropTypes.bool, + pressOnEnter?: boolean; /** Whether we should show a loading state for the main button */ - isLoading: PropTypes.bool, + isLoading?: boolean; /** The size of button size */ - buttonSize: PropTypes.oneOf(_.values(CONST.DROPDOWN_BUTTON_SIZE)), + buttonSize: ValueOf; /** Should the confirmation button be disabled? */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Additional styles to add to the component */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style?: StyleProp; /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - icon: sourcePropTypes, - iconWidth: PropTypes.number, - iconHeight: PropTypes.number, - iconDescription: PropTypes.string, - }), - ).isRequired, + options: DropdownOption[]; /** The anchor alignment of the popover menu */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), + anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + buttonRef: RefObject; }; -const defaultProps = { - isLoading: false, - isDisabled: false, - pressOnEnter: false, - menuHeaderText: '', - style: [], - buttonSize: CONST.DROPDOWN_BUTTON_SIZE.MEDIUM, - anchorAlignment: { +function ButtonWithDropdownMenu({ + isLoading = false, + isDisabled = false, + pressOnEnter = false, + menuHeaderText = '', + style, + buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM, + anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, - buttonRef: () => {}, -}; - -function ButtonWithDropdownMenu(props) { + buttonRef, + onPress, + options, +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectedItemIndex, setSelectedItemIndex] = useState(0); const [isMenuVisible, setIsMenuVisible] = useState(false); - const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); + const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); - const caretButton = useRef(null); - const selectedItem = props.options[selectedItemIndex] || _.first(props.options); - const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(props.buttonSize); - const isButtonSizeLarge = props.buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; + const caretButton = useRef(null); + const selectedItem = options[selectedItemIndex] || options[0]; + const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); + const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; useEffect(() => { if (!caretButton.current) { @@ -92,29 +92,31 @@ function ButtonWithDropdownMenu(props) { if (!isMenuVisible) { return; } - caretButton.current.measureInWindow((x, y, w, h) => { - setPopoverAnchorPosition({ - horizontal: x + w, - vertical: - props.anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP - ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding - : y - CONST.MODAL.POPOVER_MENU_PADDING, // if it is BOTTOM, menu will open above the button so NO need to add height but DO subtract padding + if ('measureInWindow' in caretButton.current) { + caretButton.current.measureInWindow((x, y, w, h) => { + setPopoverAnchorPosition({ + horizontal: x + w, + vertical: + anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP + ? y + h + CONST.MODAL.POPOVER_MENU_PADDING // if vertical anchorAlignment is TOP, menu will open below the button and we need to add the height of button and padding + : y - CONST.MODAL.POPOVER_MENU_PADDING, // if it is BOTTOM, menu will open above the button so NO need to add height but DO subtract padding + }); }); - }); - }, [windowWidth, windowHeight, isMenuVisible, props.anchorAlignment.vertical]); + } + }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); return ( - {props.options.length > 1 ? ( - + {options.length > 1 ? ( +