diff --git a/android/app/build.gradle b/android/app/build.gradle index 64ba323e74f..a312c54bcb1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -156,8 +156,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001022600 - versionName "1.2.26-0" + versionCode 1001022703 + versionName "1.2.27-3" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 329bee971e4..c88bd5d3ad3 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -7,6 +7,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin'); const dotenv = require('dotenv'); const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer'); +const HtmlInlineScriptPlugin = require('html-inline-script-webpack-plugin'); const CustomVersionFilePlugin = require('./CustomVersionFilePlugin'); const includeModules = [ @@ -23,6 +24,20 @@ const includeModules = [ '@react-navigation/drawer', ].join('|'); +const envToLogoSuffixMap = { + production: '', + staging: 'stg', + dev: 'dev', +}; + +function mapEnvToLogoSuffix(envFile) { + let env = envFile.split('.')[2]; + if (typeof env === 'undefined') { + env = 'dev'; + } + return envToLogoSuffixMap[env]; +} + /** * Get a production grade config for web or desktop * @param {Object} env @@ -33,10 +48,13 @@ const includeModules = [ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ mode: 'production', devtool: 'source-map', - entry: [ - 'babel-polyfill', - './index.js', - ], + entry: { + main: [ + 'babel-polyfill', + './index.js', + ], + splash: ['./web/splash/splash.js'], + }, output: { filename: '[name]-[contenthash].bundle.js', path: path.resolve(__dirname, '../../dist'), @@ -58,6 +76,9 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ filename: 'index.html', usePolyfillIO: platform === 'web', }), + new HtmlInlineScriptPlugin({ + scriptMatchPattern: [/splash.+[.]js$/], + }), new ProvidePlugin({ process: 'process/browser', }), @@ -144,6 +165,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // Load svg images { test: /\.svg$/, + resourceQuery: {not: [/raw/]}, exclude: /node_modules/, use: [ { @@ -151,14 +173,29 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ }, ], }, + { + test: /splash.css$/i, + use: [{ + loader: 'style-loader', + options: { + insert: 'head', + injectType: 'singletonStyleTag', + }, + }], + }, { test: /\.css$/i, use: ['style-loader', 'css-loader'], }, + { + resourceQuery: /raw/, + type: 'asset/source', + }, ], }, resolve: { alias: { + logo$: path.resolve(__dirname, `../../assets/images/new-expensify-${mapEnvToLogoSuffix(envFile)}.svg`), 'react-native-config': 'react-web-config', 'react-native$': '@expensify/react-native-web', 'react-native-web': '@expensify/react-native-web', diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 4743f8c418a..9fe36b97327 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -1,5 +1,5 @@ # Contributing to Expensify -Welcome! Thanks for checking out the new Expensify app and for taking the time to contribute! +Welcome! Thanks for checking out the New Expensify app and taking the time to contribute! ## Getting Started If you would like to become an Expensify contributor, the first step is to read this document in its entirety. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation. @@ -7,13 +7,15 @@ If you would like to become an Expensify contributor, the first step is to read #### Test Accounts You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. -**Note**: When testing chat functionality in the app please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you. +**Notes**: + +1. When testing chat functionality in the app please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you. +2. A member of our customer onboarding team gets auto-assigned to every new policy created by a non-paying account to help them set up. Please **do not interact with these teams, ask for calls, or support on your issues.** If you do need to test functionality inside the defaultRooms (#admins & #announce) for any issues you’re working on, please let them know that you are a contributor and don’t need assistance. They will proceed to ignore the chat. ##### Generating Multiple Test Accounts You can generate multiple test accounts by using a `+` postfix, for example if your email is test@test.com, you can create multiple New Expensify accounts connected to the same email address by using test+123@test.com, test+456@test.com, etc. #### Working on beta features - Some features are locked behind beta flags while development is ongoing. As a contributor you can work on these beta features locally by overriding the [`Permissions.canUseAllBetas` function](https://github.com/Expensify/App/blob/5e268df7f2989ed04bc64c0c86ed77faf134554d/src/libs/Permissions.js#L10-L12) to return `true`. ## Code of Conduct @@ -22,10 +24,17 @@ This project and everyone participating in it is governed by the Expensify [Code ## Restrictions At this time, we are not hiring contractors in Crimea, North Korea, Russia, Iran, Cuba, or Syria. -## Asking Questions -If you have any general questions, please ask in the #expensify-open-source Slack channel. To request an invite to the channel, just email contributors@expensify.com with the subject `Slack Channel Invite` and include a link to your Upwork profile. We'll send you an invite! Note: The Expensify team will not be able to respond to direct messages in Slack. +## Slack channels +All contributors should be a member of **two** Slack channels: + +1. #expensify-open-source -- used to ask **general questions**, facilitate **discussions**, and make **feature requests**. +2. #expensify-bugs -- used to discuss or report **bugs** specifically. -If you are hired for an Upwork job and have any job-specific questions, please ask in the GitHub issue or pull request. This will ensure that the person addressing your question has as much context as possible. +To request an invite to these two Slack channels, just email contributors@expensify.com with the subject `Slack Channel Invites` and include a link to your Upwork profile. We'll send you an invite! + +Note: the Expensify team will not be able to respond to direct messages in Slack. + +Note: if you are hired for an Upwork job and have any job-specific questions, please ask in the GitHub issue or pull request. This will ensure that the person addressing your question has as much context as possible. ## Reporting Vulnerabilities If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue. @@ -48,26 +57,26 @@ Please be aware that compensation for any support in solving an issue is provide - No PR within 12 business days - **Contract terminated** ## Finding Jobs -There are two ways you can find a job that you can contribute to: +A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: #### Finding a job that Expensify posted -This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view open jobs by searching for issues in GitHub with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). +This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) (you must be signed in to Upwork to view jobs). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow. You can also view all open jobs in the Expensify/App GH repository by searching for GH issues with the [`Help Wanted` label](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22). Lastly, you can follow the [@ExpensifyOSS](https://twitter.com/ExpensifyOSS) Twitter account to see a live feed of jobs that are posted. #### Proposing a job that Expensify hasn't posted - -It’s possible that you found a bug or enhancement that we haven’t posted to the [GitHub repository](https://github.com/Expensify/App/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution. If it's a valid job proposal that we choose to implement by deploying it to production — either internally or via an external contributor — then we will compensate you $250 for identifying and proposing the improvement. If the bug is fixed by a PR that is not associated with your bug report, the contributor is not eligible for compensation unless they can find the PR that fixed the bug then show their bug report preceded the one associated with the PR. +It’s possible that you found a new bug or new feature that we haven’t posted as a job to the [GitHub repository](https://github.com/Expensify/App/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution for that job. If it's a valid job proposal that we choose to implement by deploying it to production — either internally or via an external contributor — then we will compensate you $250 for identifying and proposing the bug or feature. If the bug or feature is fixed by a PR that is not associated with your proposal, then you will not be eligible for the corresponding compensation unless you can find the PR that fixed it and prove your proposal came first. - Note: If you get assigned the job you proposed **and** you complete the job, this $250 for identifying the improvement is *in addition to* the reward you will be paid for completing the job. -- Note about proposed improvements: Expensify has the right not to pay the $250 reward if the suggested improvement is already planned. Currently, Expensify plans to implement all features of the old Expensify app in New Expensify. +- Note about proposed bugs or features: Expensify has the right not to pay the $250 reward if the suggested bug or feature is already planned. Currently, Expensify plans to implement all features of the old Expensify app in New Expensify. Please follow these steps to propose a job: -1. Check to ensure an issue does not already exist for this topic in the [New Expensify Issue list](https://github.com/Expensify/App/issues) or as a reported `Bug:` in the [#expensify-open-source](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#asking-questions) Slack channel. Please use your best judgement by searching for similar titles and issue descriptions. -2. If your bug or enhancement matches an existing issue, please feel free to comment on that GitHub issue with your findings if you think it will help solve the issue. +1. Check to ensure a GH issue does not already exist for this job in the [New Expensify Issue list](https://github.com/Expensify/App/issues). +2. Check to ensure the `Bug:` or `Feature Request:` was not already posted in Slack (specifically the #expensify-bugs or #expensify-open-source [Slack channels](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#slack-channels)). Use your best judgement by searching for similar titles and issue descriptions. +3. If your bug or new feature matches with an existing issue, please comment on that Slack thread or GitHub issue with your findings if you think it will help solve the issue. 4. If there is no existing GitHub issue or Upwork job, check if the issue is happening on prod (as opposed to only happening on dev) 5. If the issue is just in dev then it means it's a new issue and has not been deployed to production. In this case, you should try to find the offending PR and comment in the issue tied to the PR and ask the assigned users to add the `DeployBlockerCash` label. If you can't find it, follow the reporting instructions in the next item, but note that the issue is a regression only found in dev and not in prod. -6. If the issue happens in main, staging, or production then report the issue(s) in the #expensify-open-source Slack channel, prefixed with `BUG:` or `Feature Request:`. Please use the templates for bugs and feature requests that are bookmarked in #expensify-open-source. View [this guide](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_CREATE_A_PLAN.md) for help creating a plan when proposing a feature request. -7. After review in #expensify-open-source, if you've provided a quality proposal that we choose to implement, a GitHub issue will be created and your Slack handle will be included in the original post after `Issue reported by:` -8. If an external contributor other than yourself is hired to work on the issue, you will also be hired for the same job in Upwork. No additional work is needed. If the issue is fixed internally, a dedicated job will be created to hire and pay you after the issue is fixed. +6. If the issue happens in main, staging, or production then report the issue(s) in the #expensify-bugs Slack channel, prefixed with `Bug:` or `Feature Request:`. Please use the templates for bugs and feature requests that are bookmarked in the #expensify-bugs channel. View [this guide](https://github.com/Expensify/App/blob/main/contributingGuides/HOW_TO_CREATE_A_PLAN.md) for help creating a plan when proposing a feature request. +7. The Expensify team will review your job proposal in the appropriate slack channel. If you've provided a quality proposal that we choose to implement, a GitHub issue will be created and your Slack handle will be included in the original post after `Issue reported by:` +8. If an external contributor other than yourself is hired to work on the issue, you will also be hired for the same job in Upwork to receive your payout. No additional work is required. If the issue is fixed internally, a dedicated job will be created to hire and pay you after the issue is fixed. 9. Payment will be made 7 days after code is deployed to production if there are no regressions. If a regression is discovered, payment will be issued 7 days after all regressions are fixed. >**Note:** Our problem solving approach at Expensify is to focus on high value problems and avoid small optimizations with results that are difficult to measure. We also prefer to identify and solve problems at their root. Given that, please ensure all proposed jobs fix a specific problem in a measurable way with evidence so they are easy to evaluate. Here's an example of a good problem/solution: @@ -80,7 +89,7 @@ Please follow these steps to propose a job: *Reminder: For technical guidance please refer to the [README](https://github.com/Expensify/App/blob/main/README.md)*. ## Posting Ideas -Additionally if you want to discuss an idea with the community without having a P/S statement yet, you can post it in #expensify-open-source with the prefix `IDEA:`. All ideas to build the future of Expensify are always welcome! i.e.: "`IDEA:` I don't have a P/S for this yet, but just kicking the idea around... what if we [insert crazy idea]?". +Additionally if you want to discuss an idea with the open source community without having a P/S statement yet, you can post it in #expensify-open-source with the prefix `IDEA:`. All ideas to build the future of Expensify are always welcome! i.e.: "`IDEA:` I don't have a P/S for this yet, but just kicking the idea around... what if we [insert crazy idea]?". #### Make sure you can test on all platforms * Expensify requires that you can test the app on iOS, MacOS, Android, Web, and mWeb. diff --git a/docs/_data/routes.yml b/docs/_data/routes.yml index 6cebdefee5b..fa92add3b41 100644 --- a/docs/_data/routes.yml +++ b/docs/_data/routes.yml @@ -9,15 +9,17 @@ hubs: title: Send money description: With only a couple of clicks, send money to your friends or coworkers. icon: /assets/images/send.svg - articles: - - href: Request-and-Send-Money - title: Request and Send Money sections: - href: workspaces title: Workspaces articles: - href: The-Free-Plan title: The Free Plan + - href: paying-friends + title: Paying Friends + articles: + - href: Request-and-Send-Money + title: Request and Send Money - href: chat title: Chat articles: diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 809463dd79b..87ce5c717e8 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -14,6 +14,10 @@ html { line-height: 1; + + @include maxBreakpoint($breakpoint-tablet) { + scroll-padding-top: 80px; + } } table { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index dcc764f10ff..3dbb5542741 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.26 + 1.2.27 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.26.0 + 1.2.27.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0affc6c431d..b008ec36951 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.26 + 1.2.27 CFBundleSignature ???? CFBundleVersion - 1.2.26.0 + 1.2.27.3 diff --git a/package-lock.json b/package-lock.json index 53241fceaad..6ee9d38168b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.26-0", + "version": "1.2.27-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.26-0", + "version": "1.2.27-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -138,6 +138,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-storybook": "^0.5.13", "flipper-plugin-bridgespy-client": "^0.1.9", + "html-inline-script-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^5.5.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", @@ -25881,6 +25882,20 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-inline-script-webpack-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-inline-script-webpack-plugin/-/html-inline-script-webpack-plugin-3.1.0.tgz", + "integrity": "sha512-Kscqm4fNWxvQXt+R0o1UV9/ug0iZTEdYKXtrDuyauwI9FUe1azR8QXB3ZHkGupXzLF3lsvWheFb9WZT/a8aKtw==", + "dev": true, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "html-webpack-plugin": "^5.0.0", + "webpack": "^5.0.0" + } + }, "node_modules/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -62376,6 +62391,13 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-inline-script-webpack-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/html-inline-script-webpack-plugin/-/html-inline-script-webpack-plugin-3.1.0.tgz", + "integrity": "sha512-Kscqm4fNWxvQXt+R0o1UV9/ug0iZTEdYKXtrDuyauwI9FUe1azR8QXB3ZHkGupXzLF3lsvWheFb9WZT/a8aKtw==", + "dev": true, + "requires": {} + }, "html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", diff --git a/package.json b/package.json index 7e3fe98b3a2..bdc8873b50c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.26-0", + "version": "1.2.27-3", "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.", @@ -167,6 +167,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-storybook": "^0.5.13", "flipper-plugin-bridgespy-client": "^0.1.9", + "html-inline-script-webpack-plugin": "^3.1.0", "html-webpack-plugin": "^5.5.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index 56dc37d5a44..e28eefbd122 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -78,6 +78,7 @@ class BigNumberPad extends React.Component { ControlSelection.unblock(); this.props.longPressHandlerStateChanged(false); }} + onMouseDown={e => e.preventDefault()} /> ); })} diff --git a/src/components/Button.js b/src/components/Button.js index e50d13abcbb..863fcee4329 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -63,6 +63,9 @@ const propTypes = { /** A function that is called when the button is released */ onPressOut: PropTypes.func, + /** Callback that is called when mousedown is triggered. */ + onMouseDown: PropTypes.func, + /** Call the onPress function when Enter key is pressed */ pressOnEnter: PropTypes.bool, @@ -124,6 +127,7 @@ const defaultProps = { onLongPress: () => {}, onPressIn: () => {}, onPressOut: () => {}, + onMouseDown: undefined, pressOnEnter: false, enterKeyEventListenerPriority: 0, style: [], @@ -249,6 +253,7 @@ class Button extends Component { }} onPressIn={this.props.onPressIn} onPressOut={this.props.onPressOut} + onMouseDown={this.props.onMouseDown} disabled={this.props.isLoading || this.props.isDisabled} style={[ this.props.isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {}, diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index d67c4b883a4..f06ed560274 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -72,7 +72,11 @@ class Hoverable extends Component { if (!this.state.isHovered) { return; } - if (this.wrapperView && !this.wrapperView.contains(event.target) && this.props.resetsOnClickOutside) { + if (this.props.resetsOnClickOutside) { + this.setIsHovered(false); + return; + } + if (this.wrapperView && !this.wrapperView.contains(event.target)) { this.setIsHovered(false); } } diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index a9cc1f057ca..b2c9e7c86da 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -56,7 +56,7 @@ const MenuItem = (props) => { styles.popoverMenuText, styles.ml3, (props.shouldShowBasicTitle ? undefined : styles.textStrong), - (props.interactive && props.disabled ? styles.disabledText : undefined), + (props.interactive && props.disabled ? {...styles.disabledText, ...styles.userSelectNone} : undefined), ], props.style); const descriptionTextStyle = StyleUtils.combineStyles([styles.textLabelSupporting, styles.ml3, styles.breakAll, styles.lh16], props.style); diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index b4c9079ca68..4fb45de1724 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -114,12 +114,13 @@ class PDFView extends Component { // If we're requesting a password then we need to hide - but still render - // the PDF component. const pdfContainerStyle = this.state.shouldRequestPassword - ? [styles.PDFView, this.props.style, styles.invisible] - : [styles.PDFView, this.props.style]; + ? [styles.PDFView, styles.noSelect, this.props.style, styles.invisible] + : [styles.PDFView, styles.noSelect, this.props.style]; return ( this.setState({windowWidth: event.nativeEvent.layout.width})} > diff --git a/src/components/Picker/BasePicker/basePickerStyles/index.android.js b/src/components/Picker/BasePicker/basePickerStyles/index.android.js index bfa6306fa7e..a4853f4ed2f 100644 --- a/src/components/Picker/BasePicker/basePickerStyles/index.android.js +++ b/src/components/Picker/BasePicker/basePickerStyles/index.android.js @@ -1,9 +1,9 @@ import styles from '../../../../styles/styles'; -const pickerStyles = (disabled, error, focused) => ({ - ...styles.picker(disabled, error, focused), +const pickerStyles = disabled => ({ + ...styles.picker(disabled), inputAndroid: { - ...styles.picker(disabled, error, focused).inputNative, + ...styles.picker(disabled).inputNative, paddingLeft: 12, }, }); diff --git a/src/components/Picker/BasePicker/basePickerStyles/index.ios.js b/src/components/Picker/BasePicker/basePickerStyles/index.ios.js index b5fc8289ba3..785e0824e52 100644 --- a/src/components/Picker/BasePicker/basePickerStyles/index.ios.js +++ b/src/components/Picker/BasePicker/basePickerStyles/index.ios.js @@ -1,8 +1,8 @@ import styles from '../../../../styles/styles'; -const pickerStyles = (disabled, error, focused) => ({ - ...styles.picker(disabled, error, focused), - inputIOS: styles.picker(disabled, error, focused).inputNative, +const pickerStyles = disabled => ({ + ...styles.picker(disabled), + inputIOS: styles.picker(disabled).inputNative, }); export default pickerStyles; diff --git a/src/components/Picker/BasePicker/basePickerStyles/index.js b/src/components/Picker/BasePicker/basePickerStyles/index.js index 8531ccbe3bb..7e242263841 100644 --- a/src/components/Picker/BasePicker/basePickerStyles/index.js +++ b/src/components/Picker/BasePicker/basePickerStyles/index.js @@ -11,10 +11,10 @@ const pickerStylesWeb = () => { return {}; }; -const pickerStyles = (disabled, error, focused) => ({ - ...styles.picker(disabled, error, focused), +const pickerStyles = disabled => ({ + ...styles.picker(disabled), inputWeb: { - ...styles.picker(disabled, error, focused).inputWeb, + ...styles.picker(disabled).inputWeb, ...pickerStylesWeb(), }, }); diff --git a/src/components/Picker/BasePicker/index.js b/src/components/Picker/BasePicker/index.js index 8a32ab7738e..e6974389076 100644 --- a/src/components/Picker/BasePicker/index.js +++ b/src/components/Picker/BasePicker/index.js @@ -40,12 +40,11 @@ class BasePicker extends React.Component { } render() { - const hasError = !_.isEmpty(this.props.errorText); return ( {this.props.label && ( @@ -87,8 +91,6 @@ class Picker extends PureComponent { onOpen={() => this.setState({isOpen: true})} onClose={() => this.setState({isOpen: false})} disabled={this.props.isDisabled} - focused={this.state.isOpen} - errorText={this.props.errorText} value={this.props.value} // eslint-disable-next-line react/jsx-props-no-spreading {...pickerProps} diff --git a/src/components/ReportActionItem/IOUQuote.js b/src/components/ReportActionItem/IOUQuote.js index 84b4b357728..5f991c8ecf0 100644 --- a/src/components/ReportActionItem/IOUQuote.js +++ b/src/components/ReportActionItem/IOUQuote.js @@ -29,7 +29,7 @@ const defaultProps = { const IOUQuote = props => ( {_.map(props.action.message, (fragment, index) => ( - + {Str.htmlDecode(fragment.text)} diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index af46f8154d0..8d3a345492a 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -189,6 +189,7 @@ class Tooltip extends PureComponent { containerStyles={this.props.containerStyles} onHoverIn={this.showTooltip} onHoverOut={this.hideTooltip} + resetsOnClickOutside > {child} diff --git a/src/components/participantPropTypes.js b/src/components/participantPropTypes.js index 3f965be445c..2977c43cbe3 100644 --- a/src/components/participantPropTypes.js +++ b/src/components/participantPropTypes.js @@ -8,7 +8,7 @@ export default PropTypes.shape({ displayName: PropTypes.string, // Avatar url of participant - avatar: PropTypes.string, + avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** First Name of the participant */ firstName: PropTypes.string, diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 6616d03511e..ae04dc86c49 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -55,14 +55,13 @@ function getSortedReportActions(reportActions) { * Finds most recent IOU report action number. * * @param {Array} reportActions - * @returns {Number} + * @returns {String} */ -function getMostRecentIOUReportSequenceNumber(reportActions) { +function getMostRecentIOUReportActionID(reportActions) { return _.chain(reportActions) - .sortBy('sequenceNumber') - .filter(action => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) + .where({actionName: CONST.REPORT.ACTIONS.TYPE.IOU}) .max(action => action.sequenceNumber) - .value().sequenceNumber; + .value().reportActionID; } /** @@ -151,7 +150,7 @@ export { getOptimisticLastReadSequenceNumberForDeletedAction, getLastVisibleMessageText, getSortedReportActions, - getMostRecentIOUReportSequenceNumber, + getMostRecentIOUReportActionID, isDeletedAction, isConsecutiveActionMadeByPreviousActor, }; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index e81e2b86319..e6e8ade3b31 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -242,6 +242,47 @@ function validateBankAccount(bankAccountID, validateCode) { }); } +function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) { + const onyxData = { + optimisticData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + errors: null, + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + }; + + const param = { + stepToOpen, + subStep, + localCurrentStep, + }; + + return API.read('OpenReimbursementAccountPage', param, onyxData); +} + /** * Updates the bank account in the database with the company step data * @@ -321,18 +362,24 @@ function verifyIdentityForBankAccount(bankAccountID, onfidoData) { }, getVBBADataForOnyx()); } +function openWorkspaceView() { + API.read('OpenWorkspaceView'); +} + export { addPersonalBankAccount, - connectBankAccountManually, + clearOnfidoToken, clearPersonalBankAccount, clearPlaid, - clearOnfidoToken, + connectBankAccountManually, connectBankAccountWithPlaid, deletePaymentBankAccount, + openReimbursementAccountPage, updateBeneficialOwnersForBankAccount, updateCompanyInformationForBankAccount, updatePersonalInformationForBankAccount, updatePlaidData, + openWorkspaceView, validateBankAccount, verifyIdentityForBankAccount, }; diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index dac287fd6f6..c49a34852c1 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -120,20 +120,24 @@ function openPaymentsPage() { * */ function getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, isOptimisticData = true) { - const onxyData = [ + const onyxData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.USER_WALLET, value: { walletLinkedAccountID: bankAccountID || fundID, walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD, - errors: null, }, }, ]; + // Only clear the error if this is optimistic data. If this is failure data, we do not want to clear the error that came from the server. + if (isOptimisticData) { + onyxData[0].value.errors = null; + } + if (previousPaymentMethod) { - onxyData.push({ + onyxData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: previousPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST, value: { @@ -145,7 +149,7 @@ function getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMet } if (currentPaymentMethod) { - onxyData.push({ + onyxData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: currentPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST, value: { @@ -156,7 +160,7 @@ function getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMet }); } - return onxyData; + return onyxData; } /** diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 49dddb0cf09..27f98451fde 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -823,7 +823,7 @@ function deleteReportComment(reportID, reportAction) { }; // If we are deleting the last visible message, let's find the previous visible one and update the lastMessageText in the LHN. - // Similarly, we are deleting the last read comment will want to update the lastReadSequenceNumber to use the previous visible message. + // Similarly, if we are deleting the last read comment we will want to update the lastReadSequenceNumber and maxSequenceNumber to use the previous visible message. const lastMessageText = ReportActionsUtils.getLastVisibleMessageText(reportID, optimisticReportActions); const lastReadSequenceNumber = ReportActionsUtils.getOptimisticLastReadSequenceNumberForDeletedAction( reportID, @@ -834,6 +834,7 @@ function deleteReportComment(reportID, reportAction) { const optimisticReport = { lastMessageText, lastReadSequenceNumber, + maxSequenceNumber: lastReadSequenceNumber, }; // If the API call fails we must show the original message again, so we revert the message content back to how it was diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 1e671e3bb5a..d15c905e400 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -19,6 +19,7 @@ import getPlaidOAuthReceivedRedirectURI from '../../libs/getPlaidOAuthReceivedRe import Text from '../../components/Text'; import {withNetwork} from '../../components/OnyxProvider'; import networkPropTypes from '../../components/networkPropTypes'; +import * as store from '../../libs/actions/ReimbursementAccount/store'; // Steps import BankAccountStep from './BankAccountStep'; @@ -163,9 +164,10 @@ class ReimbursementAccountPage extends React.Component { // We can specify a step to navigate to by using route params when the component mounts. // We want to use the same stepToOpen variable when the network state changes because we can be redirected to a different step when the account refreshes. const stepToOpen = this.getStepToOpenFromRouteParams(); - - // If we are trying to navigate to `/bank-account/new` and we already have a bank account then don't allow returning to `/new` - BankAccounts.fetchFreePlanVerifiedBankAccount(stepToOpen !== CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT ? stepToOpen : ''); + const reimbursementAccount = store.getReimbursementAccountInSetup(); + const subStep = reimbursementAccount.subStep || ''; + const localCurrentStep = reimbursementAccount.currentStep || ''; + BankAccounts.openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep); } continue() { diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index b50e82b774a..6bed29f4e24 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -150,7 +150,7 @@ class SearchPage extends Component { Navigation.navigate(ROUTES.getReportRoute(option.reportID)); }); } else { - Report.navigateToAndOpenReport(option.login); + Report.navigateToAndOpenReport([option.login]); } } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 27302710b66..6836540c642 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -173,7 +173,7 @@ class ReportActionItem extends Component { event.target.blur(); }} > - + {hovered => ( {this.props.shouldDisplayNewIndicator && ( diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index d226ac9ced9..42fe592c317 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -30,7 +30,7 @@ const ReportActionItemMessage = props => ( {_.map(_.compact(props.action.previousMessage || props.action.message), (fragment, index) => ( { > {_.map(personArray, (fragment, index) => ( ); } - /** - * This function overrides the CellRendererComponent (defaults to a plain View), giving each ReportActionItem a - * higher z-index than the one below it. This prevents issues where the ReportActionContextMenu overlapping between - * rows is hidden beneath other rows. - * - * @param {Object} index - The ReportAction item in the FlatList. - * @param {Object|Array} style – The default styles of the CellRendererComponent provided by the CellRenderer. - * @param {Object} props – All the other Props provided to the CellRendererComponent by default. - * @returns {React.Component} - */ - renderCell({item, style, ...props}) { - const cellStyle = [ - style, - {zIndex: item.action.sequenceNumber}, - ]; - // eslint-disable-next-line react/jsx-props-no-spreading - return ; - } - render() { // Native mobile does not render updates flatlist the changes even though component did update called. // To notify there something changes we can use extraData prop to flatlist diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e99a0ed70e0..722dbb840b7 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -79,7 +79,7 @@ class ReportActionsView extends React.Component { this.currentScrollOffset = 0; this.sortedReportActions = ReportActionsUtils.getSortedReportActions(props.reportActions); - this.mostRecentIOUReportSequenceNumber = ReportActionsUtils.getMostRecentIOUReportSequenceNumber(props.reportActions); + this.mostRecentIOUReportActionID = ReportActionsUtils.getMostRecentIOUReportActionID(props.reportActions); this.trackScroll = this.trackScroll.bind(this); this.toggleFloatingMessageCounter = this.toggleFloatingMessageCounter.bind(this); this.loadMoreChats = this.loadMoreChats.bind(this); @@ -142,7 +142,7 @@ class ReportActionsView extends React.Component { shouldComponentUpdate(nextProps, nextState) { if (!_.isEqual(nextProps.reportActions, this.props.reportActions)) { this.sortedReportActions = ReportActionsUtils.getSortedReportActions(nextProps.reportActions); - this.mostRecentIOUReportSequenceNumber = ReportActionsUtils.getMostRecentIOUReportSequenceNumber(nextProps.reportActions); + this.mostRecentIOUReportActionID = ReportActionsUtils.getMostRecentIOUReportActionID(nextProps.reportActions); return true; } @@ -367,7 +367,7 @@ class ReportActionsView extends React.Component { onScroll={this.trackScroll} onLayout={this.recordTimeToMeasureItemLayout} sortedReportActions={this.sortedReportActions} - mostRecentIOUReportSequenceNumber={this.mostRecentIOUReportSequenceNumber} + mostRecentIOUReportActionID={this.mostRecentIOUReportActionID} isLoadingMoreReportActions={this.props.report.isLoadingMoreReportActions} loadMoreChats={this.loadMoreChats} newMarkerSequenceNumber={this.state.newMarkerSequenceNumber} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index eec8c56f0dc..b7fc3ab58c8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -137,7 +137,7 @@ class SidebarLinks extends React.Component { /> - + { + App.setSidebarLoaded(); + this.isSidebarLoaded = true; + }} /> diff --git a/src/pages/iou/IOUTransactions.js b/src/pages/iou/IOUTransactions.js index af5718303dd..4b203e324c2 100644 --- a/src/pages/iou/IOUTransactions.js +++ b/src/pages/iou/IOUTransactions.js @@ -87,7 +87,7 @@ class IOUTransactions extends Component { chatReportID={this.props.chatReportID} iouReportID={this.props.iouReportID} action={reportAction} - key={reportAction.sequenceNumber} + key={reportAction.reportActionID} canBeRejected={canBeRejected} rejectButtonType={isCurrentUserTransactionCreator ? CONST.IOU.REPORT_ACTION_TYPE.CANCEL : CONST.IOU.REPORT_ACTION_TYPE.DECLINE} /> diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 9c0f18ad1a7..53566ed3d01 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -35,7 +35,7 @@ import * as Expensicons from '../../components/Icon/Expensicons'; const propTypes = { /** The personal details of the person who is logged in */ - personalDetails: personalDetailsPropType.isRequired, + personalDetails: personalDetailsPropType, /** URL Route params */ route: PropTypes.shape({ @@ -126,7 +126,8 @@ class WorkspaceMembersPage extends React.Component { */ toggleAllUsers() { this.setState({showTooltipForLogin: ''}); - const policyMemberList = _.keys(lodashGet(this.props, 'policyMemberList', {})); + let policyMemberList = lodashGet(this.props, 'policyMemberList', {}); + policyMemberList = _.filter(_.keys(policyMemberList), policyMember => policyMemberList[policyMember].pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const removableMembers = _.without(policyMemberList, this.props.session.email, this.props.policy.owner); this.setState(prevState => ({ selectedEmployees: removableMembers.length !== prevState.selectedEmployees.length @@ -288,10 +289,11 @@ class WorkspaceMembersPage extends React.Component { const removableMembers = []; let data = []; _.each(policyMemberList, (policyMember, email) => { + if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } if (email !== this.props.session.email && email !== this.props.policy.owner) { removableMembers.push(email); } - const details = this.props.personalDetails[email] || {displayName: email, login: email, avatar: Expensicons.FallbackAvatar}; + const details = lodashGet(this.props.personalDetails, email, {displayName: email, login: email, avatar: Expensicons.FallbackAvatar}); data.push({ ...policyMember, ...details, diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index 706ff3c5bc6..7c0783aed5c 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -22,6 +22,8 @@ import networkPropTypes from '../../components/networkPropTypes'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; const propTypes = { + shouldSkipVBBACall: PropTypes.bool, + /** Information about the network from Onyx */ network: networkPropTypes.isRequired, @@ -38,7 +40,7 @@ const propTypes = { }).isRequired, /** From Onyx */ - /** Bank account currently in setup */ + /** Bank account attached to free plan */ reimbursementAccount: reimbursementAccountPropTypes, /** User Data from Onyx */ @@ -71,6 +73,7 @@ const defaultProps = { footer: null, guidesCallTaskID: '', shouldUseScrollView: false, + shouldSkipVBBACall: false, }; class WorkspacePageWithSections extends React.Component { @@ -87,8 +90,11 @@ class WorkspacePageWithSections extends React.Component { } fetchData() { - const achState = lodashGet(this.props.reimbursementAccount, 'achData.state', ''); - BankAccounts.fetchFreePlanVerifiedBankAccount('', achState); + if (this.props.shouldSkipVBBACall) { + return; + } + + BankAccounts.openWorkspaceView(); } render() { diff --git a/src/pages/workspace/reimburse/WorkspaceReimbursePage.js b/src/pages/workspace/reimburse/WorkspaceReimbursePage.js index 3a208fcfa9e..35b3f8666e2 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimbursePage.js +++ b/src/pages/workspace/reimburse/WorkspaceReimbursePage.js @@ -27,9 +27,10 @@ const WorkspaceReimbursePage = props => ( headerText={props.translate('workspace.common.reimburse')} route={props.route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_REIMBURSE} + shouldSkipVBBACall > - {hasVBA => ( - + {() => ( + )} ); diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseView.js b/src/pages/workspace/reimburse/WorkspaceReimburseView.js index f077583faa0..5aed733351d 100644 --- a/src/pages/workspace/reimburse/WorkspaceReimburseView.js +++ b/src/pages/workspace/reimburse/WorkspaceReimburseView.js @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import _ from 'underscore'; @@ -17,6 +18,9 @@ import compose from '../../../libs/compose'; import * as Policy from '../../../libs/actions/Policy'; import CONST from '../../../CONST'; import Button from '../../../components/Button'; +import ONYXKEYS from '../../../ONYXKEYS'; +import BankAccount from '../../../libs/models/BankAccount'; +import reimbursementAccountPropTypes from '../../ReimbursementAccount/reimbursementAccountPropTypes'; import getPermittedDecimalSeparator from '../../../libs/getPermittedDecimalSeparator'; import {withNetwork} from '../../../components/OnyxProvider'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; @@ -49,12 +53,20 @@ const propTypes = { lastModified: PropTypes.number, }).isRequired, + /** From Onyx */ + /** Bank account attached to free plan */ + reimbursementAccount: reimbursementAccountPropTypes, + /** Information about the network */ network: networkPropTypes.isRequired, ...withLocalizePropTypes, }; +const defaultProps = { + reimbursementAccount: {}, +}; + class WorkspaceReimburseView extends React.Component { constructor(props) { super(props); @@ -199,6 +211,8 @@ class WorkspaceReimburseView extends React.Component { } render() { + const achState = lodashGet(this.props.reimbursementAccount, 'achData.state', ''); + const hasVBA = achState === BankAccount.STATE.OPEN; return ( <>
- {this.props.hasVBA ? ( + {hasVBA ? (
({ + picker: (disabled = false) => ({ iconContainer: { - top: 16, - right: 11, + top: 15, + right: 10, zIndex: -1, }, inputWeb: { appearance: 'none', cursor: disabled ? 'not-allowed' : 'pointer', ...picker, - ...(focused && {borderColor: themeColors.borderFocus}), - ...(error && {borderColor: themeColors.badgeDangerBG}), }, inputNative: { ...picker, - ...(focused && {borderColor: themeColors.borderFocus}), - ...(error && {borderColor: themeColors.badgeDangerBG}), }, }), diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index cbf04d3adb5..f2e2bba99e3 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -396,7 +396,7 @@ describe('actions/Report', () => { return waitForPromisesToResolve(); }) .then(() => { - expect(ReportUtils.isUnread(report)).toBe(true); + expect(ReportUtils.isUnread(report)).toBe(false); expect(report.lastMessageText).toBe('Current User Comment 2'); }); }); diff --git a/web/index.html b/web/index.html index 5d133721db8..ca117f328d6 100644 --- a/web/index.html +++ b/web/index.html @@ -58,5 +58,8 @@
+
+ +
diff --git a/web/splash/splash.css b/web/splash/splash.css new file mode 100644 index 00000000000..6f98c8434f2 --- /dev/null +++ b/web/splash/splash.css @@ -0,0 +1,23 @@ +@media screen and (min-width: 480px) { + .splash-logo > svg { + width: 104px; + height: 104px; + } +} +@media screen and (max-width: 479px) { + .splash-logo > svg { + width: 52px; + height: 52px; + } +} + +#splash { + position: absolute; + top: 0; left: 0; + right: 0; background-color: white; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/web/splash/splash.js b/web/splash/splash.js new file mode 100644 index 00000000000..f52b90d6850 --- /dev/null +++ b/web/splash/splash.js @@ -0,0 +1,20 @@ +import './splash.css'; +import newExpensifyLogo from 'logo?raw'; + +const minMilisecondsToWait = 1.5 * 1000; +let passedMiliseconds = 0; +let rootMounted = false; +const splash = document.getElementById('splash'); +const splashLogo = document.querySelector('.splash-logo'); +const root = document.getElementById('root'); + +splashLogo.innerHTML = newExpensifyLogo; + +const intervalId = setInterval(() => { + passedMiliseconds += 250; + rootMounted = root.children.length > 0; + if (passedMiliseconds >= minMilisecondsToWait && rootMounted) { + splash.parentNode.removeChild(splash); + clearInterval(intervalId); + } +}, 250);