diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 351a11506d68..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(themeColors.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/.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 a8671f140a00..afa05a280390 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 1001040702 - versionName "1.4.7-2" + versionCode 1001041106 + versionName "1.4.11-6" } 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/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 16c8f88927b1..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(themeColors.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/_data/_routes.yml b/docs/_data/_routes.yml index efdcf41b63d5..e320b690c226 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -112,21 +112,11 @@ platforms: title: Expensify Card icon: /assets/images/hand-card.svg description: Explore how the Expensify Card combines convenience and security to enhance everyday business transactions. Discover how to apply for, oversee, and maximize your card perks here. - - - href: expensify-partner-program - title: Expensify Partner Program - icon: /assets/images/handshake.svg - description: Discover how to get the most out of Expensify as an ExpensifyApproved! accountant partner. Learn how to set up your clients, receive CPE credits, and take advantage of your partner discount. - href: get-paid-back title: Get Paid Back icon: /assets/images/money-into-wallet.svg description: Whether you submit an expense report or an invoice, find out here how to ensure a smooth and timely payback process every time. - - - href: send-payments - title: Send Payments - icon: /assets/images/money-wings.svg - description: Uncover step-by-step guidance on sending direct reimbursements to employees, paying an invoice to a vendor, and utilizing third-party payment options. - href: workspace-and-domain-settings title: Workspace & Domain Settings 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/Notification-Troubbleshooting.md b/docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Notification-Troubbleshooting.md rename to docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md 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/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md similarity index 98% rename from docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md rename to docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md index f2ff837d7638..fa5879d85ea8 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-Company-Cards.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Company-Card-Settings.md @@ -1,6 +1,6 @@ --- title: Company-Card-Settings.md -description: Company card settings +description: Once you connect your cards, customize the configuration using company card settings. --- # Overview Once you’ve imported your company cards via commercial card feed, direct bank feed, or CSV import, the next step is to configure the cards’ settings. diff --git a/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md new file mode 100644 index 000000000000..750a1fc10e77 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-partner-program/Partner-Billing-Guide.md @@ -0,0 +1,87 @@ +--- +title: Partner Billing Guide +description: Understand how Expensify bills partners and their clients +--- + +# Overview + +The ExpensifyApproved! Partner Program offers exclusive billing rates and features tailored for accountants to ensure seamless client management. If you are an accountant or consultant who recommends spend management solutions to your clients, becoming an ExpensifyApproved! Accountant may be a great certification for you. This guide will walk you through the unique billing perks available to ExpensifyApproved! partners, emphasizing the importance of understanding and managing client billing effectively. To learn what perks partners receive, check out the ExpensifyApproved! program details here. + +# Get exclusive partner pricing + +All ExpensifyApproved! Partners are automatically eligible for a special rate of $9/seat monthly, without an annual commitment when they adopt the Expensify Card. This provides flexibility as active users can vary. Here are the specifics on pricing for our Approved! Partners’ clients: +- **Bundled pricing:** US Clients using the Expensify Card get up to a 50% discount, bringing their monthly bill to $9/seat. Reach out to your Partner Manager to discuss exclusive international pricing discount. +- **Unbundled pricing (or pay-per-use pricing):** Clients not using the Expensify Card are billed at $18/seat monthly. +- **No annual commitment:** Partners pay only for what they use, with no annual subscriptions required. +- **Expensify Card revenue share:** All partners receive a 0.5% revenue share on Expensify Card transactions made by clients. This revenue share can be passed back to the client for an additional discount to offset their Expensify bill. + +# Understanding the billing process + +Expensify bills the owner of the expense workspace for the activity on that workspace. If accountants retain ownership of client workspaces, they receive the bill and can then re-bill their clients based on individual agreements. + +Each month, Expensify will send a consolidated bill detailing: +- **Pay-per-use seats:** This is the number of active clients and their users for the month. +- **Expensify Card discount**: This amount reflects how much spend is put on your card, which then determines the discount for that month. +- **Total monthly price:** This amount is the overall price of Expensify when using the Expensify Card discount to offset the cost of the pay-per-use seats. +- **Workspace list:** This is an overview of all client workspaces with their respective active seats. + +## Consolidated Domain Billing + +If your firm wishes to consolidate all Expensify billing to a single account, the Consolidated Domain Billing feature is your go-to tool. It centralizes payment for all group workspaces owned by any domain member of your firm. + +### Activating Consolidated Domain Billing: + 1. Claim and verify your firm’s domain. + 2. Navigate to **Settings > Domains > [Domain Name] > Domain Admins** and set a **"Primary Domain Admin"** by using the drop down toggle to select an email address. + 3. Enable **Consolidated Domain Billing** in the same section. + +The Consolidated Domain Billing tool ensures that billing takes place under a single Expensify account associated with your firm. This eliminates the need to add multiple payment cards across various accounts to cover payments for multiple clients. + +## Maintaining a Console of all clients: + +If your firm wants to have a console view of all client workspaces and domains, you will want to create a single, centralized login to manage all client workspaces and domains, such as accounting@myfirm.com. + + 1. Create a dedicated email address that will act as the universal workspace owner, for example, accounting@myfirm.com. + 2. Register this email with Expensify or your chosen platform and ensure it is verified and secured. + 3. Within each client workspace settings, add your centralized email (e.g. accounting@myfirm.com) as a workspace admin. + 4. Do the same with each client domain. + +## Applying Client IDs to a bill + +Using client IDs for Optimized Billing in Expensify: A unique identifier feature for ExpensifyApproved! accountants. Streamline client Workspace recognition and make your billing process more efficient. + +# How to assign a client ID to a workspace + 1. Log in to your account: Ensure you’re using an Approved! accountant account. + 2. Navigate to the desired Workspace: Go to **Settings > Workspaces > [Workspace Name] > Overview**. + 3. Input the identifier: Here, you can input an alphanumeric unique identifier for each client workspace. +**Note:** If a client has multiple workspaces, ensure each workspace has a consistent client ID. + +# How to access and download billing receipts +- Accessing Billing: **Settings: Go to Settings > Your Account > Payments > Billing History.** +- Download the Receipt: Click on **"Download Receipt CSV".** + +# Deep Dive +- Using client IDs for all Workspaces: It's beneficial to use client IDs for all Workspaces to ensure each one is easily recognizable. +- Benefits of itemized billing receipts: Employing client IDs offers itemized billing by client, with each itemization detailing unique active users. + +# FAQ + +**Do I automatically get the special billing rate as an ExpensifyApproved! Partner?** +- Yes, when you join the ExpensifyApproved! program, you will automatically get the special billing rate. To join the ExpensifyApproved! Program, you need to enroll in ExpensifyApproved! University. + +**How can I check my billing details?** +- To check your billing details, be on the lookout for your detailed billing statement sent out at the end of each month. + +**Can I pass the bill to my clients?** +- Yes, you can pass the bill on to your clients. If you retain ownership of client workspaces in Expensify, you can re-bill your clients. If you’d like the client to own the billing of the Expensify, they can take over billing. + +**What if I don't want to use the Expensify Card?** +- If you prefer not to use the Expensify Card, your clients will be billed at $18/seat monthly. + +**Why use client IDs?** +- Client IDs provide a streamlined method to identify and manage workspaces, especially beneficial when a client has multiple workspaces. + +**Do I need a client ID for each Workspace?** +- Yes, if you want to ensure seamless identification and billing processes. + +**Where can I see the Billing Receipts?** +- All billing owners receive an emailed PDF of their monthly billing receipt, but a CSV version can also be downloaded from the platform. diff --git a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md index 2db69d0a8791..8243833dcc23 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager.md @@ -1,5 +1,5 @@ --- -title: Your Expensify Partner Manager +title: Expensify Partner Support description: Understanding support for our partners --- @@ -10,6 +10,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist ## 1. ExpensifyApproved! University **Purpose:** Equip your team with a comprehensive understanding of Expensify. + **Benefits:** - Foundation-level knowledge about the platform. - 3 CPE credits upon successful completion (US-only). @@ -17,16 +18,39 @@ Our well-rounded support methodology is designed to provide comprehensive assist - Visit university.Expensify.com to access our comprehensive training program. ## 2. Partner Manager -**Role:** A designated liaison for your firm. +**Role:** +A Partner Manager is a dedicated point of contact for your firm Partner Managers support our accounting partners by providing recommendations for client’s accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency. + + **Key Responsibilities:** - Handle any escalations promptly. - Organize firm-wide training sessions. - Assist with strategic planning and the introduction of new features. - Once you've completed the ExpensifyApproved! University, log in to your Expensify account. Click on the "Support" option to connect with your dedicated Partner Manager. +**How do I know if I have a Partner Manager?** + +For your firm to be assigned a Partner Manager, you must complete the ExpensifyApproved! University training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives all the benefits, including access to the Partner Manager. So everyone at your firm must complete the training to receive the maximum benefit. + +You can check to see if you’ve completed the course and enrolled in the ExpensifyApproved! Accountants program simply by logging into your Expensify account. In the bottom left-hand corner of the website, you will see the ExpensifyApproved! logo. + +**How do I contact my Partner Manager?** +1. Signing in to new.expensify.com and searching for your Partner Manager +2. Replying to or clicking the chat link on any email you get from your Partner Manager + +**How do I know if my Partner Manager is online?** + +You will be able to see if they are online via their status in new.expensify.com, which will either say “online” or have their working hours. + +**Can I get on a call with my Partner Manager?** + +Of course! You can ask your Partner Manager to schedule a call whenever you think one might be helpful. Partner Managers can discuss client onboarding strategies, firm-wide training, and client setups. + +We recommend continuing to work with Concierge for general support questions, as this team is always online and available to help immediately. ## 3. Client Setup Specialist **Purpose:** Ensure smooth onboarding for every client you refer. + **Duties:** - Comprehensive assistance with setting up Expensify. - Help with configuring accounting integrations. @@ -35,6 +59,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist ## 4. Client Account Manager **Role:** Dedicated support for ongoing client needs. + **Responsibilities:** - Address day-to-day product inquiries. - Assist clients in navigating and optimizing their use of Expensify. @@ -42,6 +67,7 @@ Our well-rounded support methodology is designed to provide comprehensive assist ## 5. Concierge chat support **Availability:** Real-time support for any urgent inquiries. + **Features:** - Immediate assistance with an average response time of under two minutes. - Available to both accountants and clients for all product-related questions. diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md similarity index 89% rename from docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md rename to docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md index 214188e35137..1b537839af77 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Creating-Per-Diem-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/Per-Diem-Expenses.md @@ -1,17 +1,17 @@ --- -title: Creating Per Diem Expenses +title: Per-Diem-Expenses description: How to create Per Diem expenses on mobile and web. --- # Overview What are Per Diems? Per diems, short for "per diem allowance" or "daily allowance," are fixed daily payments provided by your employer to cover expenses incurred during business or work-related travel. These allowances simplify expense tracking and reimbursement for meals, lodging, and incidental expenses during a trip. Per Diems can be masterfully tracked in Expensify! -## How To create per diem expenses +## How to create per diem expenses To add per diem expenses, you need three pieces of information: 1. Where did you go? - Specify your travel destination. 2. How long were you away? - Define the period you're claiming for. -3. Which rate did you use? - Select the appropriate per diem rate. +3. Which rate did you use? - Select the appropriate per diem rate (this is set by your employer). ### Step 1: On either the web or mobile app, click New Expense and choose Per Diem @@ -31,15 +31,15 @@ Finally, submit your Per Diem expense for approval, and you'll be on your way to # FAQ -## Can I Edit My Per Diems? +## Can I edit my per diem expenses? Per Diems cannot be amended. To make changes, delete the expense and recreate it as needed. -## What If My Admin Requires Daily Per Diems? +## What if my admin requires daily per diems? No problem! Create a separate Per Diem expense for each day of your trip. -## I Have Questions About the Amount I'm Due +## I have questions about the amount I'm due Reach out to your internal Admin team, as they've configured the rates in your policy to meet specific requirements. -## Can I Add Start and End Times to a Per Diem? +## Can I add start and end times to per diems? Unfortunately, you cannot add start and end times to Per Diems in Expensify. By following these steps, you can efficiently create and manage your Per Diem expenses in Expensify, making the process of tracking and getting reimbursed hassle-free. diff --git a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md b/docs/articles/expensify-classic/get-paid-back/Per-Diem.md deleted file mode 100644 index 780e5969c441..000000000000 --- a/docs/articles/expensify-classic/get-paid-back/Per-Diem.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Per Diem -description: Per Diem ---- -## 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/getting-started/support/Expensify-Support.md b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md new file mode 100644 index 000000000000..f4a6acdd8571 --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/support/Expensify-Support.md @@ -0,0 +1,117 @@ +--- +title: Your Expensify Account Manager +description: Everything you need to know about Having an Expensify account manager +redirect_from: articles/other/Your-Expensify-Account-Manager/ +--- + + + +# Introduction to Expensify Support + +Expensify offers comprehensive support for customers of all sizes. From small business owners to multinational corporations, Expensify's support teams are available to assist! + +Expensify has four different support teams. Let's go over how each of them operates below. + +**Concierge Support:** +- Concierge is available 24/7 and is the first line of support +- Users can access Concierge through the Expensify app or via email at concierge@expensify.com +- Provides quick responses, usually within minutes +- Don't be your team's tech support - refer your users to Concierge for all their Expensify product questions + +**Setup Specialists:** +- Dedicated onboarding experts to assist users in getting started with Expensify +- Assist in connecting cards and bank accounts, configuring integrations, and more + +**Account Managers:** +- A dedicated point of contact for companies with more than ten users +- Serve as product specialists +- Available to answer questions and promote best practices +- Provide ongoing support and guidance + +**ExpensifyApproved! Accounting Partner Managers:** +- Offer an additional support layer for accounting partners +- Partner Managers are dedicated Expensify team members +- Provide firmwide training, assist with strategic goal planning, and collaborative marketing + +This comprehensive support structure ensures that Expensify users receive the assistance they need, whether new to the platform or looking to optimize their experience. + +# How to get support in Expensify + +## Concierge + +Reach out to Concierge when you have general questions about Expensify. Concierge can also be used to manage expenses, submit reports, and automate approvals. You can contact Concierge while logged into your Expensify account by clicking **Support** on the left-hand menu and then selecting **Concierge**. + +Try questions such as: +- What is SmartScan? +- How do I add a mileage expense from the mobile app? +- Where can I add a Secondary Login? + +## Setup Specialist + +Expensify provides onboarding guidance for anyone through a Setup Specialist! You can contact your Setup Specialist while logged into your Expensify account by clicking **Support** on the left-hand menu and then selecting **Account Setup Specialist**. + +### What to expect from your Setup Specialist: + +- Personalized Support: Your Setup Specialist is here to assist you with tailored support. They can answer your questions and arrange screen share calls to help you set up your Expensify account or familiarize you with the product. + +- Product Demos: Are you curious about how the Expensify Card works or how it integrates with your accounting software? Just let your Setup Specialist know, and they'll guide you and your team through any feature you'd like to explore. + +- Setup Assistant: You don't have to do it alone when setting up your Expensify account. When you're ready to begin, chat with your Setup Specialist in the #admins room. They'll ensure everything is configured correctly right from the start. + + +## Account Manager + +If you have more than ten active users, or you're an ExpensifyApproved! Accountant's client, you're eligible for a designated Account Manager. + +Once you've completed onboarding to Expensify with your Setup Specialist, your Account Manager will take over as the main point of contact. Your account manager is there to support you and other Workspace admins in reviewing your account, advising on best practices, and making changes to your Workspace on your behalf whenever you need a hand. + +They will actively monitor open technical issues and be proactive with recommendations to increase efficiency and minimize time spent on expense management. + +### How to contact your Account Manager + +If you are a Workspace admin or domain admin, you will hear from your Account Manager as soon as one is assigned to your company. + +Unlike Concierge, an Account Manager's support will not be real-time, 24 hours a day. Your Account Manager will be as responsive as possible when online, but anything sent when they're offline will not be responded to until they're working again. + +You can contact your Account Manager while logged into your Expensify account by clicking **Support** on the left-hand menu and then selecting **Account Manager**. + +## Partner Manager + +A Partner Manager is a dedicated point of contact to support our ExpensifyApproved! Accountants with questions about their Expensify account. Partner Managers support our accounting partners by providing recommendations for client's accounts, assisting with firm-wide training, and ensuring partners receive the full benefits of our partnership program. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency and minimize time spent on expense management. + +For your firm to be assigned a Partner Manager, you must complete the [ExpensifyApproved! University](https://use.expensify.com/accountants-program) training course. Every external accountant or bookkeeper who completes the training is automatically enrolled in our program and receives benefits, including access to a Partner Manager. Everyone at your firm must complete the training to receive the maximum benefit. + +### How to contact your Partner Manager + +Your Partner Manager should reach out to you once you've completed ExpensifyApproved! University. Otherwise, you can contact your Partner Manager anytime by clicking **Support** on the left-hand menu and then selecting **Partner Manager**. + +## Tips on Contacting Expensify Support + +- **Use the Right Email**: Contact us from the email address linked to your Expensify account. This helps us locate your information accurately. +- **Be Clear and Specific**: When asking questions or reporting issues, provide specific examples like affected users' email addresses or report IDs. This makes it easier for us to assist you effectively. +- **Practice Kindness**: Remember that we're here to help. Please be polite, considerate, and patient as we work together to resolve any concerns you have. + +# FAQ +## Who gets an Account Manager? +Members who have 10 or more active users, or clients of ExpensifyApproved! Accounts are automatically assigned a dedicated Account Manager. + +To be assigned an Account Manager immediately, log into your Expensify account and go to **Settings > Workspaces > Group**, then click **Subscription** and increase your subscription size to 10 or more. + +## What if I'm unable to reach my Account Manager? +If you're unable to reach your account manager, then you can always reach out to Concierge for assistance. Your account manager will always reply once they're online again. + +## Can I schedule a call with my Account Manager? +Of course! You can ask your Account Manager for a call whenever you think one might be helpful. + +**What makes a successful call with an Account Manager**: +- Scheduling the call using their calendar link (this should be shared with you once you're assigned an Account Manager) +- Listing out exactly what you'd like to go over with the Account Manager, and then sharing that list with them before the call +- Having anyone else in your organization who regularly manages your Expensify setup join the call + +## When should I chat with Concierge instead of my Account Manager? +If you have a general question about the status of a report or updating an account setting, Concierge is more than capable of getting you the answers you need. + +We recommend working with Concierge on general support questions, as this team is always online and available to help immediately. + +## Who gets assigned a Setup Specialist? +This feature is specifically for new members! Whenever you start a free trial, a product Setup Specialist will be assigned to guide you through configuring your Expensify account. diff --git a/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md b/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md deleted file mode 100644 index a6fa0220c0dc..000000000000 --- a/docs/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Your Expensify Account Manager -description: Everything you need to know about Having an Expensify account manager -redirect_from: articles/other/Your-Expensify-Account-Manager/ ---- - - - -# What is an account manager? -An account manager is a dedicated point of contact to support policy admins with questions about their Expensify account. They are available to help you and other policy admins review your account, advise on best practices, and make changes to your policy on your behalf whenever you need a hand. They will actively monitor open technical issues and be proactive with recommendations to increase efficiency and minimize time spent on expense management. - -Unlike Concierge, an account manager’s support will not be real-time, 24 hours a day. A benefit of Concierge is that you get real-time support every day. Your account manager will be super responsive when online, but anything sent when they’re offline will not be responded to until they’re online again. - -For real-time responses and simple troubleshooting issues, you can always message our general support by writing to Concierge via the in-product chat or by emailing concierge@expensify.com. - -# How do I know if I have an account manager? -If you are a policy admin or domain admin, you will also hear from your account manager as soon as one gets assigned to your company. If you'd like a reminder who your account manager is, just click the Support link on the left side of Expensify - you'll see your account manager's name and photo, with an option to contact them for help. - -## How do I contact my account manager? -We make it easy to contact your account manager: - -1. Log in to your Expensify account, click "Support" along the left side of the page, and click the “Account Manager” option -2. Reply to (or click the chat link on) any email you get from your account manager -3. Sign in to new.expensify.com and go to the #admins room for any of your policies. Your account manager is in your #admin rooms ready to help you, so you can ask for help here and your account manager will respond in the chat. - -# FAQs -## Who gets an account manager? -Every customer with 10 or more paid subscribers is automatically assigned a dedicated account manager. If you have fewer than 10 active users each month, you can still get an account manager by increasing your subscription to 10 or more users, To get assigned an account manager immediately, log into your Expensify account and go to Settings > Policies > Group, then click Subscription and increase your subscription size to 10 or more. - -## How do I know if my account manager is online? -You will be able to see if they are online via their status, which will either say something like “online” or have their working hours. - -## What if I’m unable to reach my account manager? -If for some reason, you’re unable to reach your account manager, perhaps because they’re offline, then you can always reach out to Concierge for assistance. Your account manager will always get back to you when they’re online again. - -## Can I get on a call with my account manager? -Of course! You can ask your account manager to schedule a call whenever you think one might be helpful. We usually find that the most effective calls are those that deal with general setup questions. For technical troubleshooting, we typically recommend chat as that allows your account manager time to look into the issue, test things on their end, and, if needed, consult the wider Expensify technical team. It also allows you to easily refer back to instructions and links. \ No newline at end of file diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md b/docs/articles/expensify-classic/integrations/travel-integrations/Navan.md similarity index 100% rename from docs/articles/expensify-classic/integrations/travel-integrations/Trip-Actions.md rename to docs/articles/expensify-classic/integrations/travel-integrations/Navan.md diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md index 3ee1c8656b4b..16da0c0caa5b 100644 --- a/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md +++ b/docs/articles/expensify-classic/integrations/travel-integrations/Uber.md @@ -1,5 +1,24 @@ --- -title: Coming Soon -description: Coming Soon +title: Uber integration +description: Connecting your Uber account to Expensify --- -## Resource Coming Soon! +## Overview + +Link Expensify directly to your Uber account so your Uber for Business receipts populate automatically in Expensify. + +# How to connect Uber to Expensify + +You can do this right in the Uber app: + +1. Head to Account > Business hub > Get started +2. Tap Create an individual account > Get started +3. Enter your business email and tap Next +4. Select the payment card you'd like to use for your business profile +5. Choose how frequently you’d like to receive travel summaries +6. Select Expensify as your expense provider +Expensify and Uber are now connected! + +Now, every time you use Uber for Business – be it for rides or meals – the receipt will be imported and scanned into Expensify automatically. + +![Uber integration set up steps: Connecting your account](https://help.expensify.com/assets/images/Uber1.png){:width="100%"} +![Uber integration set up steps: Selecting Expensify](https://help.expensify.com/assets/images/Uber2.png){:width="100%"} diff --git a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md index e157ede1969d..3b79072aa393 100644 --- a/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md +++ b/docs/articles/new-expensify/billing-and-plan-types/The-Free-Plan.md @@ -30,16 +30,38 @@ Once you’ve completed your company setup, you should have completed the follow - Invited members to the workspace - Assigned Expensify Cards -# Inviting Members to the Free Plan: +# Inviting Members to the Free Plan - Navigate to the Settings Menu and click **_Members_** to invite your team. You can invite employees one at a time, or you can invite multiple users by listing out their email addresses separated by a comma - To use the Expensify Card, you must invite them to your workspace via your company email address (i.e., admin@companyemail.com and NOT admin@gmail.com). # Managing the Free Plan -To access your workspace settings, click your profile icon and then on your workspace name. +## Workspace Settings -This settings menu allows you to manage your workspace members, issue additional Expensify Cards, and utilize this plan’s various bill pay and payment options. +To access your workspace settings, click your profile icon and then on your workspace name. This settings menu allows you to manage your workspace members, issue additional Expensify Cards, and utilize this plan’s various bill pay and payment options. + +## Paying an Expense Report +- Once a user creates an expense it will automatically be shared with you in a Processing report. +- Pay expenses directly through Expensify by choosing ‘Reimburse > via Direct Deposit (ACH)` in a report on www.expensify.com or by choosing ‘Pay with Expensify’ in a payment request on new.expensify.com. +- Notify your user that you’ll pay them manually outside of Expensify by choosing ‘Reimburse > I’ll do it manually’ in a report on www.expensify.com or choosing ‘Pay Elsewhere’ in a payment request on new.expensify.com. +- Reports with only non-reimbursable expenses on them have the option to ‘Mark as Closed’ in the report on www.expensify.com or ‘Mark as Done’ in the payment request on new.expensify.com. + +## Changing Submitted Expenses + +Request an edit an expense or remove an expense before you pay, you can let your user know by making a comment in the Report History section of their Processing report or chatting with them on new.expensify.com. + +# Managing Expenses + +## Creating an Expense +- You can create an expense either by swiping the Expensify card or just smartscan a receipt! +- Once you create an expense it will be automatically added to a report and shared with your admin. +- You can edit or delete any expense that hasn’t been paid or closed by your admin. + +## Getting paid for Expenses +- Automatic submission is already set up, so your admin can pay you back immediately once you create an expense. +- Your admin will get a notification when you send them a new expense, but you can also remind them to pay you by making a comment in the Report History section of your Processing report or chatting with them on new.expensify.com. # FAQs + ## Do I need a business bank account to use the Free Plan? You will need a US business checking account if you want to enable the Expensify Card and set up direct ACH reimbursement. @@ -50,9 +72,27 @@ If you're not in the US, you can still use the Free Plan, but the Expensify Card The Expensify Workplace only allows for one admin (the workspace creator). -## Scheduled Submit is set to weekly on the Free Plan. Can I change this? +## I am on a paid plan, can I switch to a Free Plan? + +You can set up a Free Plan, but you must honor any active subscription you have also. If you're on a paid plan, it is likely you want more functionality than what the Free Plan offers such as a direct connection with an accounting integration and approval workflows. + +## Can I get cashback on Expensify Card purchases if I have a free plan? + +You can get 1% credited back to your settlement account once you spend over $25,000 per month across all cards and 2% when you spend over $250,000! -No, expense reports on the Free Plan submit weekly, and there is no way to customize approval settings. +## Can I switch the workspace currency on the Free Plan? It looks set to USD + +Yes, you can change the currency of the Free Plan by going to Avatar > [Workspace Name] > General settings > Default currency. + +We do require a US business bank account for reimbursements and Expensify Card settlements though, so if you have a business bank account linked to your account, then the currency of the Free Plan will be USD. + +## Is a free plan available to people outside the USA? + +Yes! You can use the free plan anywhere in the world to track expenses, send invoices etc. Just remember, anything that requires a verified business bank account (such as ACH reimbursement and the Expensify Card) is only available to those with a US checking account! + +## Can I integrate the free plan with my accounting package? + +No. In order to use our accounting sync, you will need to update to a paid plan ## With the Free Plan, can I add my own categories? @@ -60,4 +100,48 @@ Categories are standardized on the Free Plan and can’t be edited. Custom categ ## With the Free Plan, can I export reports using a custom format? -The Free Plan offers standard report export formats. You'll need to upgrade to a paid plan to create a custom export format. \ No newline at end of file +The Free Plan offers standard report export formats. You'll need to upgrade to a paid plan to create a custom export format. + +## Can I change the mileage rate or unit used for distance tracking? + +Yes. The workspace admin can access the rate and unit settings by going to Avatar > [Workspace name] > Reimburse expenses > Track distance. + +## Can I add non-Expensify cardholders to my Workspace? + +Yes. You can invite any user to your Workspace. Just click invite and enter their email and they will be invited to download the app and join! + +## Expenses are automatically shared with the admin on a Processing report on the Free Plan, can I change this? + +No. Expense reports submitted on the Free Plan are set to submit automatically and do not allow for approvals. Different Scheduled Submit settings are available on our paid plans. + +## How do I give a user I invited to my Workspace an Expensify Card? + +You can give a user an Expensify Card by going to the Company Cards Page on expensify.com and setting a card limit >$0. Please note that in order to give a user a card, they must be using a private company email address (i.e. name@companyname.com NOT name@gmail.com) + +## Can I use a different bank account to the one I have added for some of the features in the workspace? + +No. The bank account you have added will be used to issue corporate cards, reimburse expenses, collect invoices, and pay bills! + +## Can I upgrade my Free Workspace to a Paid Workspace or do I need to create a new one? + +There is no way to upgrade a workspace, so you would need to create a new Workspace. Paid workspaces have more functionalities than what the Free Plan offers, such as a direct connection with an accounting integration and approval workflows. + +## Can I create more than one Processing report at a time? + +No. To keep things simple, we only allow one Processing report per user at a time. If you need to have more than one report at a time, our paid plans support unlimited reports. + +## A user has added an expense I need to change before I pay it, how do I let them know? + +Users can edit and delete expenses on Processing reports. If you need something changed, let them know by commenting in the Report History section of the report on expensify.com or by chatting with them in new.expensify.com. + +## Can I ‘Reopen’ a report once it’s Reimbursed or Closed? + +No. Once an admin reimburses or closes a report, it cannot be ‘reopened’, but you can always comment on a report to add context. + +## I accidentally reimbursed a report too soon. Can I cancel the reimbursement? + +Depending on how quickly you report it to us, we may be able to help cancel a reimbursement. Chat Concierge to see if we can help cancel a reimbursement. + +## As an admin, can I edit users’ expenses and delete them from reports? + +No. Only users can edit and delete expenses on the Free plan. Admin control of submitted expenses on a workspace is a feature of our paid plans. If you need something changed, let the user know by commenting in the Report History section of the report on www.expensify.com or by chatting with them in new.expensify.com. diff --git a/docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md b/docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/expensify-partner-program/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- 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/articles/new-expensify/send-payments/Coming-Soon.md b/docs/articles/new-expensify/send-payments/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/send-payments/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/docs/assets/images/Uber1.png b/docs/assets/images/Uber1.png new file mode 100644 index 000000000000..d5a7d651c6b9 Binary files /dev/null and b/docs/assets/images/Uber1.png differ diff --git a/docs/assets/images/Uber2.png b/docs/assets/images/Uber2.png new file mode 100644 index 000000000000..27ac9925a900 Binary files /dev/null and b/docs/assets/images/Uber2.png differ 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 6e6f1d2ca83c..de36428b1b37 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.7 + 1.4.11 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.7.2 + 1.4.11.6 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e380b3c71728..e805f0e55da1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.7 + 1.4.11 CFBundleSignature ???? CFBundleVersion - 1.4.7.2 + 1.4.11.6 diff --git a/ios/expensify_chat_adhoc.mobileprovision.gpg b/ios/expensify_chat_adhoc.mobileprovision.gpg index 1dae451f168c..f4691df10d67 100644 Binary files a/ios/expensify_chat_adhoc.mobileprovision.gpg and b/ios/expensify_chat_adhoc.mobileprovision.gpg differ diff --git a/package-lock.json b/package-lock.json index d3b3844889bb..66b3fdc72cb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.7-2", + "version": "1.4.11-6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.7-2", + "version": "1.4.11-6", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -96,7 +96,7 @@ "react-native-pdf": "^6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -44514,9 +44514,9 @@ } }, "node_modules/react-native-picker-select": { - "version": "8.0.4", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", + "version": "8.1.0", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -84854,9 +84854,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", - "integrity": "sha512-3U/mtHN/pKC5yXtJnqj5rre8+4YPSqoXCn/3qKjb5u8BMIiuc5H3KJ0ZbKlZEg/8Uh4j0cvrtcNasdPgMqRgCQ==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", + "integrity": "sha512-ly0ZCt3K4RX7t9lfSb2OSGAw0cv8UqdMoxNfh5j+KujYYq+N8VsI9O/lmqquNeX/AMp5hM3fjetEWue4nZw/hA==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "requires": { "lodash.isequal": "^4.5.0" } diff --git a/package.json b/package.json index 66d13d2834c3..addbb623f592 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.7-2", + "version": "1.4.11-6", "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", @@ -143,7 +144,7 @@ "react-native-pdf": "^6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#eae05855286dc699954415cc1d629bfd8e8e47e2", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#0d15d4618f58e99c1261921111e68ee85bb3c2a8", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", diff --git a/src/App.js b/src/App.js index 2caa6b9ffc29..e273dcce1e47 100644 --- a/src/App.js +++ b/src/App.js @@ -53,6 +53,9 @@ function App() { diff --git a/src/CONST.ts b/src/CONST.ts index 3db7c60ad397..d594d63b9e09 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', @@ -483,6 +486,7 @@ const CONST = { MAX_REPORT_PREVIEW_RECEIPTS: 3, }, REPORT: { + MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, MAXIMUM_PARTICIPANTS: 8, SPLIT_REPORTID: '-2', ACTIONS: { @@ -507,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', @@ -516,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', @@ -537,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', @@ -558,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', @@ -963,6 +971,7 @@ const CONST = { GUIDES_DOMAIN: 'team.expensify.com', HELP: 'help@expensify.com', INTEGRATION_TESTING_CREDS: 'integrationtestingcreds@expensify.com', + NOTIFICATIONS: 'notifications@expensify.com', PAYROLL: 'payroll@expensify.com', QA: 'qa@expensify.com', QA_TRAVIS: 'qa+travisreceipts@expensify.com', @@ -982,6 +991,7 @@ const CONST = { FIRST_RESPONDER: Number(Config?.EXPENSIFY_ACCOUNT_ID_FIRST_RESPONDER ?? 9375152), HELP: Number(Config?.EXPENSIFY_ACCOUNT_ID_HELP ?? -1), INTEGRATION_TESTING_CREDS: Number(Config?.EXPENSIFY_ACCOUNT_ID_INTEGRATION_TESTING_CREDS ?? -1), + NOTIFICATIONS: Number(Config?.EXPENSIFY_ACCOUNT_ID_NOTIFICATIONS ?? 11665625), PAYROLL: Number(Config?.EXPENSIFY_ACCOUNT_ID_PAYROLL ?? 9679724), QA: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA ?? 3126513), QA_TRAVIS: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA_TRAVIS ?? 8595733), @@ -1131,6 +1141,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: { @@ -1143,6 +1155,11 @@ const CONST = { SPLIT: 'split', REQUEST: 'request', }, + REQUEST_TYPE: { + DISTANCE: 'distance', + MANUAL: 'manual', + SCAN: 'scan', + }, REPORT_ACTION_TYPE: { PAY: 'pay', CREATE: 'create', @@ -1408,6 +1425,7 @@ const CONST = { this.EMAIL.FIRST_RESPONDER, this.EMAIL.HELP, this.EMAIL.INTEGRATION_TESTING_CREDS, + this.EMAIL.NOTIFICATIONS, this.EMAIL.PAYROLL, this.EMAIL.QA, this.EMAIL.QA_TRAVIS, @@ -2858,6 +2876,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', @@ -2911,7 +2932,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/Expensify.js b/src/Expensify.js index 1b692f86a197..aece93c0ff4d 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import ConfirmModal from './components/ConfirmModal'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; +import FocusModeNotification from './components/FocusModeNotification'; import GrowlNotification from './components/GrowlNotification'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; @@ -76,6 +77,9 @@ const propTypes = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: PropTypes.bool, + /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ + focusModeNotification: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -88,6 +92,7 @@ const defaultProps = { isSidebarLoaded: false, screenShareRequest: null, isCheckingPublicRoom: true, + focusModeNotification: false, }; const SplashScreenHiddenContext = React.createContext({}); @@ -221,6 +226,7 @@ function Expensify(props) { isVisible /> ) : null} + {props.focusModeNotification ? : null} )} @@ -261,6 +267,10 @@ export default compose( screenShareRequest: { key: ONYXKEYS.SCREEN_SHARE_REQUEST, }, + focusModeNotification: { + key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, + initWithStoredValues: false, + }, }), )(Expensify); diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5576eb64736d..9cd55b41455b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -152,6 +152,12 @@ const ONYXKEYS = { /** The user's cash card and imported cards (including the Expensify Card) */ CARD_LIST: 'cardList', + /** Whether the user has tried focus mode yet */ + NVP_TRY_FOCUS_MODE: 'tryFocusMode', + + /** Boolean flag used to display the focus mode notification */ + FOCUS_MODE_NOTIFICATION: 'focusModeNotification', + /** Stores information about the user's saved statements */ WALLET_STATEMENT: 'walletStatement', @@ -265,6 +271,7 @@ const ONYXKEYS = { REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', SECURITY_GROUP: 'securityGroup_', TRANSACTION: 'transactions_', + TRANSACTION_VIOLATIONS: 'transactionViolations_', // Holds temporary transactions used during the creation and edit flow TRANSACTION_DRAFT: 'transactionsDraft_', @@ -382,6 +389,8 @@ type OnyxValues = { [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; + [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; + [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; @@ -389,7 +398,7 @@ type OnyxValues = { [ONYXKEYS.IS_PLAID_DISABLED]: boolean; [ONYXKEYS.PLAID_LINK_TOKEN]: string; [ONYXKEYS.ONFIDO_TOKEN]: string; - [ONYXKEYS.NVP_PREFERRED_LOCALE]: ValueOf; + [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale; [ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet; [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; @@ -407,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; @@ -419,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; 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 a955cf821f7f..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 = { @@ -26,13 +25,13 @@ const defaultProps = { function CurrentLocationButton({onPress, isDisabled}) { const styles = useThemeStyles(); - const theme = useTheme(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( + () => { ReportActionContextMenu.hideContextMenu(); diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 60d9f669ede9..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'; @@ -46,7 +46,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimen const propTypes = { /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ - source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + source: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]), /** Optional callback to fire when we want to preview an image and approve it for use. */ onConfirm: PropTypes.func, @@ -116,7 +116,7 @@ const defaultProps = { 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); @@ -373,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)); }, }); } @@ -401,7 +401,7 @@ function AttachmentModal(props) { let headerTitle = props.headerTitle; let shouldShowDownloadButton = false; let shouldShowThreeDotsButton = false; - if (!_.isNull(props.isReceiptAttachment)) { + if (!_.isEmpty(props.report)) { headerTitle = translate(props.isReceiptAttachment ? 'common.receipt' : 'common.attachment'); shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !props.isReceiptAttachment && !isOffline; shouldShowThreeDotsButton = props.isReceiptAttachment && isModalOpen; @@ -421,10 +421,6 @@ function AttachmentModal(props) { }} onModalHide={(e) => { props.onModalHide(e); - if (onModalHideCallbackRef.current) { - onModalHideCallbackRef.current(); - } - setShouldLoadAttachment(false); }} propagateSwipe @@ -466,6 +462,7 @@ function AttachmentModal(props) { isWorkspaceAvatar={props.isWorkspaceAvatar} fallbackSource={props.fallbackSource} isUsedInAttachmentModal + transactionID={props.transaction.transactionID} /> ) )} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index cc9edd6fa5db..1642fa32734a 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -27,8 +27,8 @@ const propTypes = { /** Additional information about the attachment file */ file: PropTypes.shape({ /** File name of the attachment */ - name: PropTypes.string, - }), + name: PropTypes.string.isRequired, + }).isRequired, /** Whether the attachment has been flagged */ hasBeenFlagged: PropTypes.bool, diff --git a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js index 7543e8d9c099..72a554de68be 100644 --- a/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js +++ b/src/components/Attachments/AttachmentCarousel/attachmentCarouselPropTypes.js @@ -5,7 +5,7 @@ import reportPropTypes from '@pages/reportPropTypes'; const propTypes = { /** source is used to determine the starting index in the array of attachments */ - source: PropTypes.string, + source: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** Callback to update the parent modal's state with a source and name from the attachments array */ onNavigate: PropTypes.func, diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index 3b8d43fe842d..6994a7a210bf 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -1,5 +1,6 @@ import {Parser as HtmlParser} from 'htmlparser2'; import _ from 'underscore'; +import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; @@ -8,6 +9,7 @@ import CONST from '@src/CONST'; * Constructs the initial component state from report actions * @param {Object} parentReportAction * @param {Object} reportActions + * @param {Object} transaction * @returns {Array} */ function extractAttachmentsFromReport(parentReportAction, reportActions) { @@ -21,14 +23,17 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) { } const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; + const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); + const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); // By iterating actions in chronological order and prepending each attachment // we ensure correct order of attachments even across actions with multiple attachments. attachments.unshift({ reportActionID: attribs['data-id'], - source: tryResolveUrlFromApiRoot(expensifySource || attribs.src), + source, isAuthTokenRequired: Boolean(expensifySource), - file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]}, + file: {name: fileName}, + isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', }); }, diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 1696f4adf0b4..fb31e32de91c 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -215,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}`, diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 4a62335a492d..ea45509d6ce3 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -68,6 +68,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, setPage(newPageIndex); setActiveSource(item.source); + onNavigate(item); }, [setShouldShowArrows, attachments, onNavigate], @@ -160,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}`, 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/Attachments/propTypes.js b/src/components/Attachments/propTypes.js index 698a41de9648..13adc468ce64 100644 --- a/src/components/Attachments/propTypes.js +++ b/src/components/Attachments/propTypes.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; const attachmentSourcePropType = PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.number]); const attachmentFilePropType = PropTypes.shape({ - name: PropTypes.string, + name: PropTypes.string.isRequired, }); const attachmentPropType = PropTypes.shape({ @@ -13,7 +13,7 @@ const attachmentPropType = PropTypes.shape({ source: attachmentSourcePropType.isRequired, /** File object can be an instance of File or Object */ - file: attachmentFilePropType, + file: attachmentFilePropType.isRequired, }); const attachmentsPropType = PropTypes.arrayOf(attachmentPropType); diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index f24b82d8e867..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'; @@ -40,7 +39,7 @@ function BaseAutoCompleteSuggestions( ref: ForwardedRef, ) { const styles = useThemeStyles(); - const theme = useTheme(); + 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.js b/src/components/Avatar.js deleted file mode 100644 index 0e173f856593..000000000000 --- a/src/components/Avatar.js +++ /dev/null @@ -1,126 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import useNetwork from '@hooks/useNetwork'; -import * as ReportUtils from '@libs/ReportUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import * as StyleUtils from '@styles/StyleUtils'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import CONST from '@src/CONST'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import Image from './Image'; - -const propTypes = { - /** Source for the avatar. Can be a URL or an icon. */ - source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - - /** Extra styles to pass to Image */ - // eslint-disable-next-line react/forbid-prop-types - imageStyles: PropTypes.arrayOf(PropTypes.object), - - /** Additional styles to pass to Icon */ - // eslint-disable-next-line react/forbid-prop-types - iconAdditionalStyles: PropTypes.arrayOf(PropTypes.object), - - /** Extra styles to pass to View wrapper */ - containerStyles: stylePropTypes, - - /** Set the size of Avatar */ - size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - - /** - * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' - * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. - */ - fill: PropTypes.string, - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. - * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. - */ - fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - - /** Denotes whether it is an avatar or a workspace avatar */ - type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name: PropTypes.string, -}; - -const defaultProps = { - source: null, - imageStyles: [], - iconAdditionalStyles: [], - containerStyles: [], - size: CONST.AVATAR_SIZE.DEFAULT, - fill: undefined, - fallbackIcon: Expensicons.FallbackAvatar, - type: CONST.ICON_TYPE_AVATAR, - name: '', -}; - -function Avatar(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - const [imageError, setImageError] = useState(false); - - useNetwork({onReconnect: () => setImageError(false)}); - - useEffect(() => { - setImageError(false); - }, [props.source]); - - if (!props.source) { - return null; - } - - const isWorkspace = props.type === CONST.ICON_TYPE_WORKSPACE; - const iconSize = StyleUtils.getAvatarSize(props.size); - - const imageStyle = - props.imageStyles && props.imageStyles.length - ? [StyleUtils.getAvatarStyle(theme, props.size), ...props.imageStyles, styles.noBorderRadius] - : [StyleUtils.getAvatarStyle(theme, props.size), styles.noBorderRadius]; - - const iconStyle = props.imageStyles && props.imageStyles.length ? [StyleUtils.getAvatarStyle(theme, props.size), styles.bgTransparent, ...props.imageStyles] : undefined; - - const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(props.name).fill : props.fill || theme.icon; - const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(props.name) : props.fallbackIcon || Expensicons.FallbackAvatar; - - return ( - - {_.isFunction(props.source) || (imageError && _.isFunction(fallbackAvatar)) ? ( - - - - ) : ( - - setImageError(true)} - /> - - )} - - ); -} - -Avatar.defaultProps = defaultProps; -Avatar.propTypes = propTypes; -Avatar.displayName = 'Avatar'; - -export default Avatar; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx new file mode 100644 index 000000000000..f801cb11e9df --- /dev/null +++ b/src/components/Avatar.tsx @@ -0,0 +1,119 @@ +import React, {useEffect, useState} from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import * as ReportUtils from '@libs/ReportUtils'; +import {AvatarSource} from '@libs/UserUtils'; +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'; +import * as Expensicons from './Icon/Expensicons'; +import Image from './Image'; + +type AvatarProps = { + /** Source for the avatar. Can be a URL or an icon. */ + source?: AvatarSource; + + /** Extra styles to pass to Image */ + imageStyles?: StyleProp; + + /** Additional styles to pass to Icon */ + iconAdditionalStyles?: StyleProp; + + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; + + /** Set the size of Avatar */ + size?: AvatarSizeName; + + /** + * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' + * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. + */ + fill?: string; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. + * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. + */ + fallbackIcon?: AvatarSource; + + /** Denotes whether it is an avatar or a workspace avatar */ + type?: AvatarType; + + /** Owner of the avatar. If user, displayName. If workspace, policy name */ + name?: string; +}; + +function Avatar({ + source, + imageStyles, + iconAdditionalStyles, + containerStyles, + size = CONST.AVATAR_SIZE.DEFAULT, + fill, + fallbackIcon = Expensicons.FallbackAvatar, + type = CONST.ICON_TYPE_AVATAR, + name = '', +}: AvatarProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const [imageError, setImageError] = useState(false); + + useNetwork({onReconnect: () => setImageError(false)}); + + useEffect(() => { + setImageError(false); + }, [source]); + + if (!source) { + return null; + } + + const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; + const iconSize = StyleUtils.getAvatarSize(size); + + 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; + + const avatarSource = imageError ? fallbackAvatar : source; + + return ( + + {typeof avatarSource === 'function' ? ( + + + + ) : ( + + setImageError(true)} + /> + + )} + + ); +} + +Avatar.displayName = 'Avatar'; + +export default Avatar; diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js index 07823eb68407..419891d9bdef 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/AvatarSkeleton.js b/src/components/AvatarSkeleton.tsx similarity index 100% rename from src/components/AvatarSkeleton.js rename to src/components/AvatarSkeleton.tsx diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.tsx similarity index 72% rename from src/components/AvatarWithDisplayName.js rename to src/components/AvatarWithDisplayName.tsx index c584929cdbdf..3cc39f9e815f 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,83 +1,77 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -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 CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; -import participantPropTypes from './participantPropTypes'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -const propTypes = { +type AvatarWithDisplayNamePropsWithOnyx = { + /** All of the actions of the report */ + parentReportActions: OnyxEntry; +}; + +type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; /** The policy which the user has access to and which the report is tied to */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), + policy?: OnyxEntry; /** The size of the avatar */ - size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), + size?: ValueOf; /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), + personalDetails: OnyxCollection; /** Whether if it's an unauthenticated user */ - isAnonymous: PropTypes.bool, + isAnonymous?: boolean; - shouldEnableDetailPageNavigation: PropTypes.bool, - - /* Onyx Props */ - /** All of the actions of the report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** Whether we should enable detail page navigation */ + shouldEnableDetailPageNavigation?: boolean; }; -const defaultProps = { - personalDetails: {}, - policy: {}, - report: {}, - parentReportActions: {}, - isAnonymous: false, - size: CONST.AVATAR_SIZE.DEFAULT, - shouldEnableDetailPageNavigation: false, -}; - -function AvatarWithDisplayName({report, policy, size, isAnonymous, parentReportActions, personalDetails, shouldEnableDetailPageNavigation}) { +function AvatarWithDisplayName({ + personalDetails, + policy, + report, + parentReportActions, + isAnonymous = false, + size = CONST.AVATAR_SIZE.DEFAULT, + shouldEnableDetailPageNavigation = false, +}: 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); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report); - const icons = ReportUtils.getIcons(report, personalDetails, policy); - const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([report.ownerAccountID], personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(_.values(ownerPersonalDetails), false); + const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); + const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); const isExpenseRequest = ReportUtils.isExpenseRequest(report); const defaultSubscriptSize = isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : size; const avatarBorderColor = isAnonymous ? theme.highlightBG : theme.componentBG; - const actorAccountID = useRef(null); + const actorAccountID = useRef(null); useEffect(() => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID], {}); - actorAccountID.current = lodashGet(parentReportAction, 'actorAccountID', -1); + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + actorAccountID.current = parentReportAction?.actorAccountID ?? -1; }, [parentReportActions, report]); const showActorDetails = useCallback(() => { @@ -86,31 +80,33 @@ function AvatarWithDisplayName({report, policy, size, isAnonymous, parentReportA return ReportUtils.navigateToDetailsPage(report); } - if (ReportUtils.isExpenseReport(report)) { + if (ReportUtils.isExpenseReport(report) && report?.ownerAccountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(report.ownerAccountID)); return; } - if (ReportUtils.isIOUReport(report)) { + if (ReportUtils.isIOUReport(report) && report?.reportID) { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID)); return; } if (ReportUtils.isChatThread(report)) { // In an ideal situation account ID won't be 0 - if (actorAccountID.current > 0) { + if (actorAccountID.current && actorAccountID.current > 0) { Navigation.navigate(ROUTES.PROFILE.getRoute(actorAccountID.current)); return; } } - // Report detail route is added as fallback but based on the current implementation this route won't be executed - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); + if (report?.reportID) { + // Report detail route is added as fallback but based on the current implementation this route won't be executed + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); + } }, [report, shouldEnableDetailPageNavigation]); const headerView = ( - {Boolean(report && title) && ( + {report && !!title && ( - {!_.isEmpty(parentNavigationSubtitleData) && ( + {Object.keys(parentNavigationSubtitleData).length > 0 && ( )} - {!_.isEmpty(subtitle) && ( + {!!subtitle && ( ); } -AvatarWithDisplayName.propTypes = propTypes; + AvatarWithDisplayName.displayName = 'AvatarWithDisplayName'; -AvatarWithDisplayName.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ parentReportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, canEvict: false, diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 922ee385ab89..b670921dff4c 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 011fe3955d34..3eb2316aa9d0 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'; @@ -42,7 +41,7 @@ type BannerProps = { function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { const styles = useThemeStyles(); - const theme = useTheme(); + 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 035d8bf4b981..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 styles = useThemeStyles(); - const theme = useTheme(); - 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 4e45687fe853..eb99d4b09396 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..321cd79f1318 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,6 +74,7 @@ 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); 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.ios.js b/src/components/Composer/index.ios.js index eb227de36a54..9852e607562b 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -66,8 +66,8 @@ const defaultProps = { function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isComposerFullSize, setIsFullComposerAvailable, ...props}) { const textInput = useRef(null); - const styles = useThemeStyles(); const theme = useTheme(); + const styles = useThemeStyles(); /** * Set the TextInput Ref 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/ConfirmationPage.js b/src/components/ConfirmationPage.tsx similarity index 56% rename from src/components/ConfirmationPage.js rename to src/components/ConfirmationPage.tsx index ac56ea3d22e9..12e8b40a0f25 100644 --- a/src/components/ConfirmationPage.js +++ b/src/components/ConfirmationPage.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; @@ -6,61 +5,52 @@ import Button from './Button'; import FixedFooter from './FixedFooter'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; +import DotLottieAnimation from './LottieAnimations/types'; import Text from './Text'; -const propTypes = { +type ConfirmationPageProps = { /** The asset to render */ - // eslint-disable-next-line react/forbid-prop-types - animation: PropTypes.object, + animation?: DotLottieAnimation; /** Heading of the confirmation page */ - heading: PropTypes.string, + heading: string; /** Description of the confirmation page */ - description: PropTypes.string, + description: string; /** The text for the button label */ - buttonText: PropTypes.string, + buttonText?: string; /** A function that is called when the button is clicked on */ - onButtonPress: PropTypes.func, + onButtonPress?: () => void; /** Whether we should show a confirmation button */ - shouldShowButton: PropTypes.bool, + shouldShowButton?: boolean; }; -const defaultProps = { - animation: LottieAnimations.Fireworks, - heading: '', - description: '', - buttonText: '', - onButtonPress: () => {}, - shouldShowButton: false, -}; - -function ConfirmationPage(props) { +function ConfirmationPage({animation = LottieAnimations.Fireworks, heading, description, buttonText = '', onButtonPress = () => {}, shouldShowButton = false}: ConfirmationPageProps) { const styles = useThemeStyles(); + return ( <> - {props.heading} - {props.description} + {heading} + {description} - {props.shouldShowButton && ( + {shouldShowButton && (