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 && (
-