diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index f52453dcdf40..92fde8a4b9c2 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -71,6 +71,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + - uses: Expensify/App/.github/actions/composite/setupNode@main - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 @@ -121,6 +127,12 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + - uses: Expensify/App/.github/actions/composite/setupNode@main - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 @@ -178,6 +190,12 @@ jobs: ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} fetch-depth: 0 + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Decrypt Developer ID Certificate @@ -192,7 +210,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Build desktop app for testing - run: npm run desktop-build-internal -- --publish always + run: npm run desktop-build-adhoc -- --publish always env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} @@ -214,6 +232,12 @@ jobs: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Configure AWS Credentials @@ -223,7 +247,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: Build web for testing - run: npm run build-staging + run: npm run build-adhoc - name: Build docs run: npm run storybook-build diff --git a/.github/workflows/validateGithubActions.yml b/.github/workflows/validateGithubActions.yml index b583ba31f2bc..d731158e646b 100644 --- a/.github/workflows/validateGithubActions.yml +++ b/.github/workflows/validateGithubActions.yml @@ -4,6 +4,8 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] + paths: + - .github/** jobs: verify: diff --git a/.github/workflows/verifyPodfile.yml b/.github/workflows/verifyPodfile.yml index 490d88463b45..892b7c03c2de 100644 --- a/.github/workflows/verifyPodfile.yml +++ b/.github/workflows/verifyPodfile.yml @@ -4,6 +4,10 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] + paths: + - ios/** + - "package.json" + - "package-lock.json" jobs: verify: diff --git a/__mocks__/react-native-dev-menu.js b/__mocks__/react-native-dev-menu.js new file mode 100644 index 000000000000..49cb4c61a209 --- /dev/null +++ b/__mocks__/react-native-dev-menu.js @@ -0,0 +1,3 @@ +export default { + addItem: jest.fn(), +}; diff --git a/android/app/build.gradle b/android/app/build.gradle index 113ba9865945..5591fffcb5d4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001030205 - versionName "1.3.2-5" + versionCode 1001030300 + versionName "1.3.3-0" } splits { diff --git a/android/settings.gradle b/android/settings.gradle index db7d62fd8ef5..b86bfc40ebde 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -11,6 +11,8 @@ include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') include ':react-native-plaid-link-sdk' project(':react-native-plaid-link-sdk').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-plaid-link-sdk/android') +include ':react-native-dev-menu' +project(':react-native-dev-menu').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-dev-menu/android') apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/react-native-gradle-plugin') diff --git a/assets/images/new-expensify-adhoc.svg b/assets/images/new-expensify-adhoc.svg new file mode 100644 index 000000000000..db2f420c4e0e --- /dev/null +++ b/assets/images/new-expensify-adhoc.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js index ba018e04b8bc..46433e4b1574 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.js @@ -6,13 +6,13 @@ const pullRequestNumber = process.env.PULL_REQUEST_NUMBER; const s3Bucket = { production: 'expensify-cash', staging: 'staging-expensify-cash', - internal: 'ad-hoc-expensify-cash', + adhoc: 'ad-hoc-expensify-cash', }; const s3Path = { production: '/', staging: '/', - internal: process.env.PULL_REQUEST_NUMBER + adhoc: process.env.PULL_REQUEST_NUMBER ? `/desktop/${pullRequestNumber}/` : '/', }; @@ -20,10 +20,10 @@ const s3Path = { const macIcon = { production: './desktop/icon.png', staging: './desktop/icon-stg.png', - internal: './desktop/icon-stg.png', + adhoc: './desktop/icon-adhoc.png', }; -const isCorrectElectronEnv = ['production', 'staging', 'internal'].includes( +const isCorrectElectronEnv = ['production', 'staging', 'adhoc'].includes( process.env.ELECTRON_ENV, ); diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 50bc7f0f9a81..59b34693b267 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -29,6 +29,7 @@ const envToLogoSuffixMap = { production: '', staging: '-stg', dev: '-dev', + adhoc: '-adhoc', }; function mapEnvToLogoSuffix(envFile) { @@ -120,7 +121,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ // React Native JavaScript environment requires the global __DEV__ variable to be accessible. // react-native-render-html uses variable to log exclusively during development. // See https://reactnative.dev/docs/javascript-environment - __DEV__: /staging|prod/.test(envFile) === false, + __DEV__: /staging|prod|adhoc/.test(envFile) === false, }), // This allows us to interactively inspect JS bundle contents diff --git a/desktop/icon-adhoc.png b/desktop/icon-adhoc.png new file mode 100644 index 000000000000..5812ad6c5404 Binary files /dev/null and b/desktop/icon-adhoc.png differ diff --git a/desktop/main.js b/desktop/main.js index 07c51a0bf5f9..662b2a136146 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -241,7 +241,7 @@ const mainWindow = (() => { if (__DEV__) { console.debug('CONFIG: ', CONFIG); app.dock.setIcon(`${__dirname}/../icon-dev.png`); - app.setName('New Expensify'); + app.setName('New Expensify Dev'); } app.on('will-finish-launching', () => { diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 577784d3810a..49f15a3b12c0 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -39,7 +39,7 @@ platform :android do desc "Build app for testing" lane :build_internal do - ENV["ENVFILE"]=".env.staging" + ENV["ENVFILE"]=".env.adhoc" gradle( project_dir: './android', @@ -118,7 +118,7 @@ platform :ios do desc "Build app for testing" lane :build_internal do require 'securerandom' - ENV["ENVFILE"]=".env.staging" + ENV["ENVFILE"]=".env.adhoc" keychain_password = SecureRandom.uuid diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 311929aa0fe5..3322f8333a92 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.2 + 1.3.3 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.3.2.5 + 1.3.3.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7162118cc778..5edfb0d38c4e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.2 + 1.3.3 CFBundleSignature ???? CFBundleVersion - 1.3.2.5 + 1.3.3.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 28de3a7fa527..16c85d147bb1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -635,6 +635,10 @@ PODS: - React-Core - RNDeviceInfo (10.3.0): - React-Core + - RNDevMenu (4.1.1): + - React-Core + - React-Core/DevSupport + - React-RCTNetwork - RNFastImage (8.6.3): - React-Core - SDWebImage (~> 5.11.1) @@ -793,6 +797,7 @@ DEPENDENCIES: - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - RNDevMenu (from `../node_modules/react-native-dev-menu`) - RNFastImage (from `../node_modules/react-native-fast-image`) - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" @@ -972,6 +977,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/datetimepicker" RNDeviceInfo: :path: "../node_modules/react-native-device-info" + RNDevMenu: + :path: "../node_modules/react-native-dev-menu" RNFastImage: :path: "../node_modules/react-native-fast-image" RNFBAnalytics: @@ -1095,6 +1102,7 @@ SPEC CHECKSUMS: RNCPicker: 0b65be85fe7954fbb2062ef079e3d1cde252d888 RNDateTimePicker: 7658208086d86d09e1627b5c34ba0cf237c60140 RNDeviceInfo: 4701f0bf2a06b34654745053db0ce4cb0c53ada7 + RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e @@ -1115,4 +1123,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: cd132e281e9e3d7e6ec2c99c08e6ec32b37886f8 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.0 diff --git a/package-lock.json b/package-lock.json index e09a38fb48de..38837b034c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.2-5", + "version": "1.3.3-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.2-5", + "version": "1.3.3-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -62,6 +62,7 @@ "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", + "react-native-dev-menu": "^4.1.1", "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", @@ -137,7 +138,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "22.3.5", + "electron": "^22.3.6", "electron-builder": "23.5.0", "electron-notarize": "^1.2.1", "eslint": "^7.6.0", @@ -21186,12 +21187,11 @@ } }, "node_modules/electron": { - "version": "22.3.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.5.tgz", - "integrity": "sha512-CTdnoTbO3sDiMv47TX3ZO640Ca57v1qpiqGChFF8oZbtfHuQjTPPaE4hsoynf22wwnBiyJNL41DpB/pfp9USnA==", + "version": "22.3.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.6.tgz", + "integrity": "sha512-/1/DivFHH5AWa/uOuqpkeg12/jjicjkBU8kYv70oeqRFwXzoyuJhgwlzER4jZXnbGjF5Nxz9900oXq/QzAViAw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^16.11.26", @@ -34560,6 +34560,14 @@ } } }, + "node_modules/react-native-dev-menu": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-native-dev-menu/-/react-native-dev-menu-4.1.1.tgz", + "integrity": "sha512-jdYjoTpFHvGXW12enaTnrgOoEgVF5JVqv4hcO8K0KV66Cvk8YLwD3XHsEiqMat+4C1osa+IG5Yt3qAiMOLBQxQ==", + "peerDependencies": { + "react-native": ">=0.61.0" + } + }, "node_modules/react-native-device-info": { "version": "10.3.0", "license": "MIT", @@ -55427,9 +55435,9 @@ } }, "electron": { - "version": "22.3.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.5.tgz", - "integrity": "sha512-CTdnoTbO3sDiMv47TX3ZO640Ca57v1qpiqGChFF8oZbtfHuQjTPPaE4hsoynf22wwnBiyJNL41DpB/pfp9USnA==", + "version": "22.3.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.6.tgz", + "integrity": "sha512-/1/DivFHH5AWa/uOuqpkeg12/jjicjkBU8kYv70oeqRFwXzoyuJhgwlzER4jZXnbGjF5Nxz9900oXq/QzAViAw==", "dev": true, "requires": { "@electron/get": "^2.0.0", @@ -64277,6 +64285,12 @@ "version": "1.4.6", "requires": {} }, + "react-native-dev-menu": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-native-dev-menu/-/react-native-dev-menu-4.1.1.tgz", + "integrity": "sha512-jdYjoTpFHvGXW12enaTnrgOoEgVF5JVqv4hcO8K0KV66Cvk8YLwD3XHsEiqMat+4C1osa+IG5Yt3qAiMOLBQxQ==", + "requires": {} + }, "react-native-device-info": { "version": "10.3.0", "requires": {} diff --git a/package.json b/package.json index a3b4a11feab6..8641f36a678a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.2-5", + "version": "1.3.3-0", "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.", @@ -19,10 +19,11 @@ "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", + "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", "desktop": "scripts/set-pusher-suffix.sh && node desktop/start.js", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", - "desktop-build-internal": "scripts/build-desktop.sh internal", + "desktop-build-adhoc": "scripts/build-desktop.sh adhoc", "ios-build": "fastlane ios build", "android-build": "fastlane android build", "android-build-e2e": "bundle exec fastlane android build_e2e", @@ -93,6 +94,7 @@ "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", + "react-native-dev-menu": "^4.1.1", "react-native-device-info": "^10.3.0", "react-native-document-picker": "^8.0.0", "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", @@ -168,7 +170,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "22.3.5", + "electron": "22.3.6", "electron-builder": "23.5.0", "electron-notarize": "^1.2.1", "eslint": "^7.6.0", diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index ce8737ee1b18..c67c37a527a2 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -5,8 +5,8 @@ export ELECTRON_ENV=${1:-development} if [[ "$ELECTRON_ENV" == "staging" ]]; then ENV_FILE=".env.staging" -elif [[ "$ELECTRON_ENV" == "internal" ]]; then - ENV_FILE=".env.staging" +elif [[ "$ELECTRON_ENV" == "adhoc" ]]; then + ENV_FILE=".env.adhoc" elif [[ "$ELECTRON_ENV" == "production" ]]; then ENV_FILE=".env.production" else diff --git a/src/CONST.js b/src/CONST.js index 3a2524b56e5e..8974bf78eb11 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -10,6 +10,7 @@ const PLATFORM_OS_MACOS = 'Mac OS'; const ANDROID_PACKAGE_NAME = 'com.expensify.chat'; const USA_COUNTRY_NAME = 'United States'; const CURRENT_YEAR = new Date().getFullYear(); +const PULL_REQUEST_NUMBER = lodashGet(Config, 'PULL_REQUEST_NUMBER', ''); const CONST = { ANDROID_PACKAGE_NAME, @@ -55,6 +56,8 @@ const CONST = { RESERVED_FIRST_NAMES: ['Expensify', 'Concierge'], }, + PULL_REQUEST_NUMBER, + CALENDAR_PICKER: { // Numbers were arbitrarily picked. MIN_YEAR: CURRENT_YEAR - 100, @@ -468,6 +471,7 @@ const CONST = { CONFIRM: 'confirm', CENTERED: 'centered', CENTERED_UNSWIPEABLE: 'centered_unswipeable', + CENTERED_SMALL: 'centered_small', BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', @@ -493,6 +497,7 @@ const CONST = { WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250, + TEST_TOOLS_MODAL_THROTTLE_TIME: 800, TOOLTIP_SENSE: 1000, TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 500, @@ -692,6 +697,7 @@ const CONST = { DEV: 'development', STAGING: 'staging', PRODUCTION: 'production', + ADHOC: 'adhoc', }, // Used to delay the initial fetching of reportActions when the app first inits or reconnects (e.g. returning @@ -2200,9 +2206,17 @@ const CONST = { SAMPLE_INPUT: '123456.789', EXPECTED_OUTPUT: 'FCFA 123,457', }, + PATHS_TO_TREAT_AS_EXTERNAL: [ 'NewExpensify.dmg', ], + + // Test tool menu parameters + TEST_TOOL: { + // Number of concurrent taps to open then the Test modal menu + NUMBER_OF_TAPS: 4, + }, + PAYPAL_SUPPORTED_CURRENCIES: [ 'AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF', 'ILS', 'JPY', 'MYR', 'MXN', 'TWD', 'NZD', 'NOK', 'PHP', diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index b0526ae4b61a..c9ed8ef43dab 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -169,6 +169,9 @@ export default { // Is Keyboard shortcuts modal open? IS_SHORTCUTS_MODAL_OPEN: 'isShortcutsModalOpen', + // Is the test tools modal open? + IS_TEST_TOOLS_MODAL_OPEN: 'isTestToolsModalOpen', + // Stores information about active wallet transfer amount, selectedAccountID, status, etc WALLET_TRANSFER: 'walletTransfer', diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 93d6e3d82a73..19549cdc8554 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -15,6 +15,7 @@ import {policyPropTypes} from '../pages/workspace/withPolicy'; import walletTermsPropTypes from '../pages/EnablePayments/walletTermsPropTypes'; import * as PolicyUtils from '../libs/PolicyUtils'; import * as PaymentMethods from '../libs/actions/PaymentMethods'; +import * as ReimbursementAccountProps from '../pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as ReportUtils from '../libs/ReportUtils'; import * as UserUtils from '../libs/UserUtils'; import themeColors from '../styles/themes/default'; @@ -43,6 +44,9 @@ const propTypes = { /** The user's wallet (coming from Onyx) */ userWallet: userWalletPropTypes, + /** Bank account attached to free plan */ + reimbursementAccount: ReimbursementAccountProps.reimbursementAccountPropTypes, + /** Information about the user accepting the terms for payments */ walletTerms: walletTermsPropTypes, @@ -58,6 +62,7 @@ const propTypes = { const defaultProps = { tooltipText: '', + reimbursementAccount: {}, policiesMemberList: {}, policies: {}, bankAccountList: {}, @@ -82,6 +87,7 @@ const AvatarWithIndicator = (props) => { () => _.some(cleanPolicies, PolicyUtils.hasPolicyError), () => _.some(cleanPolicies, PolicyUtils.hasCustomUnitsError), () => _.some(cleanPolicyMembers, PolicyUtils.hasPolicyMemberError), + () => !_.isEmpty(props.reimbursementAccount.errors), () => UserUtils.hasLoginListError(props.loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) @@ -126,6 +132,9 @@ export default withOnyx({ bankAccountList: { key: ONYXKEYS.BANK_ACCOUNT_LIST, }, + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, cardList: { key: ONYXKEYS.CARD_LIST, }, diff --git a/src/components/Badge.js b/src/components/Badge.js index d83c196e31e8..b1eafb227bc4 100644 --- a/src/components/Badge.js +++ b/src/components/Badge.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import styles from '../styles/styles'; import * as StyleUtils from '../styles/StyleUtils'; import Text from './Text'; +import CONST from '../CONST'; const propTypes = { /** Is Success type */ @@ -18,6 +19,9 @@ const propTypes = { /** Text to display in the Badge */ text: PropTypes.string.isRequired, + /** Text to display in the Badge */ + environment: PropTypes.string, + /** Styles for Badge */ // eslint-disable-next-line react/forbid-prop-types badgeStyles: PropTypes.arrayOf(PropTypes.object), @@ -32,6 +36,7 @@ const defaultProps = { pressable: false, badgeStyles: [], onPress: undefined, + environment: CONST.ENVIRONMENT.DEV, }; const Badge = (props) => { @@ -40,7 +45,7 @@ const Badge = (props) => { const wrapperStyles = ({pressed}) => ([ styles.badge, styles.ml2, - StyleUtils.getBadgeColorStyle(props.success, props.error, pressed), + StyleUtils.getBadgeColorStyle(props.success, props.error, pressed, props.environment === CONST.ENVIRONMENT.ADHOC), ...props.badgeStyles, ]); diff --git a/src/components/BlockingViews/BlockingView.js b/src/components/BlockingViews/BlockingView.js index 1d0188ac305c..21a3cc92ff5e 100644 --- a/src/components/BlockingViews/BlockingView.js +++ b/src/components/BlockingViews/BlockingView.js @@ -27,12 +27,20 @@ const propTypes = { /** Whether we should show a go back home link */ shouldShowBackHomeLink: PropTypes.bool, + + /** The custom icon width */ + iconWidth: PropTypes.number, + + /** The custom icon height */ + iconHeight: PropTypes.number, }; const defaultProps = { iconColor: themeColors.offline, shouldShowBackHomeLink: false, link: 'notFound.goBackHome', + iconWidth: variables.iconSizeSuperLarge, + iconHeight: variables.iconSizeSuperLarge, }; const BlockingView = props => ( @@ -42,8 +50,8 @@ const BlockingView = props => ( {props.title} {props.subtitle} diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.js index 5f0a5b5cebcc..119d9f469658 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.js @@ -3,10 +3,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import BlockingView from './BlockingView'; -import * as Expensicons from '../Icon/Expensicons'; +import * as Illustrations from '../Icon/Illustrations'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import HeaderWithCloseButton from '../HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; +import variables from '../../styles/variables'; import styles from '../../styles/styles'; const propTypes = { @@ -66,7 +67,9 @@ const FullPageNotFoundView = (props) => { /> {}; + +CustomDevMenu.displayName = 'CustomDevMenu'; + +export default CustomDevMenu; diff --git a/src/components/CustomDevMenu/index.native.js b/src/components/CustomDevMenu/index.native.js new file mode 100644 index 000000000000..745cd697b460 --- /dev/null +++ b/src/components/CustomDevMenu/index.native.js @@ -0,0 +1,15 @@ +import {useEffect} from 'react'; +import DevMenu from 'react-native-dev-menu'; +import toggleTestToolsModal from '../../libs/actions/TestTool'; + +const CustomDevMenu = () => { + useEffect(() => { + DevMenu.addItem('Open Test Preferences', toggleTestToolsModal); + }, []); + + return null; +}; + +CustomDevMenu.displayName = 'CustomDevMenu'; + +export default CustomDevMenu; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 112ad8ffa7cc..e3ebdbb3cd47 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -168,9 +168,11 @@ class EmojiPickerMenu extends Component { return; } - // Return if the key is related to any tab cycling event so that the default logic - // can be executed. - if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Shift' || keyBoardEvent.key === 'Enter') { + // Enable keyboard movement if tab or enter is pressed or if shift is pressed while the input + // is not focused, so that the navigation and tab cycling can be done using the keyboard without + // interfering with the input behaviour. + if (keyBoardEvent.key === 'Tab' || keyBoardEvent.key === 'Enter' + || (keyBoardEvent.key === 'Shift' && this.searchInput && !this.searchInput.isFocused())) { this.setState({isUsingKeyboardMovement: true}); return; } diff --git a/src/components/EnvironmentBadge.js b/src/components/EnvironmentBadge.js index 4365bfab1a11..9e85b3ef2d97 100644 --- a/src/components/EnvironmentBadge.js +++ b/src/components/EnvironmentBadge.js @@ -3,11 +3,14 @@ import CONST from '../CONST'; import withEnvironment, {environmentPropTypes} from './withEnvironment'; import Badge from './Badge'; import styles from '../styles/styles'; +import * as Environment from '../libs/Environment/Environment'; +import pkg from '../../package.json'; const ENVIRONMENT_SHORT_FORM = { [CONST.ENVIRONMENT.DEV]: 'DEV', [CONST.ENVIRONMENT.STAGING]: 'STG', [CONST.ENVIRONMENT.PRODUCTION]: 'PROD', + [CONST.ENVIRONMENT.ADHOC]: 'ADHOC', }; const EnvironmentBadge = (props) => { @@ -16,12 +19,15 @@ const EnvironmentBadge = (props) => { return null; } + const text = Environment.isInternalTestBuild() ? `v${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}` : ENVIRONMENT_SHORT_FORM[props.environment]; + return ( ); }; diff --git a/src/components/ExpensifyCashLogo.js b/src/components/ExpensifyCashLogo.js index 06a687460091..0c709581dacd 100644 --- a/src/components/ExpensifyCashLogo.js +++ b/src/components/ExpensifyCashLogo.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ProductionLogo from '../../assets/images/new-expensify.svg'; import DevLogo from '../../assets/images/new-expensify-dev.svg'; import StagingLogo from '../../assets/images/new-expensify-stg.svg'; +import AdhocLogo from '../../assets/images/new-expensify-adhoc.svg'; import CONST from '../CONST'; import withEnvironment, {environmentPropTypes} from './withEnvironment'; @@ -20,6 +21,7 @@ const logoComponents = { [CONST.ENVIRONMENT.DEV]: DevLogo, [CONST.ENVIRONMENT.STAGING]: StagingLogo, [CONST.ENVIRONMENT.PRODUCTION]: ProductionLogo, + [CONST.ENVIRONMENT.ADHOC]: AdhocLogo, }; const ExpensifyCashLogo = (props) => { diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js index 1e314654bba3..7d44127f1c91 100644 --- a/src/components/Modal/BaseModal.js +++ b/src/components/Modal/BaseModal.js @@ -155,6 +155,7 @@ class BaseModal extends PureComponent { modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, modalContainerStylePaddingTop: modalContainerStyle.paddingTop, modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets, }); return ( diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.js index 12eea80fa87c..389ff903ebf1 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.js @@ -104,7 +104,17 @@ class Onfido extends React.Component { onModalRequestClose: () => { Log.hmmm('Onfido user closed the modal'); }, - language: this.props.preferredLocale, + language: { + locale: this.props.preferredLocale, + + // Provide a custom phrase for the back button so that the first letter is capitalized, + // and translate the phrase while we're at it. See the issue and documentation for more context. + // https://github.com/Expensify/App/issues/17244 + // https://documentation.onfido.com/sdk/web/#custom-languages + phrases: { + 'generic.back': this.props.translate('common.back'), + }, + }, }); window.addEventListener('userAnalyticsEvent', (event) => { diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index e47e101c3ea7..0975d4db8630 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -1,4 +1,4 @@ -import {Keyboard, View} from 'react-native'; +import {Keyboard, View, PanResponder} from 'react-native'; import React from 'react'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; @@ -12,17 +12,26 @@ import HeaderGap from '../HeaderGap'; import OfflineIndicator from '../OfflineIndicator'; import compose from '../../libs/compose'; import withNavigation from '../withNavigation'; -import withWindowDimensions from '../withWindowDimensions'; import ONYXKEYS from '../../ONYXKEYS'; import {withNetwork} from '../OnyxProvider'; import {propTypes, defaultProps} from './propTypes'; import SafeAreaConsumer from '../SafeAreaConsumer'; +import TestToolsModal from '../TestToolsModal'; import withKeyboardState from '../withKeyboardState'; +import withWindowDimensions from '../withWindowDimensions'; +import withEnvironment from '../withEnvironment'; +import toggleTestToolsModal from '../../libs/actions/TestTool'; +import CustomDevMenu from '../CustomDevMenu'; class ScreenWrapper extends React.Component { constructor(props) { super(props); + this.panResponder = PanResponder.create({ + onStartShouldSetPanResponderCapture: (e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS, + onPanResponderRelease: toggleTestToolsModal, + }); + this.state = { didScreenTransitionEnd: false, }; @@ -108,9 +117,13 @@ class ScreenWrapper extends React.Component { styles.flex1, paddingStyle, ]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...(this.props.environment === CONST.ENVIRONMENT.DEV ? this.panResponder.panHandlers : {})} > + {(this.props.environment === CONST.ENVIRONMENT.DEV) && } + {(this.props.environment === CONST.ENVIRONMENT.DEV) && } {// If props.children is a function, call it to provide the insets to the children. _.isFunction(this.props.children) ? this.props.children({ @@ -137,6 +150,7 @@ ScreenWrapper.defaultProps = defaultProps; export default compose( withNavigation, + withEnvironment, withWindowDimensions, withKeyboardState, withOnyx({ diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js index 1328f2929d18..1eabcaa010ce 100644 --- a/src/components/ScreenWrapper/propTypes.js +++ b/src/components/ScreenWrapper/propTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import {windowDimensionsPropTypes} from '../withWindowDimensions'; +import {environmentPropTypes} from '../withEnvironment'; const propTypes = { /** Array of additional styles to add */ @@ -17,7 +18,7 @@ const propTypes = { /** Whether to include padding top */ includePaddingTop: PropTypes.bool, - // Called when navigated Screen's transition is finished. It does not fire when user exit the page. + /** Called when navigated Screen's transition is finished. It does not fire when user exit the page. */ onEntryTransitionEnd: PropTypes.func, /** The behavior to pass to the KeyboardAvoidingView, requires some trial and error depending on the layout/devices used. @@ -34,6 +35,8 @@ const propTypes = { shouldDismissKeyboardBeforeClose: PropTypes.bool, ...windowDimensionsPropTypes, + + ...environmentPropTypes, }; const defaultProps = { diff --git a/src/components/TestToolMenu.js b/src/components/TestToolMenu.js index eaae9aec9846..7a056ac48565 100644 --- a/src/components/TestToolMenu.js +++ b/src/components/TestToolMenu.js @@ -38,7 +38,7 @@ const defaultProps = { const TestToolMenu = props => ( <> - + Test Preferences diff --git a/src/components/TestToolRow.js b/src/components/TestToolRow.js index 5364f44537b0..deefaaf3adc0 100644 --- a/src/components/TestToolRow.js +++ b/src/components/TestToolRow.js @@ -13,7 +13,7 @@ const propTypes = { }; const TestToolRow = props => ( - + {props.title} diff --git a/src/components/TestToolsModal.js b/src/components/TestToolsModal.js new file mode 100644 index 000000000000..022fbb1f1f11 --- /dev/null +++ b/src/components/TestToolsModal.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import {View} from 'react-native'; +import ONYXKEYS from '../ONYXKEYS'; +import Modal from './Modal'; +import CONST from '../CONST'; +import toggleTestToolsModal from '../libs/actions/TestTool'; +import TestToolMenu from './TestToolMenu'; +import styles from '../styles/styles'; + +const propTypes = { + /** Details about modal */ + modal: PropTypes.shape({ + /** Indicates when an Alert modal is about to be visible */ + willAlertModalBecomeVisible: PropTypes.bool, + }), + + /** Whether the test tools modal is open */ + isTestToolsModalOpen: PropTypes.bool, +}; + +const defaultProps = { + modal: {}, + isTestToolsModalOpen: false, +}; + +const TestToolsModal = props => ( + + + + + +); + +TestToolsModal.propTypes = propTypes; +TestToolsModal.defaultProps = defaultProps; +TestToolsModal.displayName = 'TestToolsModal'; + +export default withOnyx({ + modal: { + key: ONYXKEYS.MODAL, + }, + isTestToolsModalOpen: { + key: ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN, + }, +})(TestToolsModal); diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index bed1129abbe9..6fa78a513449 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -277,7 +277,11 @@ class BaseTextInput extends Component { )} { - if (typeof this.props.innerRef === 'function') { this.props.innerRef(ref); } + if (typeof this.props.innerRef === 'function') { + this.props.innerRef(ref); + } else if (this.props.innerRef && _.has(this.props.innerRef, 'current')) { + this.props.innerRef.current = ref; + } this.input = ref; }} // eslint-disable-next-line diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index dfb90b61af98..30169d995528 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -47,7 +47,11 @@ const propTypes = { hideFocusedState: PropTypes.bool, /** Forward the inner ref */ - innerRef: PropTypes.func, + innerRef: PropTypes.oneOfType([ + PropTypes.func, + // eslint-disable-next-line react/forbid-prop-types + PropTypes.shape({current: PropTypes.any}), + ]), /** Maximum characters allowed */ maxLength: PropTypes.number, diff --git a/src/languages/en.js b/src/languages/en.js index a2527dddbac9..c479e3412ae7 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -121,6 +121,7 @@ export default { message: 'Message ', leaveRoom: 'Leave room', you: 'You', + youAfterPreposition: 'you', your: 'your', conciergeHelp: 'Please reach out to Concierge for help.', maxParticipantsReached: ({count}) => `You've selected the maximum number (${count}) of participants.`, @@ -251,6 +252,7 @@ export default { editComment: 'Edit comment', deleteComment: 'Delete comment', deleteConfirmation: 'Are you sure you want to delete this comment?', + onlyVisible: 'Only visible to', }, emojiReactions: { addReactionTooltip: 'Add reaction', diff --git a/src/languages/es.js b/src/languages/es.js index f5d713b5ce5a..4a9873efb6de 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -120,6 +120,7 @@ export default { message: 'Chatear con ', leaveRoom: 'Salir de la sala de chat', you: 'Tú', + youAfterPreposition: 'ti', your: 'tu', conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.', maxParticipantsReached: ({count}) => `Has seleccionado el número máximo (${count}) de participantes.`, @@ -250,6 +251,7 @@ export default { editComment: 'Editar comentario', deleteComment: 'Eliminar comentario', deleteConfirmation: '¿Estás seguro de que quieres eliminar este comentario?', + onlyVisible: 'Visible sólo para', }, emojiReactions: { addReactionTooltip: 'Añadir una reacción', diff --git a/src/libs/ApiUtils.js b/src/libs/ApiUtils.js index 08533c83b076..7e3e5b9cd5b9 100644 --- a/src/libs/ApiUtils.js +++ b/src/libs/ApiUtils.js @@ -24,7 +24,7 @@ Environment.getEnvironment() return; } - const defaultToggleState = ENV_NAME === CONST.ENVIRONMENT.STAGING; + const defaultToggleState = ENV_NAME === CONST.ENVIRONMENT.STAGING || ENV_NAME === CONST.ENVIRONMENT.ADHOC; shouldUseStagingServer = lodashGet(val, 'shouldUseStagingServer', defaultToggleState); }, }); diff --git a/src/libs/Environment/Environment.js b/src/libs/Environment/Environment.js index bdba9f20d12d..7bce69833d2e 100644 --- a/src/libs/Environment/Environment.js +++ b/src/libs/Environment/Environment.js @@ -8,12 +8,14 @@ const ENVIRONMENT_URLS = { [CONST.ENVIRONMENT.DEV]: CONST.DEV_NEW_EXPENSIFY_URL + CONFIG.DEV_PORT, [CONST.ENVIRONMENT.STAGING]: CONST.STAGING_NEW_EXPENSIFY_URL, [CONST.ENVIRONMENT.PRODUCTION]: CONST.NEW_EXPENSIFY_URL, + [CONST.ENVIRONMENT.ADHOC]: CONST.STAGING_NEW_EXPENSIFY_URL, }; const OLDDOT_ENVIRONMENT_URLS = { [CONST.ENVIRONMENT.DEV]: CONST.INTERNAL_DEV_EXPENSIFY_URL, [CONST.ENVIRONMENT.STAGING]: CONST.STAGING_EXPENSIFY_URL, [CONST.ENVIRONMENT.PRODUCTION]: CONST.EXPENSIFY_URL, + [CONST.ENVIRONMENT.ADHOC]: CONST.STAGING_EXPENSIFY_URL, }; /** @@ -25,6 +27,15 @@ function isDevelopment() { return lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.DEV; } +/** + * Are we running an internal test build? + * + * @return {boolean} + */ +function isInternalTestBuild() { + return lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.ADHOC && lodashGet(Config, 'PULL_REQUEST_NUMBER', ''); +} + /** * Get the URL based on the environment we are in * @@ -49,6 +60,7 @@ function getOldDotEnvironmentURL() { export { getEnvironment, + isInternalTestBuild, isDevelopment, getEnvironmentURL, getOldDotEnvironmentURL, diff --git a/src/libs/Environment/getEnvironment/index.native.js b/src/libs/Environment/getEnvironment/index.native.js index 89547e0ee54d..5b2d065933b1 100644 --- a/src/libs/Environment/getEnvironment/index.native.js +++ b/src/libs/Environment/getEnvironment/index.native.js @@ -22,7 +22,12 @@ function getEnvironment() { return resolve(environment); } - // If we haven't set the environment yet and we aren't on dev, check to see if this is a beta build + if (lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV) === CONST.ENVIRONMENT.ADHOC) { + environment = CONST.ENVIRONMENT.ADHOC; + return resolve(environment); + } + + // If we haven't set the environment yet and we aren't on dev/adhoc, check to see if this is a beta build betaChecker.isBetaBuild() .then((isBeta) => { environment = isBeta ? CONST.ENVIRONMENT.STAGING : CONST.ENVIRONMENT.PRODUCTION; diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 99df2b448f7c..db76fb45b0d9 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -96,7 +96,9 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true, }); } if (response.jsonCode === CONST.JSON_CODE.MANY_WRITES_ERROR) { - alert('Too many auth writes', 'The API call did more auth write requests than allowed. Check the APIWriteCommands class in Web-Expensify'); + const {phpCommandName, authWriteCommandsCount} = response.data; + const message = `The API call (${phpCommandName}) did ${authWriteCommandsCount - 1} more Auth write requests than allowed. Check the APIWriteCommands class in Web-Expensify`; + alert('Too many auth writes', message); } return response; }); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 607985f5fef2..2447d553fe2d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import React from 'react'; import {createStackNavigator, CardStyleInterpolators} from '@react-navigation/stack'; import styles from '../../../styles/styles'; -import CONST from '../../../CONST'; const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, @@ -490,7 +489,7 @@ const SettingsModalStackNavigator = createModalStackNavigator([ return ReimbursementAccountPage; }, name: 'ReimbursementAccount', - initialParams: {stepToOpen: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT}, + initialParams: {stepToOpen: ''}, }, { getComponent: () => { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4cd7a81e23b4..0c70edfe8f9a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -860,12 +860,7 @@ function getReportName(report, policies = {}) { const participantsWithoutCurrentUser = _.without(participants, sessionEmail); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - const displayNames = []; - for (let i = 0; i < participantsWithoutCurrentUser.length; i++) { - const login = participantsWithoutCurrentUser[i]; - displayNames.push(getDisplayNameForParticipant(login, isMultipleParticipantReport)); - } - return displayNames.join(', '); + return _.map(participantsWithoutCurrentUser, login => getDisplayNameForParticipant(login, isMultipleParticipantReport)).join(', '); } /** @@ -1708,6 +1703,32 @@ function canLeaveRoom(report, isPolicyMember) { return true; } +/** + * Returns display names for those that can see the whisper. + * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. + * + * @param {string[]} participants + * @returns {string} + */ +function getWhisperDisplayNames(participants) { + const isWhisperOnlyVisibleToCurrentUSer = this.isCurrentUserTheOnlyParticipant(participants); + + // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it + if (isWhisperOnlyVisibleToCurrentUSer) { + return Localize.translateLocal('common.youAfterPreposition'); + } + + return _.map(participants, login => getDisplayNameForParticipant(login, !isWhisperOnlyVisibleToCurrentUSer)).join(', '); +} + +/** + * @param {string[]} participants + * @returns {Boolean} + */ +function isCurrentUserTheOnlyParticipant(participants) { + return participants && participants.length === 1 && participants[0] === sessionEmail; +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -1728,6 +1749,7 @@ export { isPolicyExpenseChatAdmin, isPublicRoom, isConciergeChatReport, + isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyEmails, hasExpensifyGuidesEmails, hasOutstandingIOU, @@ -1776,4 +1798,5 @@ export { getSmallSizeAvatar, getMoneyRequestOptions, canRequestMoney, + getWhisperDisplayNames, }; diff --git a/src/libs/SelectionScraper/index.js b/src/libs/SelectionScraper/index.js index 00e46d5aed7a..2d81d9f6ccf6 100644 --- a/src/libs/SelectionScraper/index.js +++ b/src/libs/SelectionScraper/index.js @@ -43,7 +43,8 @@ const getHTMLOfSelection = () => { // If clonedSelection has no text content this data has no meaning to us. if (clonedSelection.textContent) { - let node = null; + let parent; + let child = clonedSelection; // If selection starts and ends within same text node we use its parentNode. This is because we can't // use closest function on a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node. @@ -61,19 +62,21 @@ const getHTMLOfSelection = () => { // // and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom. if (range.commonAncestorContainer instanceof HTMLElement) { - node = range.commonAncestorContainer.closest(`[${tagAttribute}]`); + parent = range.commonAncestorContainer.closest(`[${tagAttribute}]`); } else { - node = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`); + parent = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`); } - // This means "range.commonAncestorContainer" is a text node. We simply get its parent node. - if (!node) { - node = range.commonAncestorContainer.parentNode; + // Keep traversing up to clone all parents with 'data-testid' attribute. + while (parent) { + const cloned = parent.cloneNode(); + cloned.appendChild(child); + child = cloned; + + parent = parent.parentNode.closest(`[${tagAttribute}]`); } - node = node.cloneNode(); - node.appendChild(clonedSelection); - div.appendChild(node); + div.appendChild(child); } } diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 6bc5e6d608d4..d4a037baeef1 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -266,7 +266,6 @@ function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: { - errors: null, isLoading: true, }, }, diff --git a/src/libs/actions/ReimbursementAccount/errors.js b/src/libs/actions/ReimbursementAccount/errors.js index 3427524711c2..5969559f3f93 100644 --- a/src/libs/actions/ReimbursementAccount/errors.js +++ b/src/libs/actions/ReimbursementAccount/errors.js @@ -27,7 +27,10 @@ function setBankAccountFormValidationErrors(errorFields) { */ function resetReimbursementAccount() { setBankAccountFormValidationErrors({}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + errors: null, + pendingAction: null, + }); } /** diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js index 94799fa6b3c6..ae7f26a9bfcd 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js @@ -24,6 +24,17 @@ function resetFreePlanBankAccount(bankAccountID) { }, { optimisticData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + shouldShowResetModal: false, + isLoading: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + ], + successData: [ { onyxMethod: CONST.ONYX.METHOD.SET, key: ONYXKEYS.ONFIDO_TOKEN, @@ -44,29 +55,17 @@ function resetFreePlanBankAccount(bankAccountID) { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: ReimbursementAccountProps.reimbursementAccountDefaultProps, }, - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: {isLoading: true}, - }, { onyxMethod: CONST.ONYX.METHOD.SET, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, value: {}, }, ], - 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}, + value: {isLoading: false, pendingAction: null}, }, ], }); diff --git a/src/libs/actions/TestTool.js b/src/libs/actions/TestTool.js new file mode 100644 index 000000000000..20e4bba308ac --- /dev/null +++ b/src/libs/actions/TestTool.js @@ -0,0 +1,22 @@ +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import ONYXKEYS from '../../ONYXKEYS'; +import CONST from '../../CONST'; + +let isTestToolsModalOpen = false; +Onyx.connect({ + key: ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN, + callback: val => isTestToolsModalOpen = val || false, +}); + +/** + * Toggle the test tools modal open or closed. + * Throttle the toggle to make the modal stay open if you accidentally tap an extra time, which is easy to do. + */ +function toggleTestToolsModal() { + const toggle = () => Onyx.set(ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN, !isTestToolsModalOpen); + const throttledToggle = _.throttle(toggle, CONST.TIMING.TEST_TOOLS_MODAL_THROTTLE_TIME); + throttledToggle(); +} + +export default toggleTestToolsModal; diff --git a/src/libs/actions/Welcome.js b/src/libs/actions/Welcome.js index 63bdc9411572..50c5ed3c6ab7 100644 --- a/src/libs/actions/Welcome.js +++ b/src/libs/actions/Welcome.js @@ -7,6 +7,7 @@ import ROUTES from '../../ROUTES'; import * as Policy from './Policy'; import ONYXKEYS from '../../ONYXKEYS'; import SCREENS from '../../SCREENS'; +import CONST from '../../CONST'; let resolveIsReadyPromise; let isReadyPromise = new Promise((resolve) => { @@ -36,7 +37,11 @@ Onyx.connect({ key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, initWithStoredValues: false, callback: (val) => { - isFirstTimeNewExpensifyUser = val; + // If isFirstTimeNewExpensifyUser was true do not update it to false. We update it to false inside the Welcome.show logic + // More context here https://github.com/Expensify/App/pull/16962#discussion_r1167351359 + if (!isFirstTimeNewExpensifyUser) { + isFirstTimeNewExpensifyUser = val; + } checkOnReady(); }, }); @@ -96,9 +101,10 @@ Onyx.connect({ * * @param {Object} params * @param {Object} params.routes - * @param {Function} params.showCreateMenu + * @param {Function} [params.showCreateMenu] + * @param {Function} [params.showPopoverMenu] */ -function show({routes, showCreateMenu}) { +function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => {}}) { isReadyPromise.then(() => { if (!isFirstTimeNewExpensifyUser) { return; @@ -113,9 +119,21 @@ function show({routes, showCreateMenu}) { const isDisplayingWorkspaceRoute = isWorkspaceRoute || exitingToWorkspaceRoute; // We want to display the Workspace chat first since that means a user is already in a Workspace and doesn't need to create another one - const workspaceChatReport = _.find(allReports, report => ReportUtils.isPolicyExpenseChat(report) && report.ownerEmail === email); + const workspaceChatReport = _.find( + allReports, + report => ReportUtils.isPolicyExpenseChat(report) && report.ownerEmail === email && report.statusNum !== CONST.REPORT.STATUS.CLOSED, + ); if (workspaceChatReport && !isDisplayingWorkspaceRoute) { + // This key is only updated when we call ReconnectApp, setting it to false now allows the user to navigate normally instead of always redirecting to the workspace chat + Onyx.set(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, false); Navigation.navigate(ROUTES.getReportRoute(workspaceChatReport.reportID)); + + // If showPopoverMenu exists and returns true then it opened the Popover Menu successfully, and we can update isFirstTimeNewExpensifyUser + // so the Welcome logic doesn't run again + if (showPopoverMenu()) { + isFirstTimeNewExpensifyUser = false; + } + return; } @@ -124,6 +142,9 @@ function show({routes, showCreateMenu}) { if (!Policy.isAdminOfFreePolicy(allPolicies) && !isDisplayingWorkspaceRoute) { showCreateMenu(); } + + // Update isFirstTimeNewExpensifyUser so the Welcome logic doesn't run again + isFirstTimeNewExpensifyUser = false; }); } diff --git a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js index c84296d4c33d..e7468b0209e7 100644 --- a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js +++ b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {ScrollView} from 'react-native'; import _ from 'underscore'; +import lodashGet from 'lodash/get'; import * as Expensicons from '../../components/Icon/Expensicons'; import * as Illustrations from '../../components/Icon/Illustrations'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -20,6 +21,7 @@ import withPolicy from '../workspace/withPolicy'; import * as ReimbursementAccountProps from './reimbursementAccountPropTypes'; import WorkspaceResetBankAccountModal from '../workspace/WorkspaceResetBankAccountModal'; import * as BankAccounts from '../../libs/actions/BankAccounts'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; const propTypes = { /** Bank account currently in setup */ @@ -28,9 +30,6 @@ const propTypes = { /** Callback to continue to the next step of the setup */ continue: PropTypes.func.isRequired, - /** Callback to reset the bank account */ - startOver: PropTypes.func.isRequired, - /** Policy values needed in the component */ policy: PropTypes.shape({ name: PropTypes.string, @@ -44,55 +43,67 @@ const propTypes = { const defaultProps = {policyName: ''}; -const ContinueBankAccountSetup = props => ( - - - - -
- - {props.translate('workspace.bankAccount.youreAlmostDone')} - -
-
-
+const ContinueBankAccountSetup = (props) => { + const errors = lodashGet(props.reimbursementAccount, 'errors', {}); + const pendingAction = lodashGet(props.reimbursementAccount, 'pendingAction', null); + return ( + + + + +
+ + + {props.translate('workspace.bankAccount.youreAlmostDone')} + +
+
+
- {props.reimbursementAccount.shouldShowResetModal && ( - - )} -
-); + {props.reimbursementAccount.shouldShowResetModal && ( + + )} +
+ ); +}; ContinueBankAccountSetup.propTypes = propTypes; ContinueBankAccountSetup.defaultProps = defaultProps; diff --git a/src/pages/ReimbursementAccount/EnableStep.js b/src/pages/ReimbursementAccount/EnableStep.js index 2b54f7eab1fa..03b431b270f1 100644 --- a/src/pages/ReimbursementAccount/EnableStep.js +++ b/src/pages/ReimbursementAccount/EnableStep.js @@ -1,4 +1,5 @@ import React from 'react'; +import _ from 'underscore'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; @@ -14,6 +15,7 @@ import CONST from '../../CONST'; import Button from '../../components/Button'; import * as Expensicons from '../../components/Icon/Expensicons'; import MenuItem from '../../components/MenuItem'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import getBankIcon from '../../components/Icon/BankIcons'; import * as ReimbursementAccountProps from './reimbursementAccountPropTypes'; import userPropTypes from '../settings/userPropTypes'; @@ -54,6 +56,8 @@ const EnableStep = (props) => { : ''; const bankName = achData.addressName; + const errors = lodashGet(props.reimbursementAccount, 'errors', {}); + const pendingAction = lodashGet(props.reimbursementAccount, 'pendingAction', null); return ( { title={!isUsingExpensifyCard ? props.translate('workspace.bankAccount.oneMoreThing') : props.translate('workspace.bankAccount.allSet')} icon={!isUsingExpensifyCard ? Illustrations.ConciergeNew : Illustrations.ThumbsUpStars} > - - - {!isUsingExpensifyCard - ? props.translate('workspace.bankAccount.accountDescriptionNoCards') - : props.translate('workspace.bankAccount.accountDescriptionWithCards')} - - {!isUsingExpensifyCard && ( -