diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4100a13f8bee..4f7d1c71553a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,8 +18,8 @@ $ https://github.com/Expensify/App/issues/ Do NOT only link the issue number like this: $ # ---> -$ -PROPOSAL: +$ +PROPOSAL: ### Tests @@ -98,7 +98,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory - [ ] If a new CSS style is added I verified that: - [ ] A similar style doesn't already exist - - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`) + - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`) - [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic. - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. diff --git a/.github/actions/composite/setupGitForOSBotifyApp/action.yml b/.github/actions/composite/setupGitForOSBotifyApp/action.yml index 7a90cc45257d..a6c487705c56 100644 --- a/.github/actions/composite/setupGitForOSBotifyApp/action.yml +++ b/.github/actions/composite/setupGitForOSBotifyApp/action.yml @@ -60,6 +60,11 @@ runs: if: runner.debug == '1' run: echo "GIT_TRACE=true" >> "$GITHUB_ENV" + - name: Sync clock + shell: bash + run: sudo sntp -sS time.windows.com + if: runner.os == 'macOS' + - name: Generate a token id: generateToken uses: actions/create-github-app-token@9d97a4282b2c51a2f4f0465b9326399f53c890d4 diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a58745b742ad..c49530c46faa 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -17,6 +17,11 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/composite/setupNode + - name: Set dummy git credentials + run: | + git config --global user.email "test@test.com" + git config --global user.name "Test" + - name: Run performance testing script shell: bash run: | @@ -27,6 +32,7 @@ jobs: npm install --force npx reassure --baseline git switch --force --detach - + git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours npm install --force npx reassure --branch diff --git a/.imgbotconfig b/.imgbotconfig index 43d1b77166cc..45cdf03ec36e 100644 --- a/.imgbotconfig +++ b/.imgbotconfig @@ -1,7 +1,7 @@ { "ignoredFiles": [ - "assets/images/empty-state_background-fade-dark.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 - "assets/images/empty-state_background-fade-light.png" + "assets/images/themeDependent/empty-state_background-fade-dark.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499 + "assets/images/themeDependent/empty-state_background-fade-light.png" ], "aggressiveCompression": "false" } diff --git a/.storybook/public/index.css b/.storybook/public/index.css index 8ace4b240684..2d2411c083c1 100644 --- a/.storybook/public/index.css +++ b/.storybook/public/index.css @@ -24,5 +24,5 @@ a.sidebar-item[data-selected="true"], a.sidebar-item[data-selected="true"]:focus } .sidebar-container { - background: #07271f; + background: #072419; } diff --git a/.storybook/theme.js b/.storybook/theme.js index 96631764726f..67898fb00943 100644 --- a/.storybook/theme.js +++ b/.storybook/theme.js @@ -7,17 +7,17 @@ export default create({ fontBase: 'ExpensifyNeue-Regular', fontCode: 'monospace', base: 'dark', - appBg: colors.darkHighlightBackground, - colorPrimary: colors.darkDefaultButton, + appBg: colors.productDark200, + colorPrimary: colors.productDark400, colorSecondary: colors.green, - appContentBg: colors.darkAppBackground, - textColor: colors.darkPrimaryText, - barTextColor: colors.darkPrimaryText, + appContentBg: colors.productDark100, + textColor: colors.productDark900, + barTextColor: colors.productDark900, barSelectedColor: colors.green, - barBg: colors.darkAppBackground, - appBorderColor: colors.darkBorders, - inputBg: colors.darkHighlightBackground, - inputBorder: colors.darkBorders, + barBg: colors.productDark100, + appBorderColor: colors.productDark400, + inputBg: colors.productDark200, + inputBorder: colors.productDark400, appBorderRadius: 8, inputBorderRadius: 8, }); diff --git a/__mocks__/@ua/react-native-airship.js b/__mocks__/@ua/react-native-airship.js index 1672c064f9be..29be662e96a1 100644 --- a/__mocks__/@ua/react-native-airship.js +++ b/__mocks__/@ua/react-native-airship.js @@ -31,7 +31,7 @@ const Airship = { }, contact: { identify: jest.fn(), - getNamedUserId: jest.fn(), + getNamedUserId: () => Promise.resolve(undefined), reset: jest.fn(), }, }; diff --git a/android/app/build.gradle b/android/app/build.gradle index f224d895e2fa..25974d76db47 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001040801 - versionName "1.4.8-1" + versionCode 1001041115 + versionName "1.4.11-15" } flavorDimensions "default" diff --git a/assets/images/empty-state_background-fade-dark.png b/assets/images/empty-state_background-fade-dark.png deleted file mode 100644 index 1caf5630bee3..000000000000 Binary files a/assets/images/empty-state_background-fade-dark.png and /dev/null differ diff --git a/assets/images/empty-state_background-fade-light.png b/assets/images/empty-state_background-fade-light.png deleted file mode 100644 index 98456609b502..000000000000 Binary files a/assets/images/empty-state_background-fade-light.png and /dev/null differ diff --git a/assets/images/example-check-image-en.png b/assets/images/example-check-image-en.png deleted file mode 100644 index 903618776cdf..000000000000 Binary files a/assets/images/example-check-image-en.png and /dev/null differ diff --git a/assets/images/example-check-image-es.png b/assets/images/example-check-image-es.png deleted file mode 100644 index de695a43833d..000000000000 Binary files a/assets/images/example-check-image-es.png and /dev/null differ diff --git a/assets/images/home-background--android.svg b/assets/images/home-background--android.svg index 2b72b6ccabe9..29c8affce1cc 100644 --- a/assets/images/home-background--android.svg +++ b/assets/images/home-background--android.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-background--mobile.svg b/assets/images/home-background--mobile.svg index 7c4d4d8289b7..d2fa08475c9d 100644 --- a/assets/images/home-background--mobile.svg +++ b/assets/images/home-background--mobile.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-fade-gradient--mobile.svg b/assets/images/home-fade-gradient--mobile.svg index 0b24b678a2e6..ad150f3c870c 100644 --- a/assets/images/home-fade-gradient--mobile.svg +++ b/assets/images/home-fade-gradient--mobile.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/home-fade-gradient.svg b/assets/images/home-fade-gradient.svg index bfe04d545364..c446d7b46a42 100644 --- a/assets/images/home-fade-gradient.svg +++ b/assets/images/home-fade-gradient.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/themeDependent/empty-state_background-fade-dark.png b/assets/images/themeDependent/empty-state_background-fade-dark.png new file mode 100644 index 000000000000..59951ef707fb Binary files /dev/null and b/assets/images/themeDependent/empty-state_background-fade-dark.png differ diff --git a/assets/images/themeDependent/empty-state_background-fade-light.png b/assets/images/themeDependent/empty-state_background-fade-light.png new file mode 100644 index 000000000000..200996057b47 Binary files /dev/null and b/assets/images/themeDependent/empty-state_background-fade-light.png differ diff --git a/assets/images/themeDependent/example-check-image-dark-en.png b/assets/images/themeDependent/example-check-image-dark-en.png new file mode 100644 index 000000000000..6b8e84a0faa2 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-dark-en.png differ diff --git a/assets/images/themeDependent/example-check-image-dark-es.png b/assets/images/themeDependent/example-check-image-dark-es.png new file mode 100644 index 000000000000..446678e401b6 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-dark-es.png differ diff --git a/assets/images/themeDependent/example-check-image-light-en.png b/assets/images/themeDependent/example-check-image-light-en.png new file mode 100644 index 000000000000..d7b5f035c625 Binary files /dev/null and b/assets/images/themeDependent/example-check-image-light-en.png differ diff --git a/assets/images/themeDependent/example-check-image-light-es.png b/assets/images/themeDependent/example-check-image-light-es.png new file mode 100644 index 000000000000..2183b522a35b Binary files /dev/null and b/assets/images/themeDependent/example-check-image-light-es.png differ diff --git a/assets/images/thumbs-up.svg b/assets/images/thumbs-up.svg new file mode 100644 index 000000000000..ef81c88fc854 --- /dev/null +++ b/assets/images/thumbs-up.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 8c74ebfd1686..008b4f45911f 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -211,7 +211,21 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // This is also why we have to use .website.js for our own web-specific files... // Because desktop also relies on "web-specific" module implementations // This also skips packing web only dependencies to desktop and vice versa - extensions: ['.web.js', platform === 'web' ? '.website.js' : '.desktop.js', '.js', '.jsx', '.web.ts', platform === 'web' ? '.website.ts' : '.desktop.ts', '.ts', '.web.tsx', '.tsx'], + extensions: [ + '.web.js', + ...(platform === 'desktop' ? ['.desktop.js'] : []), + '.website.js', + '.js', + '.jsx', + '.web.ts', + ...(platform === 'desktop' ? ['.desktop.ts'] : []), + '.website.ts', + ...(platform === 'desktop' ? ['.desktop.tsx'] : []), + '.website.tsx', + '.ts', + '.web.tsx', + '.tsx', + ], fallback: { 'process/browser': require.resolve('process/browser'), }, diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 32d3919efbe4..543b133fe62b 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -1,6 +1,6 @@ # Overview -The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.js` file. +The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.tsx` file. ## Terminology @@ -20,11 +20,11 @@ Navigation Actions - User actions correspond to resulting navigation actions tha ## Adding RHP flows -Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.js` and `ModalStackNavigators.js` file: +Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.tsx` and `ModalStackNavigators.tsx` file: -- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.js`. +- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.tsx`. -- If you want to create new flow, add a `Screen` in `RightModalNavigator.js` and make new modal in `ModalStackNavigators.js` with chosen pages. +- If you want to create new flow, add a `Screen` in `RightModalNavigator.tsx` and make new modal in `ModalStackNavigators.tsx` with chosen pages. When creating RHP flows, you have to remember a couple things: @@ -40,7 +40,17 @@ When creating RHP flows, you have to remember a couple things: An example of adding `Settings_Workspaces` page: -1. Add path to `ROUTES.ts`: https://github.com/Expensify/App/blob/main/src/ROUTES.ts +1. Add the page name to `SCREENS.ts` which will be reused throughout the app (linkingConfig, navigators, etc.): + +```ts +const SCREENS = { + SETTINGS: { + WORKSPACES: 'Settings_Workspaces', + }, +} as const; +``` + +2. Add path to `ROUTES.ts`: https://github.com/Expensify/App/blob/main/src/ROUTES.ts ```ts export const ROUTES = { @@ -55,11 +65,11 @@ export const ROUTES = { ``` -2. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.ts`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/linkingConfig.js#L40-L42 +3. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.ts`: https://github.com/Expensify/App/blob/fbc11ca729ffa4676fb3bc8cd110ac3890debff6/src/libs/Navigation/linkingConfig.ts#L47-L50 -3. Add your page to proper navigator (it should be aligned with where you've put it in the previous step) https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/AppNavigator/ModalStackNavigators.js#L334-L338 +4. Add your page to proper navigator (it should be aligned with where you've put it in the previous step) https://github.com/Expensify/App/blob/fbc11ca729ffa4676fb3bc8cd110ac3890debff6/src/libs/Navigation/AppNavigator/ModalStackNavigators.js#L141 -4. Make sure `HeaderWithBackButton` leads to the previous page in navigation flow of your page: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/pages/workspace/WorkspacesListPage.js#L186 +5. Make sure `HeaderWithBackButton` leads to the previous page in navigation flow of your page: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/pages/workspace/WorkspacesListPage.js#L186 ## Performance solutions @@ -196,4 +206,4 @@ The action for the first step created with `getMinimalAction` looks like this: ``` ### Deeplinking -There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. \ No newline at end of file +There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 68088f623f8d..b7cbd87dc4b9 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -46,7 +46,7 @@ - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory - [ ] If a new CSS style is added I verified that: - [ ] A similar style doesn't already exist - - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/StyleUtils.js) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`) + - [ ] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`) - [ ] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic. - [ ] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases) - [ ] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected. diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html index d18ca2199e33..798fb2cf7e96 100644 --- a/docs/_includes/footer.html +++ b/docs/_includes/footer.html @@ -111,6 +111,8 @@

Get Started

+ + diff --git a/docs/_sass/_colors.scss b/docs/_sass/_colors.scss index b34a7d13b7f0..f0c89d31c580 100644 --- a/docs/_sass/_colors.scss +++ b/docs/_sass/_colors.scss @@ -1,14 +1,36 @@ -$color-green400: #03D47C; -$color-green-icons: #8B9C8F; -$color-green-borders: #1A3D32; -$color-button-background: #1A3D32; -$color-button-hovered: #2C6755; -$color-green-highlightBG: #07271F; -$color-green-highlightBG-hover: #06231c; -$color-green-appBG: #061B09; -$color-green-hover: #00a862; -$color-light-gray-green: #AFBBB0; -$color-blue300: #5AB0FF; +// Product Color Spectrum +$color-product-dark-100: #061B09; +$color-product-dark-200: #072419; +$color-product-dark-300: #0A2E25; +$color-product-dark-400: #1A3D32; +$color-product-dark-500: #224F41; +$color-product-dark-600: #2A604F; +$color-product-dark-700: #8B9C8F; +$color-product-dark-800: #AFBBB0; +$color-product-dark-900: #E7ECE9; + +// Colors for Links and Success $color-blue200: #B0D9FF; -$color-white: #E7ECE9; -$color-gray-label: #afbbb0; +$color-blue300: #5AB0FF; +$color-green400: #03D47C; +$color-green500: #00a862; + +// Overlay BG color +$color-overlay-background: rgba(26, 61, 50, 0.72); + +// UI Colors +$color-text: $color-product-dark-900; +$color-text-supporting: $color-product-dark-800; +$color-icons: $color-product-dark-700; +$color-borders: $color-product-dark-400; +$color-highlightBG: $color-product-dark-200; +$color-row-hover: $color-product-dark-300; +$color-appBG: $color-product-dark-100; +$color-success: $color-green400; +$color-accent : $color-green400; +$color-link: $color-blue300; +$color-link-hovered: $color-blue200; +$color-button-background: $color-product-dark-400; +$color-button-background-hover: $color-product-dark-500; +$color-button-success-background: $color-green400; +$color-button-success-background-hover: $color-green500; diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 7a0804b0f962..eaaa1c63badb 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -3,19 +3,6 @@ @import 'fonts'; @import 'search-bar'; -$color-appBG: $color-green-appBG; -$color-highlightBG: $color-green-highlightBG; -$color-accent : $color-green400; -$color-borders: $color-green-borders; -$color-icons: $color-green-icons; -$color-text: $color-white; -$color-link: $color-blue300; -$color-link-hovered: $color-blue200; -$color-success: $color-green400; -$color-text-supporting: $color-light-gray-green; -$color-green-hover: $color-green-hover; -$color-gray-label: $color-gray-label; - * { margin: 0; padding: 0; @@ -78,6 +65,7 @@ body { height: 100%; min-height: 100%; background: $color-appBG; + color: $color-text-supporting; } hr { @@ -148,7 +136,7 @@ textarea { font-weight: 400; font-family: "ExpensifyNeue", "Helvetica Neue", "Helvetica", Arial, sans-serif; font-size: 16px; - color: $color-text; + color: $color-text-supporting; } button { @@ -159,7 +147,7 @@ button { font-weight: bold; &.success { - background-color: $color-success; + background-color: $color-button-success-background; color: $color-text; width: 100%; border-radius: 100px; @@ -167,7 +155,7 @@ button { padding-right: 20px; &:hover { - background-color: desaturate($color-success, 15%); + background-color: $color-button-success-background-hover; cursor: pointer; } @@ -262,6 +250,7 @@ button { .lhn-header { padding: 24px; + @include breakpoint($breakpoint-tablet) { padding: 44px; } @@ -269,6 +258,7 @@ button { #header-button { display: block; padding-right: 24px; + @include breakpoint($breakpoint-tablet) { display: none; } @@ -282,7 +272,7 @@ button { margin-right: auto; @include breakpoint($breakpoint-desktop) { - width: 210px; + width: 180px; align-content: normal; display: flex; margin-left: 0; @@ -372,7 +362,7 @@ button { // Box shadow is used here because border-radius and border-collapse don't work together. It leads to double borders. // https://stackoverflow.com/questions/628301/the-border-radius-property-and-border-collapsecollapse-dont-mix-how-can-i-use border-style: hidden; - box-shadow: 0 0 0 1px $color-green-borders; + box-shadow: 0 0 0 1px $color-borders; } th:first-child { @@ -394,12 +384,12 @@ button { th, td { padding: 6px 13px; - border: 1px solid $color-green-borders; + border: 1px solid $color-borders; } thead tr th { font-weight: bold; - background-color: $color-green-highlightBG; + background-color: $color-highlightBG; } .img-wrap { @@ -457,11 +447,11 @@ button { .link { display: inline; - color: $color-link; + color: $color-text-supporting; cursor: pointer; &:hover { - color: $color-link-hovered; + color: $color-link; } } @@ -530,7 +520,7 @@ button { background-color: $color-highlightBG; &:hover { - background-color: darken($color-highlightBG, 1%); + background-color: $color-row-hover; } .row { @@ -629,13 +619,14 @@ button { } p.description { + color: $color-text-supporting; padding: 20px 0 20px 0; } p.url { padding: 0; font-size: 0.8em; - color: $color-gray-label; + color: $color-text-supporting; } } @@ -739,7 +730,7 @@ button { .get-help { flex-wrap: wrap; - margin-top: auto; + margin-top: 40px; } .floating-concierge-button { @@ -773,9 +764,12 @@ button { h3 { color: $color-success; + font-family: "ExpensifyNewKansas", "Helvetica Neue", "Helvetica", Arial, sans-serif; font-size: 17px; - font-weight: 700; + font-weight: 500; + padding: 0; margin-bottom: 16px; + margin-top: 0; } ul { @@ -787,13 +781,13 @@ button { margin: 0 0 8px; a { - color: $color-text; + color: $color-text-supporting; display: block; padding: 4px 0; word-break: break-word; &:hover { - color: $color-success; + color: $color-link; } } } @@ -803,11 +797,11 @@ button { padding-bottom: 20px; a { - color: $color-text; + color: $color-text-supporting; display: inline-block; &:hover { - color: $color-success; + color: $color-link; } } } diff --git a/docs/_sass/_search-bar.scss b/docs/_sass/_search-bar.scss index c2185ef8f36a..f414d25fc266 100644 --- a/docs/_sass/_search-bar.scss +++ b/docs/_sass/_search-bar.scss @@ -2,20 +2,6 @@ @import 'colors'; @import 'fonts'; -$color-appBG: $color-green-appBG; -$color-highlightBG: $color-green-highlightBG; -$color-highlightBG-hover: $color-green-highlightBG-hover; -$color-accent : $color-green400; -$color-borders: $color-green-borders; -$color-icons: $color-green-icons; -$color-text: $color-white; -$color-link: $color-blue300; -$color-link-hovered: $color-blue200; -$color-success: $color-green400; -$color-text-supporting: $color-light-gray-green; -$color-green-hover: $color-green-hover; -$color-gray-label: $color-gray-label; - .search-icon { margin: auto 0px; } @@ -81,7 +67,7 @@ $color-gray-label: $color-gray-label; left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.4); + background-color: $color-overlay-background; z-index: 1; } @@ -158,7 +144,7 @@ label.search-label { transform: translateY(-50%); left: 20px; pointer-events: none; - color: $color-gray-label; + color: $color-text-supporting; transform-origin: left top; user-select: none; transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1), top 500ms; @@ -194,14 +180,14 @@ label.search-label { margin-left: 15px; margin-right: 20px; border-radius: 25px; - background-color: $color-green400; + background-color: $color-button-success-background; cursor: pointer; width: 40px; height: 40px; } .gsc-search-button.gsc-search-button-v2:hover { - background-color: $color-green-hover; + background-color: $color-button-success-background-hover; } .gsc-search-button.gsc-search-button-v2 svg { @@ -227,7 +213,7 @@ label.search-label { /* Change Font result Paragraph color */ .gsc-results .gs-webResult:not(.gs-no-results-result):not(.gs-error-result) .gs-snippet, .gs-fileFormatType { - color: $color-text; + color: $color-text-supporting; } @@ -278,7 +264,7 @@ label.search-label { color: $color-text; &:hover { - background-color: $color-button-hovered; + background-color: $color-button-background-hover; text-decoration: none; } } diff --git a/docs/articles/expensify-classic/account-settings/Profile-Settings.md b/docs/articles/expensify-classic/account-settings/Profile-Settings.md deleted file mode 100644 index 3b2a0b830926..000000000000 --- a/docs/articles/expensify-classic/account-settings/Profile-Settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Profile Settings -description: Profile Settings ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md deleted file mode 100644 index 71edcdeba00d..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Personal Cards -description: Connect your credit card directly to Expensify to easily track your personal finances. ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Referral-Program.md b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md similarity index 100% rename from docs/articles/expensify-classic/getting-started/Referral-Program.md rename to docs/articles/expensify-classic/get-paid-back/Referral-Program.md diff --git a/docs/articles/expensify-classic/getting-started/Mobile-App.md b/docs/articles/expensify-classic/getting-started/Mobile-App.md deleted file mode 100644 index 7fa57abbdf61..000000000000 --- a/docs/articles/expensify-classic/getting-started/Mobile-App.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Using the App -description: Using the App ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md index 3ee1c8656b4b..cf2f0f59a4a0 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md @@ -1,5 +1,140 @@ --- -title: Coming Soon -description: Coming Soon +title: Domains +description: Want to gain greater control over your company settings in Expensify? Read on to find out more about our Domains feature and how it can help you save time and effort when managing your company expenses. --- -## Resource Coming Soon! + +# Overview +Domains is a feature in Expensify that allows admins to have more nuanced control over a specific Expensify activity, as well as providing a bird’s eye view of company card expenditure. Think of it as your command center for things like managing user account access, enforcing stricter Workspace rules for certain groups, or issuing cards and reconciling statements. +There are several settings within Domains that you can configure so that you have more control and visibility into your organization’s settings. Those features are: +- Company Cards +- Domain Admins +- Domain Members + - Two-Factor Authentication +- Domain Groups + - Domain Group Settings +- Reporting Tools +- SAML + +There are two ways to use Domains – as an unverified domain or a verified domain. An unverified domain allows you to import Company Cards and manage them, whereas a verified domain allows you to do that in addition to: +1. Receive vendor bills in Expensify +2. Fine-tune user restrictions using domain Groups +3. Configure SAML SSO for easier login to Expensify +4. Set vacation delegates for your domain members +5. Use consolidated domain billing + +# How to claim a domain +To use the domains feature with an unverified domain, you’ll need to claim the domain first. +To claim a domain, you need to be a Workspace Admin with a company email address. This allows you to manage company bills, company cards, and reconciliation. Claiming requires an email matching your company's domain. +1. Create an Expensify account +2. Set up an expense Workspace +3. Go to **Settings > _Domains_**. +Whichever member runs through those steps will automatically be made a Domain Admin. + + +# How to verify a domain +To use the domains feature with a verified domain, you’ll want to go through the steps of verifying it. + +To verify domain ownership, follow these steps: +1. Log in to your DNS service provider, which could be your Domain Name Registrar like NameCheap or GoDaddy, a dedicated DNS service provider like DNSMadeEasy or Amazon Route53, or managed internally by your company's IT department. +2. Find the page for editing DNS records for expensify.com. This might be labeled as DNS Management or Zone File Editor. +3. Add a new TXT record and set the value as: **532F6180D8** +4. Save your changes +5. Click the Verify button to confirm domain ownership + +After successful verification, you can remove the TXT DNS record. Please note that an email will be sent to all Expensify users on the domain to inform them that their accounts will be under Domain Control after verification. + +**Tips:** +Not sure how to do this? Check the below guides from some of the most popular hosts on the web: +[123-reg.co.uk](https://www.123-reg.co.uk/) +[One.com](https://www.one.com/en/) +[Wix.com](https://www.wix.com/) +Google/GSuite +[Godaddy](https://www.godaddy.com/) +When creating the TXT record, input only the code and no other values or information. +You can always confirm if you added the TXT code correctly here: https://viewdns.info/dnsrecord/?domain=[enterdomainhere] + +# Domain settings + +## Domain Admins +Domain Admins have full authority over domain settings. They can modify member group names and rules, link or modify Company Cards, and add or remove domain members and other admins. + +### Adding a Domain Admin +1. Head to **Settings > Domains > [Domain Name] > Domain Admins** +2. In the "Email or Phone" field, type in the email address of the person you want to make a Domain Admin (this can be any email not specifically tied to the domain) +3. Click "Add Admin" + +### Removing a Domain Admin: +1. If you're already a Domain Admin, go to **Settings > Domains > [Domain Name] > Domain Admins** +2. Locate the list of Domain Admins and find the one you want to remove +3. Next to the Domain Admin's name, click the red trash can icon. This will remove that person from the Domain Admin role + +## Domain Members +A domain member is a user associated with a specific domain (usually a company or another group) in Expensify and typically managed by a Domain Admin. This is also where you can enable Two-Factor authentication for your domain. + +### Adding users to the domain +When a Domain Admin adds a user to the domain, that will create a new Expensify account for that user, and they'll receive invitations to set up their account. Users can also join a verified domain by creating their own account, as long as they have an email address associated with that domain (e.g. yourname@yourcompany.com). Once they have verified the account, all Domain Admins will be notified, and the employee will be added to the Default Group. +**Important Note:** If someone who isn't a Domain Admin invites a user to a Workspace before they're invited to the domain, their account will be created, but in a closed state. A closed state means that the account cannot be used until it has been validated. Once the Domain Admin has invited the user, the user will receive a magic link to verify their account, sign in, and open the account completely. + +### How to add users +1. In your web account, go to **Settings > Domains > [Domain Name] > Domain Admins** +2. In the email field, enter the user you want to invite. This will create their Expensify account and send them an invitation + +### Removing users from the Domain +Removing a user means taking them out of your domain and closing their Expensify account completely if they don't have another login. Be cautious because closing an account is permanent and deletes any unsubmitted or processing reports. + +### How to remove users +In your web account, go to **Settings > Domains > [Domain Name] > Domain Admins** +Check the box next to the employee's name you want to remove, then click “Close Accounts”. + +### Important notes about closing accounts through Domain settings: +If a user has a Secondary Login linked to their Expensify account, they can still access their account after it's closed in the domain. This is helpful for accessing financial data, like tax-related receipts. +Closing an account through the domain permanently removes any unsubmitted receipts/reports. Make sure to approve or reimburse all employee reports before closing an account. +If an employee doesn't have a Secondary Login, they'll be automatically removed from the group Workspace. If they have a Secondary Login, it will continue to be associated with the group Workspace. + +## Domain Groups +Domain Groups can be accessed if you have verified your domain. Groups are used to set rules or permissions for groups of users so you can enforce multiple different expense workspaces and rules. If you are a Domain Admin, you can create and edit Domain Groups under **Settings > Domains > _Domain Name_ > Groups**. + +### Creating Domain Groups +1. In your Expensify account on the web, navigate to **Settings > Domains > _Domain Name_ > Groups** +2. Select “Create Group” to create the group. This will allow you to name the Group, as well as configure permissions that will apply to members of the Group. + +### Adding members to a Domain Group +1. In your Expensify account on the web, navigate to **Settings > Domains > [Domain Name] > Domain Members** +2. Select the checkbox next to the domain members you wish to add to the Domain Group +3. Select “Add to Group” to select the Group you wish to add them to + +### Editing Domain Groups +1. In your Expensify account on the web, navigate to **Settings > Domains > _Domain Name_ > Groups** +2. Next to the Group you wish to edit, select “Edit” +3. This will open the Edit Permission Group pane, where you can edit the rules and permissions for that group +4. Make your edits and click “Save” + +## Domain Group settings +These are the settings that can be customized for each group you have created. Typically, companies use two groups (Employees and Managers) and enforce stricter rules for Employees. The settings are: +- Strict Workspace Enforcement: When enabled, all Workspace rules must be followed for a report to be submitted. If a rule is violated, the report can't be submitted until the issue is fixed. Employees can't bypass this by dismissing notifications. +- Login Restrictions: Enabling this prevents users from using non-company email addresses as their primary login. Secondary logins are still allowed. +- Workspace Creation and Removal Restrictions: This feature stops users from creating new group workspaces or unsubscribing from existing workspaces. Admins who need these abilities should be in a separate group with this restriction turned off. +- Preferred Workspace: When enabled, group members can only create reports under one designated Workspace. They can move a report to a different Workspace or their personal one later if needed. This helps keep personal and company expenses separate. If a company card uses a specific Workspace, this setting overrides it for more control over company card expenses. +- Setting a Preferred Workspace: If Preferred Workspace is on, you can choose a default group Workspace for all Group Members. + +## SAML +To enable SAML SSO in Expensify you will first need to claim and verify your domain. Once you have a verified domain, you can access SAML SSO by navigating to **Settings > Domains > _Domain Name_ > SAML** + +## Enable Two-Factor Authentication (2FA) +1. As a Domain Admin, head to: **Settings > Domains > _Your Domain Name_ > Domain Members** +2. Turn on Two Factor Authentication by toggling it to ENABLED +3. Any Domain members that do not have two-factor authentication enabled will be asked to set it up on their Home page when they next log in, and won't be able to use Expensify until they do. +4. To turn it off, simply toggle it off and refresh the page. + +**Tips:** +- When using SAML, two-factor authentication cannot be required. +- For disputing digital Expensify Card purchases, two-factor authentication must be enabled. +- It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. + +# FAQ + +## How many domains can I have? +You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. + +## What’s the difference between claiming a domain and verifying a domain? +Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. diff --git a/docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md b/docs/articles/new-expensify/get-paid-back/Referral-Program.md similarity index 100% rename from docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md rename to docs/articles/new-expensify/get-paid-back/Referral-Program.md diff --git a/docs/articles/new-expensify/get-paid-back/Request-Money.md b/docs/articles/new-expensify/get-paid-back/Request-Money.md index a2b765915af0..43a72a075de7 100644 --- a/docs/articles/new-expensify/get-paid-back/Request-Money.md +++ b/docs/articles/new-expensify/get-paid-back/Request-Money.md @@ -1,6 +1,36 @@ --- -title: Request Money -description: Request Money +title: Request Money and Split Bills with Friends +description: Everything you need to know about Requesting Money and Splitting Bills with Friends! redirect_from: articles/request-money/Request-and-Split-Bills/ --- -## Resource Coming Soon! + + + +# How do these Payment Features work? +Our suite of money movement features enables you to request money owed by an individual or split a bill with a group. + +**Request Money** lets your friends pay you back directly in Expensify. When you send a payment request to a friend, Expensify will display the amount owed and the option to pay the corresponding request in a chat between you. + +**Split Bill** allows you to split payments between friends and ensures the person who settled the tab gets paid back. + +These two features ensure you can live in the moment and settle up afterward. + +# How to Request Money +- Select the Green **+** button and choose **Request Money** +- Enter the amount **$** they owe and click **Next** +- Search for the user or enter their email! +- Enter a reason for the request (optional) +- Click **Request!** +- If you change your mind, all you have to do is click **Cancel** +- The user will be able to **Settle up outside of Expensify** or pay you via **Venmo** or **PayPal.me** + +# How to Split a Bill +- Select the Green **+** button and choose **Split Bill** +- Enter the total amount for the bill and click **Next** +- Search for users or enter their emails and **Select** +- Enter a reason for the split +- The split is then shared equally between the attendees + +# FAQs +## Can I request money from more than one person at a time? +If you need to request money for more than one person at a time, you’ll want to use the Split Bill feature. The Request Money option is for one-to-one payments between two people. diff --git a/docs/assets/images/settings-old-dot.svg b/docs/assets/images/settings-old-dot.svg index ca5bc04bd0ff..85561a886459 100644 --- a/docs/assets/images/settings-old-dot.svg +++ b/docs/assets/images/settings-old-dot.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7c3fbf13697a..446b251122d6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.8 + 1.4.11 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.8.1 + 1.4.11.15 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0d2561b67b74..e8bdb7d3bf26 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.8 + 1.4.11 CFBundleSignature ???? CFBundleVersion - 1.4.8.1 + 1.4.11.15 diff --git a/package-lock.json b/package-lock.json index 51dc9df3a5f0..7dd6ac7f8a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.11-15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.11-15", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -50,7 +50,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#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -29894,8 +29894,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", - "integrity": "sha512-u5Is3a/jD9KJ/LtSofeevauDlxZX5++w+VENP2cNhT1lm1GSwRM5FlTT7bIVSyrHr0cppAgl7cLiW2aPDr2hKA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", + "integrity": "sha512-s9l/Zy3UjDBrq0WTkgEue1DXLRkkYtuqnANQlVmODHJ9HkJADjrVSv2D0U3ltqd9X7vLCLCmmwl5AUE6466gGg==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -74403,9 +74403,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", - "integrity": "sha512-u5Is3a/jD9KJ/LtSofeevauDlxZX5++w+VENP2cNhT1lm1GSwRM5FlTT7bIVSyrHr0cppAgl7cLiW2aPDr2hKA==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", + "integrity": "sha512-s9l/Zy3UjDBrq0WTkgEue1DXLRkkYtuqnANQlVmODHJ9HkJADjrVSv2D0U3ltqd9X7vLCLCmmwl5AUE6466gGg==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", diff --git a/package.json b/package.json index ac02f2db5f82..19337bfd5796 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.8-1", + "version": "1.4.11-15", "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.", @@ -51,6 +51,7 @@ "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "test:e2e": "node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", + "test:e2e:dev": "node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "node workflow_tests/utils/preGenerateTest.js", @@ -97,7 +98,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#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", diff --git a/src/CONST.ts b/src/CONST.ts index 283195562e49..d4208d51a78c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -55,6 +55,9 @@ const CONST = { ALLOWED_RECEIPT_EXTENSIONS: ['jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'], }, + // This is limit set on servers, do not update without wider internal discussion + API_TRANSACTION_CATEGORY_MAX_LENGTH: 255, + AUTO_AUTH_STATE: { NOT_STARTED: 'not-started', SIGNING_IN: 'signing-in', @@ -508,6 +511,7 @@ const CONST = { TASKREOPENED: 'TASKREOPENED', POLICYCHANGELOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', + ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', ADD_CATEGORY: 'POLICYCHANGELOG_ADD_CATEGORY', ADD_CUSTOM_UNIT: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT', ADD_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT_RATE', @@ -517,6 +521,7 @@ const CONST = { ADD_TAG: 'POLICYCHANGELOG_ADD_TAG', DELETE_ALL_TAGS: 'POLICYCHANGELOG_DELETE_ALL_TAGS', DELETE_APPROVER_RULE: 'POLICYCHANGELOG_DELETE_APPROVER_RULE', + DELETE_BUDGET: 'POLICYCHANGELOG_DELETE_BUDGET', DELETE_CATEGORY: 'POLICYCHANGELOG_DELETE_CATEGORY', DELETE_CUSTOM_UNIT: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT', DELETE_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT_RATE', @@ -538,6 +543,7 @@ const CONST = { UPDATE_AUTOHARVESTING: 'POLICYCHANGELOG_UPDATE_AUTOHARVESTING', UPDATE_AUTOREIMBURSEMENT: 'POLICYCHANGELOG_UPDATE_AUTOREIMBURSEMENT', UPDATE_AUTOREPORTING_FREQUENCY: 'POLICYCHANGELOG_UPDATE_AUTOREPORTING_FREQUENCY', + UPDATE_BUDGET: 'POLICYCHANGELOG_UPDATE_BUDGET', UPDATE_CATEGORY: 'POLICYCHANGELOG_UPDATE_CATEGORY', UPDATE_CURRENCY: 'POLICYCHANGELOG_UPDATE_CURRENCY', UPDATE_CUSTOM_UNIT: 'POLICYCHANGELOG_UPDATE_CUSTOM_UNIT', @@ -559,6 +565,7 @@ const CONST = { UPDATE_REPORT_FIELD: 'POLICYCHANGELOG_UPDATE_REPORT_FIELD', UPDATE_TAG: 'POLICYCHANGELOG_UPDATE_TAG', UPDATE_TAG_ENABLED: 'POLICYCHANGELOG_UPDATE_TAG_ENABLED', + UPDATE_TAG_LIST: 'POLICYCHANGELOG_UPDATE_TAG_LIST', UPDATE_TAG_LIST_NAME: 'POLICYCHANGELOG_UPDATE_TAG_LIST_NAME', UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME', UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED', @@ -644,6 +651,9 @@ const CONST = { OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', }, + NEXT_STEP: { + FINISHED: 'Finished!', + }, COMPOSER: { MAX_LINES: 16, MAX_LINES_SMALL_SCREEN: 6, @@ -1134,6 +1144,8 @@ const CONST = { }, IOU: { + // This is the transactionID used when going through the create money request flow so that it mimics a real transaction (like the edit flow) + OPTIMISTIC_TRANSACTION_ID: '1', // Note: These payment types are used when building IOU reportAction message values in the server and should // not be changed. PAYMENT_TYPE: { @@ -1146,6 +1158,11 @@ const CONST = { SPLIT: 'split', REQUEST: 'request', }, + REQUEST_TYPE: { + DISTANCE: 'distance', + MANUAL: 'manual', + SCAN: 'scan', + }, REPORT_ACTION_TYPE: { PAY: 'pay', CREATE: 'create', @@ -1153,6 +1170,7 @@ const CONST = { DECLINE: 'decline', CANCEL: 'cancel', DELETE: 'delete', + APPROVE: 'approve', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { @@ -2754,6 +2772,9 @@ const CONST = { NEW_CHAT: 'chat', NEW_ROOM: 'room', RECEIPT_TAB_ID: 'ReceiptTab', + IOU_REQUEST_TYPE: 'iouRequestType', + }, + TAB_REQUEST: { MANUAL: 'manual', SCAN: 'scan', DISTANCE: 'distance', @@ -2807,7 +2828,7 @@ const CONST = { HORIZONTAL_SPACER: { DEFAULT_BORDER_BOTTOM_WIDTH: 1, DEFAULT_MARGIN_VERTICAL: 8, - HIDDEN_MARGIN_VERTICAL: 0, + HIDDEN_MARGIN_VERTICAL: 4, HIDDEN_BORDER_BOTTOM_WIDTH: 0, }, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1bc06e231448..a268c008cee8 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -416,6 +416,7 @@ type OnyxValues = { [ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean; [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; + [ONYXKEYS.IS_LOADING_APP]: boolean; [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer; [ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string; [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean; @@ -428,6 +429,7 @@ type OnyxValues = { [ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken; [ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer; [ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number; + [ONYXKEYS.DEMO_INFO]: OnyxTypes.DemoInfo; [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; @@ -451,12 +453,13 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; - [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: boolean; + [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a3aa28c44609..53763d6d7cd1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -13,7 +13,8 @@ function getUrlWithBackToParam(url: TUrl, backTo?: string): const ROUTES = { HOME: '', - /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ + + // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', FLAG_COMMENT: { route: 'flag/:reportID/:reportActionID', @@ -306,6 +307,82 @@ const ROUTES = { MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', + MONEY_REQUEST_CREATE: { + route: 'create/:iouType/start/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const, + }, + MONEY_REQUEST_STEP_CONFIRMATION: { + route: 'create/:iouType/confirmation/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}/` as const, + }, + MONEY_REQUEST_STEP_AMOUNT: { + route: 'create/:iouType/amount/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_CATEGORY: { + route: 'create/:iouType/category/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_CURRENCY: { + route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => + getUrlWithBackToParam(`create/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo), + }, + MONEY_REQUEST_STEP_DATE: { + route: 'create/:iouType/date/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_DESCRIPTION: { + route: 'create/:iouType/description/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_DISTANCE: { + route: 'create/:iouType/distance/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_MERCHANT: { + route: 'create/:iouType/merchante/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/merchante/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_PARTICIPANTS: { + route: 'create/:iouType/participants/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_SCAN: { + route: 'create/:iouType/scan/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}/`, backTo), + }, + MONEY_REQUEST_STEP_TAG: { + route: 'create/:iouType/tag/:transactionID/:reportID/', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + 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), + }, + MONEY_REQUEST_CREATE_TAB_DISTANCE: { + route: 'create/:iouType/start/:transactionID/:reportID/distance', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/distance` as const, + }, + MONEY_REQUEST_CREATE_TAB_MANUAL: { + route: 'create/:iouType/start/:transactionID/:reportID/manual', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/manual` as const, + }, + MONEY_REQUEST_CREATE_TAB_SCAN: { + route: 'create/:iouType/start/:transactionID/:reportID/scan', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/scan` as const, + }, + IOU_REQUEST: 'request/new', IOU_SEND: 'send/new', IOU_SEND_ADD_BANK_ACCOUNT: 'send/new/add-bank-account', @@ -391,6 +468,7 @@ const ROUTES = { MONEY2020: 'money2020', } as const; +export {getUrlWithBackToParam}; export default ROUTES; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f4cbcf4f2564..921f57953482 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -2,6 +2,7 @@ * This is a file containing constants for all of the screen names. In most cases, we should use the routes for * navigation. But there are situations where we may need to access screen names directly. */ +import DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', @@ -18,27 +19,225 @@ const SCREENS = { UNLINK_LOGIN: 'UnlinkLogin', SETTINGS: { ROOT: 'Settings_Root', - PREFERENCES: 'Settings_Preferences', + SHARE_CODE: 'Settings_Share_Code', WORKSPACES: 'Settings_Workspaces', SECURITY: 'Settings_Security', - STATUS: 'Settings_Status', - WALLET: 'Settings_Wallet', - WALLET_DOMAIN_CARD: 'Settings_Wallet_DomainCard', - WALLET_CARD_GET_PHYSICAL: { - NAME: 'Settings_Card_Get_Physical_Name', - PHONE: 'Settings_Card_Get_Physical_Phone', - ADDRESS: 'Settings_Card_Get_Physical_Address', - CONFIRM: 'Settings_Card_Get_Physical_Confirm', + ABOUT: 'Settings_About', + APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', + LOUNGE_ACCESS: 'Settings_Lounge_Access', + ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', + ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', + CLOSE: 'Settings_Close', + TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth', + REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', + + PROFILE: { + ROOT: 'Settings_Profile', + DISPLAY_NAME: 'Settings_Display_Name', + CONTACT_METHODS: 'Settings_ContactMethods', + CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', + NEW_CONTACT_METHOD: 'Settings_NewContactMethod', + STATUS: 'Settings_Status', + STATUS_SET: 'Settings_Status_Set', + PRONOUNS: 'Settings_Pronouns', + TIMEZONE: 'Settings_Timezone', + TIMEZONE_SELECT: 'Settings_Timezone_Select', + + PERSONAL_DETAILS: { + INITIAL: 'Settings_PersonalDetails_Initial', + LEGAL_NAME: 'Settings_PersonalDetails_LegalName', + DATE_OF_BIRTH: 'Settings_PersonalDetails_DateOfBirth', + ADDRESS: 'Settings_PersonalDetails_Address', + ADDRESS_COUNTRY: 'Settings_PersonalDetails_Address_Country', + }, + }, + + PREFERENCES: { + ROOT: 'Settings_Preferences', + PRIORITY_MODE: 'Settings_Preferences_PriorityMode', + LANGUAGE: 'Settings_Preferences_Language', + THEME: 'Settings_Preferences_Theme', + }, + + WALLET: { + ROOT: 'Settings_Wallet', + DOMAIN_CARD: 'Settings_Wallet_DomainCard', + CARD_GET_PHYSICAL: { + NAME: 'Settings_Card_Get_Physical_Name', + PHONE: 'Settings_Card_Get_Physical_Phone', + ADDRESS: 'Settings_Card_Get_Physical_Address', + CONFIRM: 'Settings_Card_Get_Physical_Confirm', + }, + TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', + CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', + ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', + REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', + CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', }, }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', }, + RIGHT_MODAL: { + SETTINGS: 'Settings', + NEW_CHAT: 'NewChat', + SEARCH: 'Search', + DETAILS: 'Details', + PROFILE: 'Profile', + REPORT_DETAILS: 'Report_Details', + REPORT_SETTINGS: 'Report_Settings', + REPORT_WELCOME_MESSAGE: 'Report_WelcomeMessage', + PARTICIPANTS: 'Participants', + MONEY_REQUEST: 'MoneyRequest', + NEW_TASK: 'NewTask', + TEACHERS_UNITE: 'TeachersUnite', + TASK_DETAILS: 'Task_Details', + ENABLE_PAYMENTS: 'EnablePayments', + SPLIT_DETAILS: 'SplitDetails', + ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + WALLET_STATEMENT: 'Wallet_Statement', + FLAG_COMMENT: 'Flag_Comment', + EDIT_REQUEST: 'EditRequest', + SIGN_IN: 'SignIn', + PRIVATE_NOTES: 'Private_Notes', + ROOM_MEMBERS: 'RoomMembers', + ROOM_INVITE: 'RoomInvite', + REFERRAL: 'Referral', + }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', + + MONEY_REQUEST: { + MANUAL_TAB: 'manual', + SCAN_TAB: 'scan', + DISTANCE_TAB: 'distance', + CREATE: 'Money_Request_Create', + STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', + STEP_AMOUNT: 'Money_Request_Step_Amount', + STEP_CATEGORY: 'Money_Request_Step_Category', + STEP_CURRENCY: 'Money_Request_Step_Currency', + STEP_DATE: 'Money_Request_Step_Date', + STEP_DESCRIPTION: 'Money_Request_Step_Description', + STEP_DISTANCE: 'Money_Request_Step_Distance', + STEP_MERCHANT: 'Money_Request_Step_Merchant', + STEP_PARTICIPANTS: 'Money_Request_Step_Participants', + STEP_SCAN: 'Money_Request_Step_Scan', + STEP_TAG: 'Money_Request_Step_Tag', + STEP_WAYPOINT: 'Money_Request_Step_Waypoint', + ROOT: 'Money_Request', + AMOUNT: 'Money_Request_Amount', + PARTICIPANTS: 'Money_Request_Participants', + CONFIRMATION: 'Money_Request_Confirmation', + CURRENCY: 'Money_Request_Currency', + DATE: 'Money_Request_Date', + DESCRIPTION: 'Money_Request_Description', + CATEGORY: 'Money_Request_Category', + TAG: 'Money_Request_Tag', + MERCHANT: 'Money_Request_Merchant', + WAYPOINT: 'Money_Request_Waypoint', + EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', + DISTANCE: 'Money_Request_Distance', + RECEIPT: 'Money_Request_Receipt', + }, + + IOU_SEND: { + ADD_BANK_ACCOUNT: 'IOU_Send_Add_Bank_Account', + ADD_DEBIT_CARD: 'IOU_Send_Add_Debit_Card', + ENABLE_PAYMENTS: 'IOU_Send_Enable_Payments', + }, + + REPORT_SETTINGS: { + ROOT: 'Report_Settings_Root', + ROOM_NAME: 'Report_Settings_Room_Name', + NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', + WRITE_CAPABILITY: 'Report_Settings_Write_Capability', + }, + + NEW_TASK: { + ROOT: 'NewTask_Root', + TASK_ASSIGNEE_SELECTOR: 'NewTask_TaskAssigneeSelector', + TASK_SHARE_DESTINATION_SELECTOR: 'NewTask_TaskShareDestinationSelector', + DETAILS: 'NewTask_Details', + TITLE: 'NewTask_Title', + DESCRIPTION: 'NewTask_Description', + }, + + TASK: { + TITLE: 'Task_Title', + DESCRIPTION: 'Task_Description', + ASSIGNEE: 'Task_Assignee', + }, + + PRIVATE_NOTES: { + VIEW: 'PrivateNotes_View', + LIST: 'PrivateNotes_List', + EDIT: 'PrivateNotes_Edit', + }, + + REPORT_DETAILS: { + ROOT: 'Report_Details_Root', + SHARE_CODE: 'Report_Details_Share_Code', + }, + + WORKSPACE: { + INITIAL: 'Workspace_Initial', + SETTINGS: 'Workspace_Settings', + CARD: 'Workspace_Card', + REIMBURSE: 'Workspace_Reimburse', + RATE_AND_UNIT: 'Workspace_RateAndUnit', + BILLS: 'Workspace_Bills', + INVOICES: 'Workspace_Invoices', + TRAVEL: 'Workspace_Travel', + MEMBERS: 'Workspace_Members', + INVITE: 'Workspace_Invite', + INVITE_MESSAGE: 'Workspace_Invite_Message', + CURRENCY: 'Workspace_Settings_Currency', + }, + + EDIT_REQUEST: { + ROOT: 'EditRequest_Root', + CURRENCY: 'EditRequest_Currency', + }, + + NEW_CHAT: { + ROOT: 'NewChat_Root', + NEW_CHAT: 'chat', + NEW_ROOM: 'room', + }, + + SPLIT_DETAILS: { + ROOT: 'SplitDetails_Root', + EDIT_REQUEST: 'SplitDetails_Edit_Request', + EDIT_CURRENCY: 'SplitDetails_Edit_Currency', + }, + + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', + INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', + I_AM_A_TEACHER: 'I_Am_A_Teacher', + ENABLE_PAYMENTS_ROOT: 'EnablePayments_Root', + ADD_PERSONAL_BANK_ACCOUNT_ROOT: 'AddPersonalBankAccount_Root', + REIMBURSEMENT_ACCOUNT_ROOT: 'Reimbursement_Account_Root', + WALLET_STATEMENT_ROOT: 'WalletStatement_Root', + SIGN_IN_ROOT: 'SignIn_Root', + DETAILS_ROOT: 'Details_Root', + PROFILE_ROOT: 'Profile_Root', + REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root', + REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', + ROOM_MEMBERS_ROOT: 'RoomMembers_Root', + ROOM_INVITE_ROOT: 'RoomInvite_Root', + SEARCH_ROOT: 'Search_Root', + FLAG_COMMENT_ROOT: 'FlagComment_Root', + REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', + GET_ASSISTANCE: 'GetAssistance', + REFERRAL_DETAILS: 'Referral_Details', + KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', } as const; +type Screen = DeepValueOf; + export default SCREENS; export {PROTECTED_SCREENS}; +export type {Screen}; diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index c18b706e1acf..68d529c4a78d 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -168,7 +168,7 @@ function AddPlaidBankAccount({ value: account.plaidAccountID, label: `${account.addressName} ${account.mask}`, })); - const {icon, iconSize, iconStyles} = getBankIcon({themeStyles: styles}); + const {icon, iconSize, iconStyles} = getBankIcon({styles}); const plaidErrors = lodashGet(plaidData, 'errors'); const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : ''; const bankName = lodashGet(plaidData, 'bankName'); diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 98650f94232b..1621328d388f 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -11,7 +11,8 @@ import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import AddressSearch from './AddressSearch'; import CountrySelector from './CountrySelector'; -import Form from './Form'; +import FormProvider from './Form/FormProvider'; +import InputWrapper from './Form/InputWrapper'; import StatePicker from './StatePicker'; import TextInput from './TextInput'; @@ -115,7 +116,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS }, []); return ( -
- { @@ -146,7 +148,8 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS /> - - {isUSAForm ? ( - ) : ( - )} - - - + ); } diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.js index 6f5148edd436..90d2c15733f1 100644 --- a/src/components/AddressSearch/CurrentLocationButton.js +++ b/src/components/AddressSearch/CurrentLocationButton.js @@ -7,8 +7,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; import colors from '@styles/colors'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; const propTypes = { @@ -25,14 +24,14 @@ const defaultProps = { }; function CurrentLocationButton({onPress, isDisabled}) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( + () => { ReportActionContextMenu.hideContextMenu(); diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..712ef6be769e 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -8,7 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import Banner from './Banner'; type ArchivedReportFooterOnyxProps = { @@ -16,7 +16,7 @@ type ArchivedReportFooterOnyxProps = { reportClosedAction: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 57b0c6466a7f..79be536945ac 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import lodashExtend from 'lodash/extend'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -19,8 +19,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import useNativeDriver from '@libs/useNativeDriver'; import reportPropTypes from '@pages/reportPropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -90,6 +90,9 @@ const propTypes = { /** Denotes whether it is a workspace avatar or not */ isWorkspaceAvatar: PropTypes.bool, + + /** Whether it is a receipt attachment or not */ + isReceiptAttachment: PropTypes.bool, }; const defaultProps = { @@ -107,18 +110,18 @@ const defaultProps = { onModalHide: () => {}, onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, + isReceiptAttachment: false, }; function AttachmentModal(props) { const theme = useTheme(); const styles = useThemeStyles(); - const onModalHideCallbackRef = useRef(null); + const StyleUtils = useStyleUtils(); const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false); const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); - const [isAttachmentReceipt, setIsAttachmentReceipt] = useState(null); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); const [source, setSource] = useState(props.source); @@ -154,7 +157,6 @@ function AttachmentModal(props) { (attachment) => { setSource(attachment.source); setFile(attachment.file); - setIsAttachmentReceipt(attachment.isReceipt); setIsAuthTokenRequired(attachment.isAuthTokenRequired); onCarouselAttachmentChange(attachment); }, @@ -357,7 +359,7 @@ function AttachmentModal(props) { const sourceForAttachmentView = props.source || source; const threeDotsMenuItems = useMemo(() => { - if (!isAttachmentReceipt || !props.parentReport || !props.parentReportActions) { + if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { return []; } const menuItems = []; @@ -371,8 +373,8 @@ function AttachmentModal(props) { icon: Expensicons.Camera, text: props.translate('common.replace'), onSelected: () => { - onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); closeModal(); + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); }, }); } @@ -392,17 +394,17 @@ function AttachmentModal(props) { } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAttachmentReceipt, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); + }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. - // isAttachmentReceipt will be null until its certain what the file is, in which case it will then be true|false. + // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. let headerTitle = props.headerTitle; let shouldShowDownloadButton = false; let shouldShowThreeDotsButton = false; - if (!_.isNull(isAttachmentReceipt)) { - headerTitle = translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !isAttachmentReceipt && !isOffline; - shouldShowThreeDotsButton = isAttachmentReceipt && isModalOpen; + if (!_.isEmpty(props.report)) { + headerTitle = translate(props.isReceiptAttachment ? 'common.receipt' : 'common.attachment'); + shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !props.isReceiptAttachment && !isOffline; + shouldShowThreeDotsButton = props.isReceiptAttachment && isModalOpen; } return ( @@ -419,10 +421,6 @@ function AttachmentModal(props) { }} onModalHide={(e) => { props.onModalHide(e); - if (onModalHideCallbackRef.current) { - onModalHideCallbackRef.current(); - } - setShouldLoadAttachment(false); }} propagateSwipe @@ -443,7 +441,7 @@ function AttachmentModal(props) { shouldOverlay /> - {!_.isEmpty(props.report) ? ( + {!_.isEmpty(props.report) && !props.isReceiptAttachment ? ( ) )} @@ -486,7 +485,7 @@ function AttachmentModal(props) { )} )} - {isAttachmentReceipt && ( + {props.isReceiptAttachment && ( )} - {!isAttachmentReceipt && ( + {!props.isReceiptAttachment && ( { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } - // We're handling receipts differently here because receipt images are not - // part of the report action message, the images are constructed client-side - if (ReportActionsUtils.isMoneyRequestAction(action)) { - const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']); - if (!transactionID) { - return; - } - - if (TransactionUtils.hasReceipt(transaction)) { - const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction); - const isLocalFile = typeof image === 'string' && _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => image.startsWith(prefix)); - attachments.unshift({ - source: tryResolveUrlFromApiRoot(image), - isAuthTokenRequired: !isLocalFile, - file: {name: transaction.filename}, - isReceipt: true, - transactionID, - }); - return; - } - } - const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], ''); const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN; const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 141e619e489e..fb31e32de91c 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {FlatList, Keyboard, PixelRatio, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -28,7 +27,7 @@ const viewabilityConfig = { itemVisiblePercentThreshold: 95, }; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) { const styles = useThemeStyles(); const scrollRef = useRef(null); @@ -39,21 +38,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); - const [isReceipt, setIsReceipt] = useState(false); - const compareImage = useCallback( - (attachment) => { - if (attachment.isReceipt && isReceipt) { - return attachment.transactionID === transaction.transactionID; - } - return attachment.source === source; - }, - [source, isReceipt, transaction], - ); + const compareImage = useCallback((attachment) => attachment.source === source, [source]); useEffect(() => { const parentReportAction = parentReportActions[report.parentReportActionID]; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction); + const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); const initialPage = _.findIndex(attachmentsFromReport, compareImage); @@ -88,12 +78,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, // to get the index of the current page const entry = _.first(viewableItems); if (!entry) { - setIsReceipt(false); setActiveSource(null); return; } - setIsReceipt(entry.item.isReceipt); setPage(entry.index); setActiveSource(entry.item.source); @@ -227,7 +215,6 @@ AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; export default compose( - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ reportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, @@ -241,15 +228,6 @@ export default compose( canEvict: false, }, }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), withLocalize, withWindowDimensions, )(AttachmentCarousel); diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 6bf4e63c01e7..ea45509d6ce3 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Keyboard, PixelRatio, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -18,7 +17,7 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport'; import AttachmentCarouselPager from './Pager'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction, onClose}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) { const styles = useThemeStyles(); const pagerRef = useRef(null); @@ -28,21 +27,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [activeSource, setActiveSource] = useState(source); const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); - const [isReceipt, setIsReceipt] = useState(false); - const compareImage = useCallback( - (attachment) => { - if (attachment.isReceipt && isReceipt) { - return attachment.transactionID === transaction.transactionID; - } - return attachment.source === source; - }, - [source, isReceipt, transaction], - ); + const compareImage = useCallback((attachment) => attachment.source === source, [source]); useEffect(() => { const parentReportAction = parentReportActions[report.parentReportActionID]; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction); + const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); const initialPage = _.findIndex(attachmentsFromReport, compareImage); @@ -77,7 +67,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const item = attachments[newPageIndex]; setPage(newPageIndex); - setIsReceipt(item.isReceipt); setActiveSource(item.source); onNavigate(item); @@ -172,7 +161,6 @@ AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; export default compose( - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ reportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, @@ -186,14 +174,5 @@ export default compose( canEvict: false, }, }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), withLocalize, )(AttachmentCarousel); diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index e484abe041b9..6e1ed651ae06 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -15,8 +15,8 @@ import useNetwork from '@hooks/useNetwork'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; import * as TransactionUtils from '@libs/TransactionUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import cursor from '@styles/utilities/cursor'; import variables from '@styles/variables'; @@ -80,6 +80,7 @@ function AttachmentView({ }) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); const [imageError, setImageError] = useState(false); diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index efde2b24992f..07db455968a3 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -5,8 +5,7 @@ import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; @@ -39,8 +38,8 @@ function BaseAutoCompleteSuggestions( }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const scrollRef = useRef>(null); /** @@ -49,7 +48,7 @@ function BaseAutoCompleteSuggestions( const renderItem = useCallback( ({item, index}: RenderSuggestionMenuItemProps): ReactElement => ( StyleUtils.getAutoCompleteSuggestionItemStyle(theme, highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} + style={({hovered}) => StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)} hoverDimmingValue={1} onMouseDown={(e) => e.preventDefault()} onPress={() => onSelect(index)} @@ -59,7 +58,7 @@ function BaseAutoCompleteSuggestions( {renderSuggestionMenuItem(item, index)} ), - [highlightedSuggestionIndex, renderSuggestionMenuItem, onSelect, accessibilityLabelExtractor, theme], + [accessibilityLabelExtractor, renderSuggestionMenuItem, StyleUtils, highlightedSuggestionIndex, onSelect], ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 24b846c265a9..3ccbb4efaf5a 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import {View} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; @@ -15,6 +15,7 @@ import type {AutoCompleteSuggestionsProps} from './types'; */ function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { + const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); const [{width, left, bottom}, setContainerState] = React.useState({ diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index d394a84bd207..f801cb11e9df 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -3,10 +3,10 @@ import {StyleProp, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import * as ReportUtils from '@libs/ReportUtils'; import {AvatarSource} from '@libs/UserUtils'; -import * as StyleUtils from '@styles/StyleUtils'; -import type {AvatarSizeName} from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; +import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; import {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; @@ -60,6 +60,7 @@ function Avatar({ }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [imageError, setImageError] = useState(false); useNetwork({onReconnect: () => setImageError(false)}); @@ -75,8 +76,8 @@ function Avatar({ const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle = [StyleUtils.getAvatarStyle(theme, size), imageStyles, styles.noBorderRadius]; - const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(theme, size), styles.bgTransparent, imageStyles] : undefined; + const imageStyle = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill ?? theme.icon; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js index a37f228a0d0d..dcb0470c5ee5 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.js @@ -17,8 +17,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ImageCropView from './ImageCropView'; @@ -63,6 +63,7 @@ const defaultProps = { function AvatarCropModal(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const translateY = useSharedValue(0); diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.js index a50409da64f4..94289b24d6ca 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.js @@ -6,7 +6,7 @@ import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import ControlSelection from '@libs/ControlSelection'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import gestureHandlerPropTypes from './gestureHandlerPropTypes'; @@ -51,6 +51,7 @@ const defaultProps = { function ImageCropView(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); const originalImageHeight = props.originalImageHeight; diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 9229cb80cf4c..041c180595f1 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -5,8 +5,8 @@ import {ValueOf} from 'type-fest'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -55,6 +55,7 @@ function AvatarWithDisplayName({ }: AvatarWithDisplayNameProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index ff963589d80f..82212c66db04 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,6 +1,6 @@ import React, {useCallback} from 'react'; import {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; @@ -34,12 +34,13 @@ type BadgeProps = { function Badge({success = false, error = false, pressable = false, text, environment = CONST.ENVIRONMENT.DEV, badgeStyles, textStyles, onPress = () => {}}: BadgeProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const textColorStyles = success || error ? styles.textWhite : undefined; const Wrapper = pressable ? PressableWithoutFeedback : View; const wrapperStyles: (state: PressableStateCallbackType) => StyleProp = useCallback( - ({pressed}) => [styles.badge, styles.ml2, StyleUtils.getBadgeColorStyle(styles, success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), badgeStyles], - [success, error, environment, badgeStyles, styles], + ({pressed}) => [styles.badge, styles.ml2, StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC), badgeStyles], + [styles.badge, styles.ml2, StyleUtils, success, error, environment, badgeStyles], ); return ( diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 6e5ad8970f1a..cfe817c849c0 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -2,8 +2,7 @@ import React, {memo} from 'react'; import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Hoverable from './Hoverable'; @@ -41,8 +40,8 @@ type BannerProps = { }; function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( @@ -67,7 +66,7 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend )} diff --git a/src/components/BaseMiniContextMenuItem.js b/src/components/BaseMiniContextMenuItem.js deleted file mode 100644 index 3252938e4ca5..000000000000 --- a/src/components/BaseMiniContextMenuItem.js +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import DomUtils from '@libs/DomUtils'; -import getButtonState from '@libs/getButtonState'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import variables from '@styles/variables'; -import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; -import Tooltip from './Tooltip/PopoverAnchorTooltip'; - -const propTypes = { - /** - * Text to display when hovering the menu item - */ - tooltipText: PropTypes.string.isRequired, - - /** - * Callback to fire on press - */ - onPress: PropTypes.func.isRequired, - - /** - * The children to display within the menu item - */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** - * Whether the button should be in the active state - */ - isDelayButtonStateComplete: PropTypes.bool, - - /** - * A ref to forward to the Pressable - */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), -}; - -const defaultProps = { - isDelayButtonStateComplete: true, - innerRef: () => {}, -}; - -/** - * Component that renders a mini context menu item with a - * pressable. Also renders a tooltip when hovering the item. - * @param {Object} props - * @returns {JSX.Element} - */ -function BaseMiniContextMenuItem(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - return ( - - { - if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { - DomUtils.getActiveElement().blur(); - return; - } - - // Allow text input blur on right click - if (!e || e.button === 2) { - return; - } - - // Prevent text input blur on left click - e.preventDefault(); - }} - accessibilityLabel={props.tooltipText} - style={({hovered, pressed}) => [ - styles.reportActionContextMenuMiniButton, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed, props.isDelayButtonStateComplete)), - props.isDelayButtonStateComplete && styles.cursorDefault, - ]} - > - {(pressableState) => ( - - {_.isFunction(props.children) ? props.children(pressableState) : props.children} - - )} - - - ); -} - -BaseMiniContextMenuItem.propTypes = propTypes; -BaseMiniContextMenuItem.defaultProps = defaultProps; -BaseMiniContextMenuItem.displayName = 'BaseMiniContextMenuItem'; - -const BaseMiniContextMenuItemWithRef = React.forwardRef((props, ref) => ( - -)); - -BaseMiniContextMenuItemWithRef.displayName = 'BaseMiniContextMenuItemWithRef'; - -export default BaseMiniContextMenuItemWithRef; diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/BaseMiniContextMenuItem.tsx new file mode 100644 index 000000000000..082c0e20801a --- /dev/null +++ b/src/components/BaseMiniContextMenuItem.tsx @@ -0,0 +1,85 @@ +import React, {ForwardedRef} from 'react'; +import {PressableStateCallbackType, View} from 'react-native'; +import DomUtils from '@libs/DomUtils'; +import getButtonState from '@libs/getButtonState'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import variables from '@styles/variables'; +import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; +import Tooltip from './Tooltip/PopoverAnchorTooltip'; + +type BaseMiniContextMenuItemProps = { + /** + * Text to display when hovering the menu item + */ + tooltipText: string; + + /** + * Callback to fire on press + */ + onPress: () => void; + + /** + * The children to display within the menu item + */ + children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); + + /** + * Whether the button should be in the active state + */ + isDelayButtonStateComplete: boolean; +}; + +/** + * Component that renders a mini context menu item with a + * pressable. Also renders a tooltip when hovering the item. + */ +function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonStateComplete = true}: BaseMiniContextMenuItemProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + return ( + + { + if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { + const activeElement = DomUtils.getActiveElement(); + if (activeElement instanceof HTMLElement) { + activeElement?.blur(); + } + return; + } + + // Allow text input blur on right click + if (!event || event.button === 2) { + return; + } + + // Prevent text input blur on left click + event.preventDefault(); + }} + accessibilityLabel={tooltipText} + style={({hovered, pressed}) => [ + styles.reportActionContextMenuMiniButton, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isDelayButtonStateComplete)), + isDelayButtonStateComplete && styles.cursorDefault, + ]} + > + {(pressableState) => ( + + {typeof children === 'function' ? children(pressableState) : children} + + )} + + + ); +} + +BaseMiniContextMenuItem.displayName = 'BaseMiniContextMenuItem'; + +export default React.forwardRef(BaseMiniContextMenuItem); diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index b6861eb820b1..9cbd19e03dc7 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; +import useActiveElement from '@hooks/useActiveElement'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import HapticFeedback from '@libs/HapticFeedback'; import useTheme from '@styles/themes/useTheme'; @@ -158,6 +159,9 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const isFocused = useIsFocused(); + const activeElement = useActiveElement(); + const accessibilityRoles: string[] = Object.values(CONST.ACCESSIBILITY_ROLE); + const shouldDisableEnterShortcut = accessibilityRoles.includes(activeElement?.role ?? '') && activeElement?.role !== CONST.ACCESSIBILITY_ROLE.TEXT; const keyboardShortcutCallback = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { @@ -170,7 +174,7 @@ function Button( ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, { - isActive: pressOnEnter, + isActive: pressOnEnter && !shouldDisableEnterShortcut, shouldBubble: allowBubble, priority: enterKeyEventListenerPriority, shouldPreventDefault: false, diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.js index 15f2e2f4d6de..a5f311740f19 100644 --- a/src/components/ButtonWithDropdownMenu.js +++ b/src/components/ButtonWithDropdownMenu.js @@ -3,8 +3,8 @@ import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Button from './Button'; @@ -74,12 +74,13 @@ const defaultProps = { function ButtonWithDropdownMenu(props) { 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 {windowWidth, windowHeight} = useWindowDimensions(); const caretButton = useRef(null); - const selectedItem = props.options[selectedItemIndex]; + const selectedItem = props.options[selectedItemIndex] || _.first(props.options); const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(props.buttonSize); const isButtonSizeLarge = props.buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; diff --git a/src/components/CardPreview.js b/src/components/CardPreview.tsx similarity index 62% rename from src/components/CardPreview.js rename to src/components/CardPreview.tsx index df944d930a92..6dc8abfb80ef 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.tsx @@ -1,41 +1,28 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PrivatePersonalDetails, Session} from '@src/types/onyx'; import Text from './Text'; -const propTypes = { +type CardPreviewOnyxProps = { /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), + privatePersonalDetails: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged-in user email */ - email: PropTypes.string, - }), + session: OnyxEntry; }; -const defaultProps = { - privatePersonalDetails: { - legalFirstName: '', - legalLastName: '', - }, - session: { - email: '', - }, -}; +type CardPreviewProps = CardPreviewOnyxProps; -function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, session: {email}}) { +function CardPreview({privatePersonalDetails, session}: CardPreviewProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); - const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : email; + const {legalFirstName, legalLastName} = privatePersonalDetails ?? {}; + const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : session?.email ?? ''; return ( @@ -55,11 +42,9 @@ function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, s ); } -CardPreview.propTypes = propTypes; -CardPreview.defaultProps = defaultProps; CardPreview.displayName = 'CardPreview'; -export default withOnyx({ +export default withOnyx({ privatePersonalDetails: { key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22577ec2b7f9..5dd3164eadcc 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,7 +1,7 @@ import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -63,6 +63,7 @@ function Checkbox( ) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const handleSpaceKey = (event?: ReactKeyboardEvent) => { if (event?.code !== 'Space') { @@ -98,7 +99,7 @@ function Checkbox( {children ?? ( {children}; + return {children}; } export default ColorSchemeWrapper; diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 48eb89bb0296..4bb3df5c1b85 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -15,8 +15,8 @@ import * as ComposerUtils from '@libs/ComposerUtils'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -168,6 +168,7 @@ function Composer({ }) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); const textInput = useRef(null); @@ -428,7 +429,7 @@ function Composer({ Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {}, ], - [numberOfLines, maxLines, styles.overflowHidden, styles.rtlTextRenderForSafari, style, isComposerFullSize], + [numberOfLines, maxLines, styles.overflowHidden, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize], ); return ( diff --git a/src/components/ConfirmedRoute.js b/src/components/ConfirmedRoute.js index 4ddd537fdd7d..79b97b38194a 100644 --- a/src/components/ConfirmedRoute.js +++ b/src/components/ConfirmedRoute.js @@ -115,9 +115,6 @@ function ConfirmedRoute({mapboxAccessToken, transaction}) { } export default withOnyx({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, mapboxAccessToken: { key: ONYXKEYS.MAPBOX_ACCESS_TOKEN, }, diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js index a5a8985e3978..2cabd71b11cb 100644 --- a/src/components/ContextMenuItem.js +++ b/src/components/ContextMenuItem.js @@ -3,9 +3,7 @@ import React, {forwardRef, useImperativeHandle} from 'react'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getButtonState from '@libs/getButtonState'; -import getContextMenuItemStyles from '@styles/getContextMenuItemStyles'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import BaseMiniContextMenuItem from './BaseMiniContextMenuItem'; import Icon from './Icon'; @@ -54,8 +52,8 @@ const defaultProps = { }; function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, description, isAnonymousAction, isFocused, innerRef}) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); const [isThrottledButtonActive, setThrottledButtonInactive] = useThrottledButtonState(); @@ -87,7 +85,7 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, )} @@ -99,8 +97,8 @@ function ContextMenuItem({onPress, successIcon, successText, icon, text, isMini, wrapperStyle={styles.pr9} success={!isThrottledButtonActive} description={description} - descriptionTextStyle={styles.breakAll} - style={getContextMenuItemStyles(styles, windowWidth)} + descriptionTextStyle={styles.breakWord} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} isAnonymousAction={isAnonymousAction} focused={isFocused} interactive={isThrottledButtonActive} diff --git a/src/components/CopyTextToClipboard.js b/src/components/CopyTextToClipboard.js index acd3f08f2b22..678537c6a3d7 100644 --- a/src/components/CopyTextToClipboard.js +++ b/src/components/CopyTextToClipboard.js @@ -13,12 +13,14 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types textStyles: PropTypes.arrayOf(PropTypes.object), urlToCopy: PropTypes.string, + accessibilityRole: PropTypes.string, ...withLocalizePropTypes, }; const defaultProps = { textStyles: [], urlToCopy: null, + accessibilityRole: undefined, }; function CopyTextToClipboard(props) { @@ -34,6 +36,7 @@ function CopyTextToClipboard(props) { icon={Expensicons.Copy} textStyles={props.textStyles} onPress={copyToClipboard} + accessibilityRole={props.accessibilityRole} /> ); } diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 13fc215f1d8c..b138bc949937 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -7,6 +7,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import refPropTypes from './refPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ @@ -23,7 +24,7 @@ const propTypes = { inputID: PropTypes.string.isRequired, /** React ref being forwarded to the MenuItemWithTopDescription */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, }; const defaultProps = { diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx index 685db8031330..6f6daddc51b8 100644 --- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx +++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx @@ -3,8 +3,8 @@ import {View} from 'react-native'; import {Circle, Rect} from 'react-native-svg'; import {ValueOf} from 'type-fest'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -26,6 +26,7 @@ type CurrentUserPersonalDetailsSkeletonViewProps = { function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE, backgroundColor, foregroundColor}: CurrentUserPersonalDetailsSkeletonViewProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize); const avatarPlaceholderRadius = avatarPlaceholderSize / 2; const spaceBetweenAvatarAndHeadline = styles.mb3.marginBottom + styles.mt1.marginTop + (variables.lineHeightXXLarge - variables.fontSizeXLarge) / 2; diff --git a/src/components/DatePicker/CalendarPicker/ArrowIcon.js b/src/components/DatePicker/CalendarPicker/ArrowIcon.js index 1b9c9a06db34..a03e18085706 100644 --- a/src/components/DatePicker/CalendarPicker/ArrowIcon.js +++ b/src/components/DatePicker/CalendarPicker/ArrowIcon.js @@ -3,7 +3,7 @@ import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -22,6 +22,7 @@ const defaultProps = { function ArrowIcon(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index eaa6a8b45b33..a404c4746397 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -8,12 +8,11 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; import ArrowIcon from './ArrowIcon'; import generateMonthMatrix from './generateMonthMatrix'; @@ -34,7 +33,7 @@ const propTypes = { ...withLocalizePropTypes, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; const defaultProps = { @@ -238,7 +237,7 @@ class CalendarPicker extends React.PureComponent { style={[ this.props.themeStyles.calendarDayContainer, isSelected ? this.props.themeStyles.calendarDayContainerSelected : {}, - !isDisabled ? StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(hovered, pressed)) : {}, + !isDisabled ? this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed)) : {}, ]} > {day} @@ -264,4 +263,4 @@ class CalendarPicker extends React.PureComponent { CalendarPicker.propTypes = propTypes; CalendarPicker.defaultProps = defaultProps; -export default compose(withLocalize, withTheme, withThemeStyles)(CalendarPicker); +export default compose(withLocalize, withThemeStyles, withStyleUtils)(CalendarPicker); diff --git a/src/components/DistanceMapView/index.android.js b/src/components/DistanceMapView/index.android.js index 848167de653d..532d42ac0be5 100644 --- a/src/components/DistanceMapView/index.android.js +++ b/src/components/DistanceMapView/index.android.js @@ -6,12 +6,13 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MapView from '@components/MapView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as distanceMapViewPropTypes from './distanceMapViewPropTypes'; function DistanceMapView(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isMapReady, setIsMapReady] = useState(false); const {isOffline} = useNetwork(); const {translate} = useLocalize(); diff --git a/src/components/DistanceRequest/DistanceRequestFooter.js b/src/components/DistanceRequest/DistanceRequestFooter.js index b212dae615e4..d374f90a1b6c 100644 --- a/src/components/DistanceRequest/DistanceRequestFooter.js +++ b/src/components/DistanceRequest/DistanceRequestFooter.js @@ -41,7 +41,7 @@ const propTypes = { expiration: PropTypes.string, }), - /* Onyx Props */ + /** The transaction being interacted with */ transaction: transactionPropTypes, }; @@ -135,9 +135,6 @@ DistanceRequestFooter.propTypes = propTypes; DistanceRequestFooter.defaultProps = defaultProps; export default withOnyx({ - transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - }, mapboxAccessToken: { key: ONYXKEYS.MAPBOX_ACCESS_TOKEN, }, diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index 6fa5dfede620..be34a42ead5e 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -23,6 +23,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as MapboxToken from '@userActions/MapboxToken'; import * as Transaction from '@userActions/Transaction'; +import * as TransactionEdit from '@userActions/TransactionEdit'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import DistanceRequestFooter from './DistanceRequestFooter'; @@ -38,6 +39,9 @@ const propTypes = { /** Are we editing an existing distance request, or creating a new one? */ isEditingRequest: PropTypes.bool, + /** Are we editing the distance while creating a new distance request */ + isEditingNewRequest: PropTypes.bool, + /** Called on submit of this page */ onSubmit: PropTypes.func.isRequired, @@ -61,17 +65,17 @@ const defaultProps = { transactionID: '', report: {}, isEditingRequest: false, + isEditingNewRequest: false, transaction: {}, }; -function DistanceRequest({transactionID, report, transaction, route, isEditingRequest, onSubmit}) { +function DistanceRequest({transactionID, report, transaction, route, isEditingRequest, isEditingNewRequest, onSubmit}) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const [optimisticWaypoints, setOptimisticWaypoints] = useState(null); const [hasError, setHasError] = useState(false); - const isEditing = Navigation.getActiveRoute().includes('address'); const reportID = lodashGet(report, 'reportID', ''); const waypoints = useMemo(() => optimisticWaypoints || lodashGet(transaction, 'comment.waypoints', {waypoint0: {}, waypoint1: {}}), [optimisticWaypoints, transaction]); const waypointsList = _.keys(waypoints); @@ -90,12 +94,36 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe const haveValidatedWaypointsChanged = !_.isEqual(previousValidatedWaypoints, validatedWaypoints); const isRouteAbsentWithoutErrors = !hasRoute && !hasRouteError; const shouldFetchRoute = (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && _.size(validatedWaypoints) > 1; + const transactionWasSaved = useRef(false); useEffect(() => { MapboxToken.init(); return MapboxToken.stop; }, []); + useEffect(() => { + if (!isEditingNewRequest && !isEditingRequest) { + return () => {}; + } + // This effect runs when the component is mounted and unmounted. It's purpose is to be able to properly + // discard changes if the user cancels out of making any changes. This is accomplished by backing up the + // original transaction, letting the user modify the current transaction, and then if the user ever + // cancels out of the modal without saving changes, the original transaction is restored from the backup. + + // On mount, create the backup transaction. + TransactionEdit.createBackupTransaction(transaction); + + return () => { + // If the user cancels out of the modal without without saving changes, then the original transaction + // needs to be restored from the backup so that all changes are removed. + if (transactionWasSaved.current) { + return; + } + TransactionEdit.restoreOriginalTransactionFromBackup(transaction.transactionID); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { const transactionWaypoints = lodashGet(transaction, 'comment.waypoints', {}); if (!lodashGet(transaction, 'transactionID') || !_.isEmpty(transactionWaypoints)) { @@ -134,7 +162,7 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe }, [waypoints, previousWaypoints]); const navigateBack = () => { - Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); + Navigation.goBack(isEditingNewRequest ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); }; /** @@ -182,8 +210,13 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe setHasError(true); return; } + + if (isEditingNewRequest || isEditingRequest) { + transactionWasSaved.current = true; + } + onSubmit(waypoints); - }, [onSubmit, setHasError, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, waypoints]); + }, [onSubmit, setHasError, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, waypoints, isEditingNewRequest, isEditingRequest]); const content = ( <> @@ -211,7 +244,7 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe waypoints={waypoints} hasRouteError={hasRouteError} navigateToWaypointEditPage={navigateToWaypointEditPage} - transactionID={transactionID} + transaction={transaction} /> } /> @@ -238,7 +271,7 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe ); - if (!isEditing) { + if (!isEditingNewRequest) { return content; } diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index b90093e20fc3..6a7d78768ed7 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -3,8 +3,8 @@ import React from 'react'; import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import fileDownload from '@libs/fileDownload'; import * as Localize from '@libs/Localize'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import Icon from './Icon'; @@ -45,6 +45,7 @@ function isReceiptError(message: string | ReceiptError): message is ReceiptError function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndicatorMessageProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (Object.keys(messages).length === 0) { return null; @@ -92,7 +93,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica {message} diff --git a/src/components/EReceipt.js b/src/components/EReceipt.js index 85c753c7ccb3..f5e5b7f2f6b3 100644 --- a/src/components/EReceipt.js +++ b/src/components/EReceipt.js @@ -6,7 +6,7 @@ import useLocalize from '@hooks/useLocalize'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,6 +31,7 @@ const defaultProps = { function EReceipt({transaction, transactionID}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); // Get receipt colorway, or default to Yellow. diff --git a/src/components/EReceiptThumbnail.js b/src/components/EReceiptThumbnail.js index f54e246b8b1e..1f719862412b 100644 --- a/src/components/EReceiptThumbnail.js +++ b/src/components/EReceiptThumbnail.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -37,12 +37,9 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function getBackgroundImage(transaction) { - return backgroundImages[StyleUtils.getEReceiptColorCode(transaction)]; -} - function EReceiptThumbnail({transaction}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); // Get receipt colorway, or default to Yellow. const {backgroundColor: primaryColor, color: secondaryColor} = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)); @@ -75,6 +72,8 @@ function EReceiptThumbnail({transaction}) { receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; } + const backgroundImage = useMemo(() => backgroundImages[StyleUtils.getEReceiptColorCode(transaction)], [StyleUtils, transaction]); + return ( diff --git a/src/components/EmojiPicker/CategoryShortcutButton.js b/src/components/EmojiPicker/CategoryShortcutButton.js index aeb31dd87397..8738f210276e 100644 --- a/src/components/EmojiPicker/CategoryShortcutButton.js +++ b/src/components/EmojiPicker/CategoryShortcutButton.js @@ -5,8 +5,8 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -25,6 +25,7 @@ const propTypes = { function CategoryShortcutButton(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [isHighlighted, setIsHighlighted] = useState(false); @@ -38,11 +39,7 @@ function CategoryShortcutButton(props) { onPress={props.onPress} onHoverIn={() => setIsHighlighted(true)} onHoverOut={() => setIsHighlighted(false)} - style={({pressed}) => [ - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(false, pressed)), - styles.categoryShortcutButton, - isHighlighted && styles.emojiItemHighlighted, - ]} + style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]} accessibilityLabel={`emojiPicker.headers.${props.code}`} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js index 9fc1224f96c0..96d7ee88b816 100644 --- a/src/components/EmojiPicker/EmojiPicker.js +++ b/src/components/EmojiPicker/EmojiPicker.js @@ -6,7 +6,7 @@ import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import EmojiPickerMenu from './EmojiPickerMenu'; @@ -22,6 +22,7 @@ const propTypes = { const EmojiPicker = forwardRef((props, ref) => { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isEmojiPickerVisible, setIsEmojiPickerVisible] = useState(false); const [emojiPopoverAnchorPosition, setEmojiPopoverAnchorPosition] = useState({ horizontal: 0, diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index 2926d6346b1b..165646d4795d 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -6,8 +6,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; @@ -31,8 +30,8 @@ const defaultProps = { }; function EmojiPickerButton(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); @@ -41,7 +40,7 @@ function EmojiPickerButton(props) { [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(hovered, pressed))]} + style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={props.isDisabled} onPress={() => { if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { @@ -56,7 +55,7 @@ function EmojiPickerButton(props) { {({hovered, pressed}) => ( )} diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index 6fd24adf04aa..02a5954cb705 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -8,8 +8,7 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import CONST from '@src/CONST'; @@ -26,8 +25,8 @@ const defaultProps = { }; function EmojiPickerButtonDropdown(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []); @@ -66,7 +65,7 @@ function EmojiPickerButtonDropdown(props) { diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 91716cd7500e..95db6eb41167 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -18,7 +18,7 @@ import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import * as ReportUtils from '@libs/ReportUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -52,6 +52,7 @@ function EmojiPickerMenu(props) { const {forwardedRef, frequentlyUsedEmojis, preferredSkinTone, onEmojiSelected, preferredLocale, translate} = props; const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); @@ -308,19 +309,23 @@ function EmojiPickerMenu(props) { } const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); onEmojiSelected(emoji, item); + // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer. + keyBoardEvent.preventDefault(); + // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again. + keyBoardEvent.stopPropagation(); return; } // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input // is not focused, so that the navigation and tab cycling can be done using the keyboard without // interfering with the input behaviour. - if (!ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' || (keyBoardEvent.key === 'Shift' && searchInputRef.current && !searchInputRef.current.isFocused())) { setIsUsingKeyboardMovement(true); return; } // We allow typing in the search box if any key is pressed apart from Arrow keys. - if (searchInputRef.current && !searchInputRef.current.isFocused()) { + if (searchInputRef.current && !searchInputRef.current.isFocused() && ReportUtils.shouldAutoFocusOnKeyPress(keyBoardEvent)) { searchInputRef.current.focus(); } }, diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js index 772c32ff4a88..f1560c07b397 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js @@ -15,7 +15,7 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -43,6 +43,7 @@ const defaultProps = { function EmojiPickerMenu({preferredLocale, onEmojiSelected, preferredSkinTone, translate, frequentlyUsedEmojis}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const emojiList = useAnimatedRef(); // eslint-disable-next-line react-hooks/exhaustive-deps const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]); diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js index f674d3c4fa0e..ae2cdf46dfc0 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js @@ -2,11 +2,10 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import * as Browser from '@libs/Browser'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -35,7 +34,7 @@ const propTypes = { isHighlighted: PropTypes.bool, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; class EmojiPickerMenuItem extends PureComponent { @@ -100,7 +99,7 @@ class EmojiPickerMenuItem extends PureComponent { style={({pressed}) => [ this.props.isFocused ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, this.state.isHovered || this.props.isHighlighted ? this.props.themeStyles.emojiItemHighlighted : {}, - Browser.isMobile() && StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(false, pressed)), + Browser.isMobile() && this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), this.props.themeStyles.emojiItem, ]} accessibilityLabel={this.props.emoji} @@ -124,8 +123,8 @@ EmojiPickerMenuItem.defaultProps = { // Significantly speeds up re-renders of the EmojiPickerMenu's FlatList // by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withTheme( - withThemeStyles( +export default withThemeStyles( + withStyleUtils( React.memo( EmojiPickerMenuItem, (prevProps, nextProps) => prevProps.isFocused === nextProps.isFocused && prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji, diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js index ca24dde6b861..7934cc0f03d4 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js @@ -2,10 +2,9 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; const propTypes = { @@ -37,7 +36,7 @@ const propTypes = { isUsingKeyboardMovement: PropTypes.bool, ...withThemeStylesPropTypes, - ...withThemePropTypes, + ...withStyleUtilsPropTypes, }; class EmojiPickerMenuItem extends PureComponent { @@ -75,7 +74,7 @@ class EmojiPickerMenuItem extends PureComponent { onBlur={this.props.onBlur} ref={(ref) => (this.ref = ref)} style={({pressed}) => [ - StyleUtils.getButtonBackgroundColorStyle(this.props.theme, getButtonState(false, pressed)), + this.props.StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), this.props.isHighlighted && this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemKeyboardHighlighted : {}, this.props.isHighlighted && !this.props.isUsingKeyboardMovement ? this.props.themeStyles.emojiItemHighlighted : {}, this.props.themeStyles.emojiItem, @@ -102,8 +101,8 @@ EmojiPickerMenuItem.defaultProps = { // Significantly speeds up re-renders of the EmojiPickerMenu's FlatList // by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action. -export default withTheme( - withThemeStyles( +export default withThemeStyles( + withStyleUtils( React.memo( EmojiPickerMenuItem, (prevProps, nextProps) => diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 6917d3dec185..01f840677e5e 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -3,8 +3,7 @@ import {View} from 'react-native'; import type {SimpleEmoji} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import getStyledTextArray from '@libs/GetStyledTextArray'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; import Text from './Text'; @@ -43,8 +42,8 @@ type EmojiSuggestionsProps = { const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); /** * Render an emoji suggestion menu item component. */ @@ -63,7 +62,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr {styledTextArray.map(({text, isColored}) => ( {text} @@ -73,7 +72,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr ); }, - [styles, theme, prefix, preferredSkinToneIndex], + [prefix, styles.autoCompleteSuggestionContainer, styles.emojiSuggestionsEmoji, styles.emojiSuggestionsText, preferredSkinToneIndex, StyleUtils], ); return ( diff --git a/src/components/ExpensifyWordmark.tsx b/src/components/ExpensifyWordmark.tsx index 1402b48df0d9..49559d1cc6d5 100644 --- a/src/components/ExpensifyWordmark.tsx +++ b/src/components/ExpensifyWordmark.tsx @@ -5,8 +5,8 @@ import DevLogo from '@assets/images/expensify-logo--dev.svg'; import StagingLogo from '@assets/images/expensify-logo--staging.svg'; import ProductionLogo from '@assets/images/expensify-wordmark.svg'; import useEnvironment from '@hooks/useEnvironment'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -28,6 +28,7 @@ const logoComponents = { function ExpensifyWordmark({isSmallScreenWidth, style}: ExpensifyWordmarkProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {environment} = useEnvironment(); // PascalCase is required for React components, so capitalize the const here const LogoComponent = logoComponents[environment]; diff --git a/src/components/FlatList/index.android.js b/src/components/FlatList/index.android.js deleted file mode 100644 index f7c3da39ed84..000000000000 --- a/src/components/FlatList/index.android.js +++ /dev/null @@ -1,79 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, {forwardRef, useCallback, useContext} from 'react'; -import {FlatList} from 'react-native'; -import {ActionListContext} from '@pages/home/ReportScreenContext'; - -const propTypes = { - /** Same as for FlatList */ - onScroll: PropTypes.func, - - /** Same as for FlatList */ - onLayout: PropTypes.func, - - /** Same as for FlatList */ - // eslint-disable-next-line react/forbid-prop-types - maintainVisibleContentPosition: PropTypes.object, - - /** Passed via forwardRef so we can access the FlatList ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(FlatList)})]).isRequired, -}; - -const defaultProps = { - /** Same as for FlatList */ - onScroll: undefined, - - /** Same as for FlatList */ - onLayout: undefined, - - /** Same as for FlatList */ - maintainVisibleContentPosition: undefined, -}; - -// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android). -// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen. -function CustomFlatList(props) { - const {scrollPosition, setScrollPosition} = useContext(ActionListContext); - - const onScreenFocus = useCallback(() => { - if (!props.innerRef.current || !scrollPosition.offset) { - return; - } - if (props.innerRef.current && scrollPosition.offset) { - props.innerRef.current.scrollToOffset({offset: scrollPosition.offset, animated: false}); - } - }, [scrollPosition.offset, props.innerRef]); - - useFocusEffect( - useCallback(() => { - onScreenFocus(); - }, [onScreenFocus]), - ); - - return ( - props.onScroll(event)} - onMomentumScrollEnd={(event) => { - setScrollPosition({offset: event.nativeEvent.contentOffset.y}); - }} - ref={props.innerRef} - /> - ); -} - -CustomFlatList.propTypes = propTypes; -CustomFlatList.defaultProps = defaultProps; - -const CustomFlatListWithRef = forwardRef((props, ref) => ( - -)); - -CustomFlatListWithRef.displayName = 'CustomFlatListWithRef'; - -export default CustomFlatListWithRef; diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx new file mode 100644 index 000000000000..84345f6e0ed4 --- /dev/null +++ b/src/components/FlatList/index.android.tsx @@ -0,0 +1,43 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {ForwardedRef, forwardRef, useCallback, useContext} from 'react'; +import {FlatList, FlatListProps} from 'react-native'; +import {ActionListContext} from '@pages/home/ReportScreenContext'; + +// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android). +// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen. +function CustomFlatList(props: FlatListProps, ref: ForwardedRef) { + const {scrollPosition, setScrollPosition} = useContext(ActionListContext); + + const onScreenFocus = useCallback(() => { + if (typeof ref === 'function') { + return; + } + if (!ref?.current || !scrollPosition?.offset) { + return; + } + if (ref.current && scrollPosition.offset) { + ref.current.scrollToOffset({offset: scrollPosition.offset, animated: false}); + } + }, [scrollPosition?.offset, ref]); + + useFocusEffect( + useCallback(() => { + onScreenFocus(); + }, [onScreenFocus]), + ); + + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + onScroll={(event) => props.onScroll?.(event)} + onMomentumScrollEnd={(event) => { + setScrollPosition({offset: event.nativeEvent.contentOffset.y}); + }} + ref={ref} + /> + ); +} + +CustomFlatList.displayName = 'CustomFlatListWithRef'; +export default forwardRef(CustomFlatList); diff --git a/src/components/FlatList/index.js b/src/components/FlatList/index.ts similarity index 100% rename from src/components/FlatList/index.js rename to src/components/FlatList/index.ts diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index c49f69c336eb..791eb150f8c9 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {Animated, Easing, View} from 'react-native'; import compose from '@libs/compose'; -import * as StyleUtils from '@styles/StyleUtils'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import withStyleUtils, {withStyleUtilsPropTypes} from './withStyleUtils'; import withTheme, {withThemePropTypes} from './withTheme'; import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles'; @@ -28,8 +28,9 @@ const propTypes = { buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), ...withLocalizePropTypes, - ...withThemeStylesPropTypes, ...withThemePropTypes, + ...withThemeStylesPropTypes, + ...withStyleUtilsPropTypes, }; const defaultProps = { @@ -100,7 +101,7 @@ class FloatingActionButton extends PureComponent { this.props.onPress(e); }} onLongPress={() => {}} - style={[this.props.themeStyles.floatingActionButton, StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} + style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} > FloatingActionButtonWithLocalizeWithRef.displayName = 'FloatingActionButtonWithLocalizeWithRef'; -export default compose(withThemeStyles, withTheme)(FloatingActionButtonWithLocalizeWithRef); +export default compose(withThemeStyles, withTheme, withStyleUtils)(FloatingActionButtonWithLocalizeWithRef); diff --git a/src/components/FocusModeNotification.js b/src/components/FocusModeNotification.js index 37d8e4848b98..ea6db244091a 100644 --- a/src/components/FocusModeNotification.js +++ b/src/components/FocusModeNotification.js @@ -1,7 +1,7 @@ import React, {useEffect} from 'react'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import styles from '@styles/styles'; +import useThemeStyles from '@styles/useThemeStyles'; import * as Link from '@userActions/Link'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; @@ -12,6 +12,7 @@ import TextLinkWithRef from './TextLink'; function FocusModeNotification() { const {environmentURL} = useEnvironment(); const {translate} = useLocalize(); + const styles = useThemeStyles(); useEffect(() => { User.updateChatPriorityMode(CONST.PRIORITY_MODE.GSD, true); }, []); diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index af2511fc9f74..63953d8303db 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -112,7 +112,7 @@ function getInitialValueByType(valueType) { function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}) { const inputRefs = useRef({}); const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js deleted file mode 100644 index bec02c3d51f0..000000000000 --- a/src/components/FormHelpMessage.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import * as Localize from '@libs/Localize'; -import stylePropTypes from '@styles/stylePropTypes'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import Text from './Text'; - -const propTypes = { - /** Error or hint text. Ignored when children is not empty */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Children to render next to dot indicator */ - children: PropTypes.node, - - /** Indicates whether to show error or hint */ - isError: PropTypes.bool, - - /** Container style props */ - style: stylePropTypes, -}; - -const defaultProps = { - message: '', - children: null, - isError: true, - style: [], -}; - -function FormHelpMessage(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - if (_.isEmpty(props.message) && _.isEmpty(props.children)) { - return null; - } - - const translatedMessage = Localize.translateIfPhraseKey(props.message); - return ( - - {props.isError && ( - - )} - - - {props.children || {translatedMessage}} - - - ); -} - -FormHelpMessage.propTypes = propTypes; -FormHelpMessage.defaultProps = defaultProps; -FormHelpMessage.displayName = 'FormHelpMessage'; - -export default FormHelpMessage; diff --git a/src/components/FormHelpMessage.tsx b/src/components/FormHelpMessage.tsx new file mode 100644 index 000000000000..27a1f5827d75 --- /dev/null +++ b/src/components/FormHelpMessage.tsx @@ -0,0 +1,49 @@ +import isEmpty from 'lodash/isEmpty'; +import React from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import * as Localize from '@libs/Localize'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +type FormHelpMessageProps = { + /** Error or hint text. Ignored when children is not empty */ + message?: Localize.MaybePhraseKey; + + /** Children to render next to dot indicator */ + children?: React.ReactNode; + + /** Indicates whether to show error or hint */ + isError?: boolean; + + /** Container style props */ + style?: StyleProp; +}; + +function FormHelpMessage({message = '', children, isError = true, style}: FormHelpMessageProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + if (isEmpty(message) && isEmpty(children)) { + return null; + } + + const translatedMessage = Localize.translateIfPhraseKey(message); + + return ( + + {isError && ( + + )} + {children ?? {translatedMessage}} + + ); +} + +FormHelpMessage.displayName = 'FormHelpMessage'; + +export default FormHelpMessage; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js index 1b59219f38be..457a9dce66d9 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js @@ -1,7 +1,7 @@ import React from 'react'; import {Animated} from 'react-native'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; @@ -11,6 +11,7 @@ const propTypes = { function GrowlNotificationContainer(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets; return ( diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 8cddd3c017de..9a2d7c673c5b 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -60,8 +60,12 @@ function BaseHTMLEngineProvider(props) { }), 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}), 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}), + 'next-steps': defaultHTMLElementModels.span.extend({ + tagName: 'next-steps', + mixedUAStyles: {...styles.textLabelSupporting}, + }), }), - [styles.colorMuted, styles.formError, styles.mb0], + [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting], ); // We need to memoize this prop to make it referentially stable. diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 8f1406439be9..49642308a357 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -62,7 +62,7 @@ function AnchorRenderer(props) { key={props.key} displayName={displayName} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling - onPress={internalNewExpensifyPath || internalExpensifyPath ? Link.openLink : undefined} + onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} > diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js index 8cc062d754bc..9d101b8a5190 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js @@ -3,10 +3,11 @@ import {splitBoxModelStyle} from 'react-native-render-html'; import _ from 'underscore'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import InlineCodeBlock from '@components/InlineCodeBlock'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import htmlRendererPropTypes from './htmlRendererPropTypes'; function CodeRenderer(props) { + const StyleUtils = useStyleUtils(); // We split wrapper and inner styles // "boxModelStyle" corresponds to border, margin, padding and backgroundColor const {boxModelStyle, otherStyle: textStyle} = splitBoxModelStyle(props.style); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js index 65c287b8e86b..82769598d84a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.js @@ -2,18 +2,17 @@ import React from 'react'; import {TNodeChildrenRenderer} from 'react-native-render-html'; import _ from 'underscore'; import Text from '@components/Text'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import htmlRendererPropTypes from './htmlRendererPropTypes'; function MentionHereRenderer(props) { - const theme = useTheme(); + const StyleUtils = useStyleUtils(); return ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index 0027a557ab02..fbdacb6b47b0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -13,8 +13,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -28,8 +27,8 @@ const propTypes = { }; function MentionUserRenderer(props) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); @@ -77,7 +76,7 @@ function MentionUserRenderer(props) { }} > func, shouldNavigateToTopMostReport = false, }) { - const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isDownloadButtonActive, temporarilyDisableDownloadButton] = useThrottledButtonState(); const {translate} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); @@ -133,7 +132,7 @@ function HeaderWithBackButton({ > diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 49ff4a64446b..5e4c0192ca86 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; import {BankIcon, BankName, BankNameKey} from '@src/types/onyx/Bank'; type BankIconParams = { - themeStyles: ThemeStyles; + styles: ThemeStyles; bankName?: BankName; isCard?: boolean; }; @@ -115,7 +115,8 @@ function getBankNameKey(bankName: string): BankNameKey { /** * Returns Bank Icon Object that matches to existing bank icons or default icons */ -export default function getBankIcon({themeStyles, bankName, isCard = false}: BankIconParams): BankIcon { + +export default function getBankIcon({styles, bankName, isCard = false}: BankIconParams): BankIcon { const bankIcon: BankIcon = { icon: isCard ? GenericBankCard : GenericBank, }; @@ -130,11 +131,11 @@ export default function getBankIcon({themeStyles, bankName, isCard = false}: Ban // For default Credit Card icon the icon size should not be set. if (!isCard) { bankIcon.iconSize = variables.iconSizeExtraLarge; - bankIcon.iconStyles = [themeStyles.bankIconContainer]; + bankIcon.iconStyles = [styles.bankIconContainer]; } else { bankIcon.iconHeight = variables.bankCardHeight; bankIcon.iconWidth = variables.bankCardWidth; - bankIcon.iconStyles = [themeStyles.assignedCardsIconContainer]; + bankIcon.iconStyles = [styles.assignedCardsIconContainer]; } return bankIcon; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 3d4f0edb1656..b47daf0711b2 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -117,6 +117,7 @@ import Youtube from '@assets/images/social-youtube.svg'; import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; +import ThumbsUp from '@assets/images/thumbs-up.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -242,6 +243,7 @@ export { Shield, Sync, Task, + ThumbsUp, ThreeDots, Transfer, Trashcan, diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 25bd0a083be0..80abe1872c12 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,8 +1,8 @@ import React, {PureComponent} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -import withTheme, {ThemeProps} from '@components/withTheme'; -import withThemeStyles, {type ThemeStylesProps} from '@components/withThemeStyles'; -import * as StyleUtils from '@styles/StyleUtils'; +import withStyleUtils, {WithStyleUtilsProps} from '@components/withStyleUtils'; +import withTheme, {WithThemeProps} from '@components/withTheme'; +import withThemeStyles, {type WithThemeStylesProps} from '@components/withThemeStyles'; import variables from '@styles/variables'; import IconWrapperStyles from './IconWrapperStyles'; @@ -41,8 +41,9 @@ type IconProps = { /** Additional styles to add to the Icon */ additionalStyles?: StyleProp; -} & ThemeStylesProps & - ThemeProps; +} & WithThemeStylesProps & + WithThemeProps & + WithStyleUtilsProps; // We must use a class component to create an animatable component with the Animated API // eslint-disable-next-line react/prefer-stateless-function @@ -62,14 +63,14 @@ class Icon extends PureComponent { render() { const width = this.props.small ? variables.iconSizeSmall : this.props.width; const height = this.props.small ? variables.iconSizeSmall : this.props.height; - const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles]; + const iconStyles = [this.props.StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles]; const fill = this.props.fill ?? this.props.theme.icon; if (this.props.inline) { return ( { } } -export default withTheme(withThemeStyles(Icon)); +export default withTheme(withThemeStyles(withStyleUtils(Icon))); diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js index fd607a0e81a9..1fd81277b545 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.js @@ -5,7 +5,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -33,6 +33,7 @@ const defaultProps = { function ImageView({isAuthTokenRequired, url, fileName, onError}) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [isLoading, setIsLoading] = useState(true); const [containerHeight, setContainerHeight] = useState(0); const [containerWidth, setContainerWidth] = useState(0); @@ -257,7 +258,7 @@ function ImageView({isAuthTokenRequired, url, fileName, onError}) { = 1 ? styles.pRelative : styles.pAbsolute), ...styles.flex1, }} diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 3b2de574ba17..053f201ac109 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -24,9 +24,8 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import * as ReportUtils from '@libs/ReportUtils'; import * as ContextMenuActions from '@pages/home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as optionRowStyles from '@styles/optionRowStyles'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -66,6 +65,7 @@ const defaultProps = { function OptionRowLHN(props) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const popoverAnchor = useRef(null); const isFocusedRef = useRef(true); const {isSmallScreenWidth} = useWindowDimensions(); @@ -103,7 +103,7 @@ function OptionRowLHN(props) { props.style, ); const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles(styles)] : [styles.flex1]; + props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] @@ -158,6 +158,8 @@ function OptionRowLHN(props) { optionItem.type === CONST.REPORT.TYPE.CHAT && _.isEmpty(optionItem.chatType) && !optionItem.isThread && lodashGet(optionItem, 'displayNamesWithTooltips.length', 0) > 2; const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text; + const subscriptAvatarBorderColor = props.isFocused ? focusedBackgroundColor : theme.sidebar; + return ( {isPermissionDenied ? ( - {`${translate('location.permissionDenied')} ${translate('location.please')}`} + {`${translate('location.permissionDenied')} ${translate('location.please')}`} {` ${translate('location.allowPermission')} `} - {translate('location.tryAgain')} + {translate('location.tryAgain')} ) : ( {translate('location.notFound')} diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index c40962c8e631..91430602a115 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -6,7 +6,7 @@ import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import * as Browser from '@libs/Browser'; import * as ValidationUtils from '@libs/ValidationUtils'; -import * as StyleUtils from '@styles/StyleUtils'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -108,6 +108,7 @@ const getInputPlaceholderSlots = (length) => Array.from(Array(length).keys()); function MagicCodeInput(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const inputRefs = useRef(); const [input, setInput] = useState(TEXT_INPUT_EMPTY_STATE); const [focusedIndex, setFocusedIndex] = useState(0); @@ -407,7 +408,7 @@ function MagicCodeInput(props) { ( const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const [mapRef, setMapRef] = useState(null); const [currentPosition, setCurrentPosition] = useState(cachedUserLocation); diff --git a/src/components/MapView/index.js b/src/components/MapView/index.js deleted file mode 100644 index 551f57e34ed2..000000000000 --- a/src/components/MapView/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import MapView from './MapView'; - -export default MapView; diff --git a/src/components/MapView/index.tsx b/src/components/MapView/index.tsx new file mode 100644 index 000000000000..f273845fe4c0 --- /dev/null +++ b/src/components/MapView/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MapView from './MapView'; +import {ComponentProps} from './types'; + +function MapViewComponent(props: ComponentProps) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +export default MapViewComponent; diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index ae4b566f98ee..b42540bacb13 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,8 +1,8 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import getStyledTextArray from '@libs/GetStyledTextArray'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import {Icon} from '@src/types/onyx/OnyxCommon'; @@ -54,6 +54,7 @@ const keyExtractor = (item: Mention) => item.alternateText; function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainer = () => {}}: MentionSuggestionsProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); /** * Render a suggestion menu item component. */ @@ -83,7 +84,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe {text} @@ -99,7 +100,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe {text} @@ -109,7 +110,19 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe ); }, - [styles, theme, prefix], + [ + prefix, + styles.autoCompleteSuggestionContainer, + styles.ph2, + styles.mentionSuggestionsAvatarContainer, + styles.mentionSuggestionsText, + styles.flexShrink1, + styles.flex1, + styles.mentionSuggestionsDisplayName, + styles.mentionSuggestionsHandle, + theme.success, + StyleUtils, + ], ); return ( diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index a551d4a9205a..0e07fcd22b4c 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -7,8 +7,8 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -88,6 +88,7 @@ const defaultProps = { const MenuItem = React.forwardRef((props, ref) => { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const style = StyleUtils.combineStyles(props.style, styles.popoverMenuItem); const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = React.useState(''); @@ -181,7 +182,7 @@ const MenuItem = React.forwardRef((props, ref) => { style={({pressed}) => [ style, !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(theme, getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), (isHovered || pressed) && props.hoverAndPressStyle, ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, @@ -227,7 +228,6 @@ const MenuItem = React.forwardRef((props, ref) => { fill={ props.iconFill || StyleUtils.getIconFillColor( - theme, getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true, ) @@ -262,11 +262,7 @@ const MenuItem = React.forwardRef((props, ref) => { height={props.iconHeight} fill={ props.secondaryIconFill || - StyleUtils.getIconFillColor( - theme, - getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), - true, - ) + StyleUtils.getIconFillColor(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true) } /> @@ -382,7 +378,7 @@ const MenuItem = React.forwardRef((props, ref) => { )} diff --git a/src/components/MessagesRow.js b/src/components/MessagesRow.tsx similarity index 60% rename from src/components/MessagesRow.js rename to src/components/MessagesRow.tsx index e4d6240ba0fd..02b78942dfcf 100644 --- a/src/components/MessagesRow.js +++ b/src/components/MessagesRow.tsx @@ -1,55 +1,45 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; +import * as Localize from '@libs/Localize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import DotIndicatorMessage from './DotIndicatorMessage'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip'; -const propTypes = { +type MessagesRowProps = { /** The messages to display */ - messages: PropTypes.objectOf( - PropTypes.oneOfType([PropTypes.oneOfType([PropTypes.string, PropTypes.object]), PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - ), + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ - type: PropTypes.oneOf(['error', 'success']).isRequired, + type: 'error' | 'success'; /** A function to run when the X button next to the message is clicked */ - onClose: PropTypes.func, + onClose?: () => void; /** Additional style object for the container */ - containerStyles: stylePropTypes, + containerStyles?: StyleProp; /** Whether we can dismiss the messages */ - canDismiss: PropTypes.bool, + canDismiss?: boolean; }; -const defaultProps = { - messages: {}, - onClose: () => {}, - containerStyles: [], - canDismiss: true, -}; - -function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { +function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, canDismiss = true}: MessagesRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - if (_.isEmpty(messages)) { + + if (isEmptyObject(messages)) { return null; } return ( - + @@ -69,8 +59,6 @@ function MessagesRow({messages, type, onClose, containerStyles, canDismiss}) { ); } -MessagesRow.propTypes = propTypes; -MessagesRow.defaultProps = defaultProps; MessagesRow.displayName = 'MessagesRow'; export default MessagesRow; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index f5c498b24893..1ea284b55280 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -7,9 +7,8 @@ import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import useNativeDriver from '@libs/useNativeDriver'; -import getModalStyles from '@styles/getModalStyles'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; @@ -44,6 +43,7 @@ function BaseModal( ) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); const safeAreaInsets = useSafeAreaInsets(); @@ -133,9 +133,7 @@ function BaseModal( hideBackdrop, } = useMemo( () => - getModalStyles( - theme, - styles, + StyleUtils.getModalStyles( type, { windowWidth, @@ -146,7 +144,7 @@ function BaseModal( innerContainerStyle, outerStyle, ), - [innerContainerStyle, isSmallScreenWidth, outerStyle, popoverAnchorPosition, theme, type, windowHeight, windowWidth, styles], + [StyleUtils, type, windowWidth, windowHeight, isSmallScreenWidth, popoverAnchorPosition, innerContainerStyle, outerStyle], ); const { diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index bc683b6f6311..bfc899f9c278 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,14 +1,15 @@ import React, {useState} from 'react'; import withWindowDimensions from '@components/withWindowDimensions'; import StatusBar from '@libs/StatusBar'; -import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; +import useStyleUtils from '@styles/useStyleUtils'; import CONST from '@src/CONST'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = () => {}, children, ...rest}: BaseModalProps) { const theme = useTheme(); + const StyleUtils = useStyleUtils(); const [previousStatusBarColor, setPreviousStatusBarColor] = useState(); const setStatusBarColor = (color = theme.appBG) => { @@ -32,7 +33,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( if (statusBarColor) { setPreviousStatusBarColor(statusBarColor); // If it is a full screen modal then match it with appBG, otherwise we use the backdrop color - setStatusBarColor(isFullScreenModal ? theme.appBG : StyleUtils.getThemeBackgroundColor(theme, statusBarColor)); + setStatusBarColor(isFullScreenModal ? theme.appBG : StyleUtils.getThemeBackgroundColor(statusBarColor)); } onModalShow?.(); diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 880e46b2592a..9a1f59d64efa 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -75,19 +75,24 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; + const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; - const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); + const isPayer = isGroupPolicy + ? // In a group policy, the admin approver can pay the report directly by skipping the approval step + isPolicyAdmin && (isApproved || isManager) + : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = useMemo( + const shouldShowPayButton = useMemo( () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { - if (policyType !== CONST.POLICY.TYPE.CORPORATE) { + if (!isGroupPolicy) { return false; } return isManager && !isDraft && !isApproved && !isSettled; - }, [policyType, isManager, isDraft, isApproved, isSettled]); + }, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]); + const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextSteps = isFromPaidPolicy && nextStep && !_.isEmpty(nextStep.message); @@ -120,22 +125,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} + shouldHidePaymentOptions={!shouldShowPayButton} + shouldShowApproveButton={shouldShowApproveButton} style={[styles.pv2]} formattedAmount={formattedAmount} /> )} - {shouldShowApproveButton && !isSmallScreenWidth && ( - -