diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss
index 44ab37ffbca8..809463dd79b8 100644
--- a/docs/_sass/_main.scss
+++ b/docs/_sass/_main.scss
@@ -390,12 +390,12 @@ button {
.cards-group {
display: grid;
grid-template-columns: auto;
- row-gap: 12px;
- column-gap: 4%;
+ row-gap: 20px;
+ column-gap: 20px;
padding-bottom: 20px;
@include breakpoint($breakpoint-desktop) {
- grid-template-columns: 48% 48%;
+ grid-template-columns: auto auto;
}
}
@@ -456,12 +456,26 @@ button {
h3.title {
padding: 0;
margin: 0;
+
+ &.with-margin {
+ margin: 0 0 4px 0;
+ }
}
+
+
p.description {
padding: 0;
- margin: 16px 0 0 0;
+ margin: 0;
font-weight: normal;
+
+ &.with-min-height {
+ min-height: 68px;
+
+ @include breakpoint($breakpoint-tablet) {
+ min-height: 48px;
+ }
+ }
}
}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 1937c0102918..dcc764f10ffa 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
-
1.2.24
+
1.2.26
CFBundleSignature
????
CFBundleURLTypes
@@ -30,7 +30,7 @@
CFBundleVersion
-
1.2.24.4
+
1.2.26.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index f4241782e2fc..0affc6c431d0 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
-
1.2.24
+
1.2.26
CFBundleSignature
????
CFBundleVersion
-
1.2.24.4
+
1.2.26.0
diff --git a/package-lock.json b/package-lock.json
index 0a44a3029502..53241fceaad0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.2.24-4",
+ "version": "1.2.26-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.2.24-4",
+ "version": "1.2.26-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -38,7 +38,7 @@
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"dotenv": "^8.2.0",
- "expensify-common": "git+https://github.com/Expensify/expensify-common.git#6010b3c49a16f63ee9cecc0c720f6d10395775ef",
+ "expensify-common": "git+https://github.com/Expensify/expensify-common.git#805a4c34debc7e27b14e33847ac4df4d59b6d878",
"fbjs": "^3.0.2",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
@@ -68,7 +68,7 @@
"react-native-image-picker": "^4.8.5",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.27",
+ "react-native-onyx": "1.0.29",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^2.0.0",
"react-native-permissions": "^3.0.1",
@@ -23860,8 +23860,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#6010b3c49a16f63ee9cecc0c720f6d10395775ef",
- "integrity": "sha512-QRKmQUEVzdEwOJEsh1XFhcGdSl+taPdHYTUZt0Bdbb9SyQvBHX4g6/NqMIfQRYiBq6q3eKpAd4nTKmkaHAfKsA==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#805a4c34debc7e27b14e33847ac4df4d59b6d878",
+ "integrity": "sha512-46gx59CkEPWEH4Y0VDIZreZg7fvaPdi8TEitTu7kiejKmKJWnPxNt/2RDZK3QFHLUiMy0RzTxAozLqR7N2MsYw==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
@@ -35432,9 +35432,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.27",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.27.tgz",
- "integrity": "sha512-+kC4SQhgl+kI2PEnj6RAGSteMV2Amy9+1BvUV4gxpKEseMzcTHlpozxNeYiXYfgIo7lIsk0DicZbtwBuB3sgaw==",
+ "version": "1.0.29",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.29.tgz",
+ "integrity": "sha512-Zg7v6bimkRPqaAM5C/CqMswSObPGX+2BK2Gr7r4P1htSMjNlvn+fZ9gD4mwJBPgnoEUqMdFGLUshksHbE+GSMg==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -60809,9 +60809,9 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#6010b3c49a16f63ee9cecc0c720f6d10395775ef",
- "integrity": "sha512-QRKmQUEVzdEwOJEsh1XFhcGdSl+taPdHYTUZt0Bdbb9SyQvBHX4g6/NqMIfQRYiBq6q3eKpAd4nTKmkaHAfKsA==",
- "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#6010b3c49a16f63ee9cecc0c720f6d10395775ef",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#805a4c34debc7e27b14e33847ac4df4d59b6d878",
+ "integrity": "sha512-46gx59CkEPWEH4Y0VDIZreZg7fvaPdi8TEitTu7kiejKmKJWnPxNt/2RDZK3QFHLUiMy0RzTxAozLqR7N2MsYw==",
+ "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#805a4c34debc7e27b14e33847ac4df4d59b6d878",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
@@ -69778,9 +69778,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.27",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.27.tgz",
- "integrity": "sha512-+kC4SQhgl+kI2PEnj6RAGSteMV2Amy9+1BvUV4gxpKEseMzcTHlpozxNeYiXYfgIo7lIsk0DicZbtwBuB3sgaw==",
+ "version": "1.0.29",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.29.tgz",
+ "integrity": "sha512-Zg7v6bimkRPqaAM5C/CqMswSObPGX+2BK2Gr7r4P1htSMjNlvn+fZ9gD4mwJBPgnoEUqMdFGLUshksHbE+GSMg==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index cc2599d0d705..7e3fe98b3a2b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.2.24-4",
+ "version": "1.2.26-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.",
@@ -67,7 +67,7 @@
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"dotenv": "^8.2.0",
- "expensify-common": "git+https://github.com/Expensify/expensify-common.git#6010b3c49a16f63ee9cecc0c720f6d10395775ef",
+ "expensify-common": "git+https://github.com/Expensify/expensify-common.git#805a4c34debc7e27b14e33847ac4df4d59b6d878",
"fbjs": "^3.0.2",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
@@ -97,7 +97,7 @@
"react-native-image-picker": "^4.8.5",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.27",
+ "react-native-onyx": "1.0.29",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^2.0.0",
"react-native-permissions": "^3.0.1",
diff --git a/src/CONST.js b/src/CONST.js
index 02656fb2682e..88b4e789a3ad 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -265,9 +265,6 @@ const CONST = {
REMOVED_FROM_POLICY: 'removedFromPolicy',
POLICY_DELETED: 'policyDeleted',
},
- ERROR: {
- INACCESSIBLE_REPORT: 'Report not found',
- },
MESSAGE: {
TYPE: {
COMMENT: 'COMMENT',
@@ -318,6 +315,7 @@ const CONST = {
MAX_ROOM_NAME_LENGTH: 80,
LAST_MESSAGE_TEXT_MAX_LENGTH: 200,
OWNER_EMAIL_FAKE: '__FAKE__',
+ DEFAULT_REPORT_NAME: 'Chat Report',
},
COMPOSER: {
MAX_LINES: 16,
@@ -351,7 +349,6 @@ const CONST = {
SEARCH_RENDER: 'search_render',
HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render',
REPORT_INITIAL_RENDER: 'report_initial_render',
- HOMEPAGE_REPORTS_LOADED: 'homepage_reports_loaded',
SWITCH_REPORT: 'switch_report',
SIDEBAR_LOADED: 'sidebar_loaded',
COLD: 'cold',
@@ -576,6 +573,7 @@ const CONST = {
STEP: {
// In the order they appear in the Wallet flow
ADDITIONAL_DETAILS: 'AdditionalDetailsStep',
+ ADDITIONAL_DETAILS_KBA: 'AdditionalDetailsKBAStep',
ONFIDO: 'OnfidoStep',
TERMS: 'TermsStep',
ACTIVATE: 'ActivateStep',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 6872398c20f1..3ae1dcc48022 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -107,7 +107,6 @@ export default {
WORKSPACE_INVOICES: 'workspace/:policyID/invoices',
WORKSPACE_TRAVEL: 'workspace/:policyID/travel',
WORKSPACE_MEMBERS: 'workspace/:policyID/members',
- WORKSPACE_BANK_ACCOUNT: 'workspace/:policyID/bank-account',
WORKSPACE_NEW_ROOM: 'workspace/new-room',
getWorkspaceInitialRoute: policyID => `workspace/${policyID}`,
getWorkspaceInviteRoute: policyID => `workspace/${policyID}/invite`,
@@ -118,7 +117,6 @@ export default {
getWorkspaceInvoicesRoute: policyID => `workspace/${policyID}/invoices`,
getWorkspaceTravelRoute: policyID => `workspace/${policyID}/travel`,
getWorkspaceMembersRoute: policyID => `workspace/${policyID}/members`,
- getWorkspaceBankAccountRoute: policyID => `workspace/${policyID}/bank-account`,
getRequestCallRoute: taskID => `request-call/${taskID}`,
REQUEST_CALL: 'request-call/:taskID',
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 83bf0e5754aa..36e70e075733 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -245,15 +245,6 @@ class AttachmentModal extends PureComponent {
}
render() {
- // When the confirm button is visible we don't need bottom padding on the attachment view.
- const attachmentViewPaddingStyles = this.props.onConfirm
- ? [styles.pl5, styles.pr5, styles.pt5]
- : styles.p5;
-
- const attachmentViewStyles = this.props.isSmallScreenWidth || this.props.isMediumScreenWidth
- ? [styles.imageModalImageCenterContainer]
- : [styles.imageModalImageCenterContainer, attachmentViewPaddingStyles];
-
const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
const {fileName, fileExtension} = FileUtils.splitExtensionFromFileName(originalFileName);
@@ -285,7 +276,7 @@ class AttachmentModal extends PureComponent {
/>
) : ''}
/>
-
+
{this.state.reportId ? (
(
successIcon={Expensicons.Checkmark}
successText={props.translate('reportActionContextMenu.copied')}
isMini
- autoReset
onPress={() => Clipboard.setString(props.value)}
/>
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index 639742efa00d..96f28164e166 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -41,7 +41,7 @@ const defaultProps = {
isMini: false,
successIcon: null,
successText: '',
- autoReset: false,
+ autoReset: true,
description: '',
};
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
index 36adfe30f6b5..3597aa641192 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -14,6 +14,7 @@ import styles from '../../../styles/styles';
import Navigation from '../../../libs/Navigation/Navigation';
import AnchorForCommentsOnly from '../../AnchorForCommentsOnly';
import AnchorForAttachmentsOnly from '../../AnchorForAttachmentsOnly';
+import ROUTES from '../../../ROUTES';
const AnchorRenderer = (props) => {
const htmlAttribs = props.tnode.attributes;
@@ -30,6 +31,18 @@ const AnchorRenderer = (props) => {
&& attrHref.replace(CONFIG.EXPENSIFY.EXPENSIFY_URL, '');
const navigateToLink = () => {
+ // There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
+ // https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
+ // clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
+ // Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
+ // the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
+ if (attrHref.startsWith(CONFIG.EXPENSIFY.EXPENSIFY_URL) && attrHref.indexOf('newdotreport?reportID=') > -1) {
+ const reportID = attrHref.split('newdotreport?reportID=').pop();
+ const reportRoute = ROUTES.getReportRoute(reportID);
+ Navigation.navigate(reportRoute);
+ return;
+ }
+
// If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
// instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
if (internalNewExpensifyPath) {
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 14662bc370fa..91b1867a6f91 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -62,6 +62,8 @@ class BaseModal extends PureComponent {
swipeDirection,
animationIn,
animationOut,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaMargin,
shouldAddTopSafeAreaPadding,
shouldAddBottomSafeAreaPadding,
hideBackdrop,
@@ -128,8 +130,12 @@ class BaseModal extends PureComponent {
safeAreaPaddingBottom,
safeAreaPaddingLeft,
safeAreaPaddingRight,
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaMargin,
shouldAddBottomSafeAreaPadding,
shouldAddTopSafeAreaPadding,
+ modalContainerStyleMarginTop: modalContainerStyle.marginTop,
+ modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
});
diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js
index 144de7de6497..482e31405ef0 100644
--- a/src/components/PDFView/PDFPasswordForm.js
+++ b/src/components/PDFView/PDFPasswordForm.js
@@ -5,10 +5,7 @@ import {View, ScrollView} from 'react-native';
import Button from '../Button';
import Text from '../Text';
import TextInput from '../TextInput';
-import Icon from '../Icon';
-import * as Expensicons from '../Icon/Expensicons';
import styles from '../../styles/styles';
-import colors from '../../styles/colors';
import PDFInfoMessage from './PDFInfoMessage';
import compose from '../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
@@ -120,26 +117,16 @@ class PDFPasswordForm extends Component {
onChangeText={this.updatePassword}
returnKeyType="done"
onSubmitEditing={this.submitPassword}
- errorText={this.state.validationErrorText}
+ errorText={this.props.isPasswordInvalid ? this.props.translate('attachmentView.passwordIncorrect') : this.state.validationErrorText}
onFocus={() => this.props.onPasswordFieldFocused(true)}
onBlur={this.validateAndNotifyPasswordBlur}
autoFocus={this.props.shouldAutofocusPasswordField}
secureTextEntry
/>
- {this.props.isPasswordInvalid && (
-
-
-
-
- {this.props.translate('attachmentView.passwordIncorrect')}
-
-
-
- )}
diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js
index 2c5e147e22b9..f896135bc87c 100644
--- a/src/components/PDFView/index.native.js
+++ b/src/components/PDFView/index.native.js
@@ -143,7 +143,7 @@ class PDFView extends Component {
)}
{this.state.shouldRequestPassword && (
-
+
this.setState({isPasswordInvalid: false})}
diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js
index 8b819ba3c8d4..12e38b6294be 100644
--- a/src/components/SettlementButton.js
+++ b/src/components/SettlementButton.js
@@ -40,40 +40,36 @@ const defaultProps = {
};
class SettlementButton extends React.Component {
- constructor(props) {
- super(props);
+ componentDidMount() {
+ PaymentMethods.openPaymentsPage();
+ }
+ getButtonOptionsFromProps() {
const buttonOptions = [];
- if (props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(props.betas) && Permissions.canUseWallet(props.betas)) {
+ if (this.props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(this.props.betas) && Permissions.canUseWallet(this.props.betas)) {
buttonOptions.push({
- text: props.translate('iou.settleExpensify'),
+ text: this.props.translate('iou.settleExpensify'),
icon: Expensicons.Wallet,
value: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
});
}
- if (props.shouldShowPaypal) {
+ if (this.props.shouldShowPaypal) {
buttonOptions.push({
- text: props.translate('iou.settlePaypalMe'),
+ text: this.props.translate('iou.settlePaypalMe'),
icon: Expensicons.PayPal,
value: CONST.IOU.PAYMENT_TYPE.PAYPAL_ME,
});
}
buttonOptions.push({
- text: props.translate('iou.settleElsewhere'),
+ text: this.props.translate('iou.settleElsewhere'),
icon: Expensicons.Cash,
value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
});
- this.state = {
- buttonOptions,
- };
- }
-
- componentDidMount() {
- PaymentMethods.openPaymentsPage();
+ return buttonOptions;
}
render() {
@@ -98,7 +94,7 @@ class SettlementButton extends React.Component {
this.props.onPress(iouPaymentType);
}}
- options={this.state.buttonOptions}
+ options={this.getButtonOptionsFromProps()}
/>
)}
diff --git a/src/languages/en.js b/src/languages/en.js
index 40e761ed2209..46af537b89e0 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -382,7 +382,6 @@ export default {
payPalMe: 'PayPal.me/',
yourPayPalUsername: 'Your PayPal username',
addPayPalAccount: 'Add PayPal account',
- editPayPalAccount: 'Update PayPal account',
growlMessageOnSave: 'Your PayPal username was successfully added',
formatError: 'Invalid PayPal.me username',
},
diff --git a/src/languages/es.js b/src/languages/es.js
index 01a6b85579f0..5b80314f5099 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -384,7 +384,6 @@ export default {
yourPayPalUsername: 'Tu usuario de PayPal',
addPayPalAccount: 'Agregar cuenta de PayPal',
growlMessageOnSave: 'Su nombre de usuario de PayPal se agregó correctamente',
- editPayPalAccount: 'Actualizar cuenta de PayPal',
formatError: 'Usuario PayPal.me no válido',
},
addDebitCardPage: {
diff --git a/src/libs/EmojiUtils.js b/src/libs/EmojiUtils.js
index ed67a2aed1fd..22a15f04f379 100644
--- a/src/libs/EmojiUtils.js
+++ b/src/libs/EmojiUtils.js
@@ -195,7 +195,14 @@ function replaceEmojis(text) {
for (let i = 0; i < emojiData.length; i++) {
const checkEmoji = emojisTrie.search(emojiData[i].slice(1, -1));
if (checkEmoji && checkEmoji.metaData.code) {
- newText = newText.replace(emojiData[i], checkEmoji.metaData.code);
+ let emojiReplacement = checkEmoji.metaData.code;
+
+ // If this is the last emoji in the message and it's the end of the message so far,
+ // add a space after it so the user can keep typing easily.
+ if (i === emojiData.length - 1 && text.endsWith(emojiData[i])) {
+ emojiReplacement += ' ';
+ }
+ newText = newText.replace(emojiData[i], emojiReplacement);
}
}
return newText;
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index 0b9b62c25916..2c6407f70164 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -86,7 +86,6 @@ class AuthScreens extends React.Component {
super(props);
Timing.start(CONST.TIMING.HOMEPAGE_INITIAL_RENDER);
- Timing.start(CONST.TIMING.HOMEPAGE_REPORTS_LOADED);
}
componentDidMount() {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 788e6b49e541..d37b2229d977 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -378,13 +378,6 @@ const SettingsModalStackNavigator = createModalStackNavigator([
},
name: 'Workspace_Members',
},
- {
- getComponent: () => {
- const WorkspaceBankAccountPage = require('../../../pages/workspace/WorkspaceBankAccountPage').default;
- return WorkspaceBankAccountPage;
- },
- name: 'Workspace_BankAccount',
- },
{
getComponent: () => {
const WorkspaceInvitePage = require('../../../pages/workspace/WorkspaceInvitePage').default;
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index dafe15789e54..d3ba9fdb96ca 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -127,10 +127,6 @@ export default {
Workspace_Members: {
path: ROUTES.WORKSPACE_MEMBERS,
},
- Workspace_BankAccount: {
- path: ROUTES.WORKSPACE_BANK_ACCOUNT,
- exact: true,
- },
Workspace_Invite: {
path: ROUTES.WORKSPACE_INVITE,
},
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 4f2128063d76..9252911b4535 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -816,7 +816,7 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency,
*/
function buildOptimisticChatReport(
participantList,
- reportName = 'Chat Report',
+ reportName = CONST.REPORT.DEFAULT_REPORT_NAME,
chatType = '',
policyID = CONST.POLICY.OWNER_EMAIL_FAKE,
ownerEmail = CONST.REPORT.OWNER_EMAIL_FAKE,
diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js
index 482538eb4993..4d17ee63ebbc 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -213,12 +213,13 @@ function setUpPoliciesAndNavigate(session) {
// and those are passed as a search parameter when using transition links
const ownerEmail = url.searchParams.get('ownerEmail');
const makeMeAdmin = url.searchParams.get('makeMeAdmin');
+ const policyName = url.searchParams.get('policyName');
const shouldCreateFreePolicy = !isLoggingInAsNewUser
&& Str.startsWith(url.pathname, Str.normalizeUrl(ROUTES.TRANSITION_FROM_OLD_DOT))
&& exitTo === ROUTES.WORKSPACE_NEW;
if (shouldCreateFreePolicy) {
- Policy.createWorkspace(ownerEmail, makeMeAdmin);
+ Policy.createWorkspace(ownerEmail, makeMeAdmin, policyName);
return;
}
if (!isLoggingInAsNewUser && exitTo) {
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index 3571894df938..2128172e872b 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -77,6 +77,7 @@ function deleteWorkspace(policyID, reports) {
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
statusNum: CONST.REPORT.STATUS.CLOSED,
hasDraft: false,
+ oldPolicyName: allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`].name,
},
})),
];
@@ -84,7 +85,7 @@ function deleteWorkspace(policyID, reports) {
// Restore the old report stateNum and statusNum
const failureData = [
..._.map(reports, ({
- reportID, stateNum, statusNum, hasDraft,
+ reportID, stateNum, statusNum, hasDraft, oldPolicyName,
}) => ({
onyxMethod: CONST.ONYX.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
@@ -92,6 +93,7 @@ function deleteWorkspace(policyID, reports) {
stateNum,
statusNum,
hasDraft,
+ oldPolicyName,
},
})),
];
@@ -119,6 +121,38 @@ function isAdminOfFreePolicy(policies) {
&& policy.role === CONST.POLICY.ROLE.ADMIN);
}
+/**
+* Check if the user has any active free policies (aka workspaces)
+*
+* @param {Array} policies
+* @returns {Boolean}
+*/
+function hasActiveFreePolicy(policies) {
+ const adminFreePolicies = _.filter(policies, policy => policy
+ && policy.type === CONST.POLICY.TYPE.FREE
+ && policy.role === CONST.POLICY.ROLE.ADMIN);
+
+ if (adminFreePolicies.length === 0) {
+ return false;
+ }
+
+ if (_.some(adminFreePolicies, policy => !policy.pendingAction)) {
+ return true;
+ }
+
+ if (_.some(adminFreePolicies, policy => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)) {
+ return true;
+ }
+
+ if (_.some(adminFreePolicies, policy => policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)) {
+ return false;
+ }
+
+ // If there are no add or delete pending actions the only option left is an update
+ // pendingAction, in which case we should return true.
+ return true;
+}
+
/**
* Remove the passed members from the policy employeeList
*
@@ -662,10 +696,11 @@ function generatePolicyID() {
*
* @param {String} [ownerEmail] Optional, the email of the account to make the owner of the policy
* @param {Boolean} [makeMeAdmin] Optional, leave the calling account as an admin on the policy
+ * @param {String} [policyName] Optional, custom policy name we will use for created workspace
*/
-function createWorkspace(ownerEmail = '', makeMeAdmin = false) {
+function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '') {
const policyID = generatePolicyID();
- const workspaceName = generateDefaultWorkspaceName(ownerEmail);
+ const workspaceName = policyName || generateDefaultWorkspaceName(ownerEmail);
const {
announceChatReportID,
@@ -871,6 +906,7 @@ export {
removeMembers,
addMembersToWorkspace,
isAdminOfFreePolicy,
+ hasActiveFreePolicy,
setWorkspaceErrors,
clearCustomUnitErrors,
hideWorkspaceAlertMessage,
diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js
index 28cfbea88a9f..f695137764fa 100644
--- a/src/libs/actions/ReimbursementAccount/navigation.js
+++ b/src/libs/actions/ReimbursementAccount/navigation.js
@@ -6,7 +6,6 @@ import CONST from '../../../CONST';
import ONYXKEYS from '../../../ONYXKEYS';
import ROUTES from '../../../ROUTES';
import Navigation from '../../Navigation/Navigation';
-import BankAccount from '../../models/BankAccount';
const WITHDRAWAL_ACCOUNT_STEPS = [
{
@@ -95,18 +94,9 @@ function goToWithdrawalAccountSetupStep(stepID, achData) {
/**
* Navigate to the correct bank account route based on the bank account state and type
- *
- * @param {String} policyID
*/
-function navigateToBankAccountRoute(policyID) {
- const achData = store.getReimbursementAccountInSetup();
- const state = lodashGet(achData, 'state');
- const isShowPage = lodashGet(achData, 'bankAccountID') && state !== BankAccount.STATE.OPEN;
- if (isShowPage) {
- Navigation.navigate(ROUTES.getWorkspaceBankAccountRoute(policyID));
- } else {
- Navigation.navigate(ROUTES.getBankAccountRoute());
- }
+function navigateToBankAccountRoute() {
+ Navigation.navigate(ROUTES.getBankAccountRoute());
}
export {
diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
index 021623a51aa9..24a7919fe824 100644
--- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
+++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
@@ -1,11 +1,7 @@
import lodashGet from 'lodash/get';
import ONYXKEYS from '../../../ONYXKEYS';
-import CONST from '../../../CONST';
import * as store from './store';
-import Navigation from '../../Navigation/Navigation';
-import ROUTES from '../../../ROUTES';
import * as API from '../../API';
-import BankAccount from '../../models/BankAccount';
/**
* Reset user's reimbursement account. This will delete the bank account.
@@ -19,34 +15,43 @@ function resetFreePlanBankAccount() {
throw new Error('Missing credentials when attempting to reset free plan bank account');
}
- const achData = {
- useOnfido: true,
- policyID: '',
- isInSetup: true,
- domainLimit: 0,
- currentStep: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT,
- state: BankAccount.STATE.DELETED,
- };
-
API.write('RestartBankAccountSetup',
{
bankAccountID,
ownerEmail: store.getCredentials().login,
},
{
- optimisticData: [{
- onyxMethod: 'merge',
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {achData, shouldShowResetModal: false},
- },
- {
- onyxMethod: 'set',
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT,
- value: null,
- }],
+ optimisticData: [
+ {
+ onyxMethod: 'set',
+ key: ONYXKEYS.ONFIDO_TOKEN,
+ value: '',
+ },
+ {
+ onyxMethod: 'set',
+ key: ONYXKEYS.PLAID_DATA,
+ value: {},
+ },
+ {
+ onyxMethod: 'set',
+ key: ONYXKEYS.PLAID_LINK_TOKEN,
+ value: '',
+ },
+ {
+ onyxMethod: 'set',
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ achData: {},
+ shouldShowResetModal: false,
+ },
+ },
+ {
+ onyxMethod: 'set',
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT,
+ value: null,
+ },
+ ],
});
-
- Navigation.navigate(ROUTES.getBankAccountRoute());
}
export default resetFreePlanBankAccount;
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index ee240ac8ce67..49dddb0cf094 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -8,12 +8,10 @@ import ONYXKEYS from '../../ONYXKEYS';
import * as Pusher from '../Pusher/pusher';
import LocalNotification from '../Notification/LocalNotification';
import PushNotification from '../Notification/PushNotification';
-import * as PersonalDetails from './PersonalDetails';
import Navigation from '../Navigation/Navigation';
import * as ActiveClientManager from '../ActiveClientManager';
import Visibility from '../Visibility';
import ROUTES from '../../ROUTES';
-import Timing from './Timing';
import * as DeprecatedAPI from '../deprecatedAPI';
import * as API from '../API';
import CONFIG from '../../CONFIG';
@@ -25,6 +23,7 @@ import Growl from '../Growl';
import * as Localize from '../Localize';
import DateUtils from '../DateUtils';
import * as ReportActionsUtils from '../ReportActionsUtils';
+import * as OptionsListUtils from '../OptionsListUtils';
let currentUserEmail;
let currentUserAccountID;
@@ -216,118 +215,6 @@ function fetchIOUReport(iouReportID, chatReportID) {
});
}
-/**
- * Given debtorEmail finds active IOU report ID via GetIOUReport API call
- *
- * @param {String} debtorEmail
- * @returns {Promise}
- */
-function fetchIOUReportID(debtorEmail) {
- return DeprecatedAPI.GetIOUReport({
- debtorEmail,
- }).then((response) => {
- const iouReportID = response.reportID || 0;
- if (response.jsonCode !== 200) {
- console.error(response.message);
- return;
- }
- if (iouReportID === 0) {
- // If there is no IOU report for this user then we will assume it has been paid and do nothing here.
- // All reports are initialized with hasOutstandingIOU: false. Since the IOU report we were looking for has
- // been settled then there's nothing more to do.
- Log.info('GetIOUReport returned a reportID of 0, not fetching IOU report data');
- return;
- }
- return iouReportID;
- });
-}
-
-/**
- * Fetches chat reports when provided a list of chat report IDs.
- * If the shouldRedirectIfInaccessible flag is set, we redirect to the Concierge chat
- * when we find an inaccessible chat
- * @param {Array} chatList
- * @param {Boolean} shouldRedirectIfInaccessible
- * @returns {Promise} only used internally when fetchAllReports() is called
- */
-function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) {
- let fetchedReports;
- const simplifiedReports = {};
- return DeprecatedAPI.GetReportSummaryList({reportIDList: chatList.join(',')})
- .then(({reportSummaryList, jsonCode}) => {
- Log.info('[Report] successfully fetched report data', false, {chatList});
- fetchedReports = reportSummaryList;
-
- // If we receive a 404 response while fetching a single report, treat that report as inaccessible.
- if (jsonCode === 404 && shouldRedirectIfInaccessible) {
- throw new Error(CONST.REPORT.ERROR.INACCESSIBLE_REPORT);
- }
-
- return Promise.all(_.map(fetchedReports, (chatReport) => {
- // If there aren't any IOU actions, we don't need to fetch any additional data
- if (!chatReport.hasIOUAction) {
- return;
- }
-
- // Group chat reports cannot and should not be associated with a specific IOU report
- const participants = getParticipantEmailsFromReport(chatReport);
- if (participants.length > 1) {
- return;
- }
- if (participants.length === 0) {
- Log.alert('[Report] Report with IOU action but does not have any participant.', {
- reportID: chatReport.reportID,
- participants,
- });
- return;
- }
-
- return fetchIOUReportID(participants[0])
- .then((iouReportID) => {
- if (!iouReportID) {
- return Promise.resolve();
- }
-
- return fetchIOUReport(iouReportID, chatReport.reportID);
- });
- }));
- })
- .then((iouReportObjects) => {
- // Process the reports and store them in Onyx. At the same time we'll save the simplified reports in this
- // variable called simplifiedReports which hold the participants (minus the current user) for each report.
- // Using this simplifiedReport we can call PersonalDetails.getFromReportParticipants to get the
- // personal details of all the participants and even link up their avatars to report icons.
- const reportIOUData = {};
- _.each(fetchedReports, (report) => {
- const simplifiedReport = getSimplifiedReportObject(report);
- simplifiedReports[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = simplifiedReport;
- });
-
- _.each(iouReportObjects, (iouReportObject) => {
- if (!iouReportObject) {
- return;
- }
-
- const iouReportKey = `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReportObject.reportID}`;
- const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${iouReportObject.chatReportID}`;
- reportIOUData[iouReportKey] = iouReportObject;
- simplifiedReports[reportKey].iouReportID = iouReportObject.reportID;
- simplifiedReports[reportKey].hasOutstandingIOU = iouReportObject.stateNum
- === CONST.REPORT.STATE_NUM.PROCESSING && iouReportObject.total !== 0;
- });
-
- // We use mergeCollection such that it updates the collection in one go.
- // Any withOnyx subscribers to this key will also receive the complete updated props just once
- // than updating props for each report and re-rendering had merge been used.
- Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_IOUS, reportIOUData);
- Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, simplifiedReports);
-
- // Fetch the personal details if there are any
- PersonalDetails.getFromReportParticipants(_.values(simplifiedReports));
- return simplifiedReports;
- });
-}
-
/**
* Given IOU object, save the data to Onyx.
*
@@ -481,91 +368,6 @@ function unsubscribeFromReportChannel(reportID) {
Pusher.unsubscribe(pusherChannelName);
}
-/**
- * Get the report ID for a chat report for a specific
- * set of participants and navigate to it if wanted.
- *
- * @param {String[]} participants
- * @param {Boolean} shouldNavigate
- * @returns {Promise}
- */
-function fetchOrCreateChatReport(participants, shouldNavigate = true) {
- if (participants.length < 2) {
- throw new Error('fetchOrCreateChatReport() must have at least two participants.');
- }
-
- return DeprecatedAPI.CreateChatReport({
- emailList: participants.join(','),
- })
- .then((data) => {
- if (data.jsonCode !== 200) {
- console.error(data.message);
- Growl.error(data.message);
- return;
- }
-
- // Merge report into Onyx
- const simplifiedReportObject = getSimplifiedReportObject(data);
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${data.reportID}`, simplifiedReportObject);
-
- // Fetch the personal details if there are any
- PersonalDetails.getFromReportParticipants([simplifiedReportObject]);
-
- if (shouldNavigate) {
- // Redirect the logged in person to the new report
- Navigation.navigate(ROUTES.getReportRoute(data.reportID));
- }
-
- // We are returning an array with a report object here since fetchAllReports calls this method or
- // fetchChatReportsByIDs which returns an array of report objects.
- return [simplifiedReportObject];
- });
-}
-
-/**
- * Get all of our reports
- *
- * @param {Boolean} shouldRecordHomePageTiming whether or not performance timing should be measured
- * @returns {Promise}
- */
-function fetchAllReports(
- shouldRecordHomePageTiming = false,
-) {
- Onyx.set(ONYXKEYS.IS_LOADING_REPORT_DATA, true);
- return DeprecatedAPI.Get({
- returnValueList: 'chatList',
- })
- .then((response) => {
- if (response.jsonCode !== 200) {
- return;
- }
-
- // The cast here is necessary as Get rvl='chatList' may return an int or Array
- const reportIDs = _.filter(String(response.chatList).split(','), _.identity);
-
- // Get all the chat reports if they have any, otherwise create one with concierge
- if (reportIDs.length > 0) {
- return fetchChatReportsByIDs(reportIDs);
- }
-
- return fetchOrCreateChatReport([currentUserEmail, CONST.EMAIL.CONCIERGE], false);
- })
- .then((returnedReports) => {
- Onyx.set(ONYXKEYS.IS_LOADING_REPORT_DATA, false);
-
- // If at this point the user still doesn't have a Concierge report, create it for them.
- // This means they were a participant in reports before their account was created (e.g. default rooms)
- const hasConciergeChat = _.some(returnedReports, report => ReportUtils.isConciergeChatReport(report));
- if (!hasConciergeChat) {
- fetchOrCreateChatReport([currentUserEmail, CONST.EMAIL.CONCIERGE], false);
- }
-
- if (shouldRecordHomePageTiming) {
- Timing.end(CONST.TIMING.HOMEPAGE_REPORTS_LOADED);
- }
- });
-}
-
/**
* Add up to two report actions to a report. This method can be called for the following situations:
*
@@ -708,6 +510,7 @@ function openReport(reportID, participantList = [], newReportObject = {}) {
isLoadingMoreReportActions: false,
lastVisitedTimestamp: Date.now(),
lastReadSequenceNumber: getMaxSequenceNumber(reportID),
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
},
}],
successData: [{
@@ -756,6 +559,25 @@ function openReport(reportID, participantList = [], newReportObject = {}) {
onyxData);
}
+/**
+ * This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat.
+ *
+ * @param {Array} userLogins list of user logins.
+ */
+function navigateToAndOpenReport(userLogins) {
+ const formattedUserLogins = _.map(userLogins, login => OptionsListUtils.addSMSDomainIfPhoneNumber(login).toLowerCase());
+ let newChat = {};
+ const chat = ReportUtils.getChatByParticipants(formattedUserLogins);
+ if (!chat) {
+ newChat = ReportUtils.buildOptimisticChatReport(formattedUserLogins);
+ }
+ const reportID = chat ? chat.reportID : newChat.reportID;
+
+ // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
+ openReport(reportID, newChat.participants, newChat);
+ Navigation.navigate(ROUTES.getReportRoute(reportID));
+}
+
/**
* Get the latest report history without marking the report as read.
*
@@ -969,7 +791,7 @@ function handleReportChanged(report) {
// A report can be missing a name if a comment is received via pusher event
// and the report does not yet exist in Onyx (eg. a new DM created with the logged in person)
if (report.reportID && report.reportName === undefined) {
- fetchChatReportsByIDs([report.reportID]);
+ openReport(report.reportID);
}
}
@@ -1218,7 +1040,7 @@ function updateNotificationPreference(reportID, previousValue, newValue) {
function navigateToConciergeChat() {
// If we don't have a chat with Concierge then create it
if (!conciergeChatReportID) {
- fetchOrCreateChatReport([currentUserEmail, CONST.EMAIL.CONCIERGE], true);
+ navigateToAndOpenReport([CONST.EMAIL.CONCIERGE]);
return;
}
@@ -1250,8 +1072,7 @@ function addPolicyReport(policy, reportName, visibility) {
);
// Onyx.set is used on the optimistic data so that it is present before navigating to the workspace room. With Onyx.merge the workspace room reportID is not present when
- // fetchReportIfNeeded is called on the ReportScreen, so fetchChatReportsByIDs is called which is unnecessary since the optimistic data will be stored in Onyx.
- // If there was an error creating the room, then fetchChatReportsByIDs throws an error and the user is navigated away from the report instead of showing the RBR error message.
+ // fetchReportIfNeeded is called on the ReportScreen, so openReport is called which is unnecessary since the optimistic data will be stored in Onyx.
// Therefore, Onyx.set is used instead of Onyx.merge.
const optimisticData = [
{
@@ -1521,9 +1342,6 @@ Onyx.connect({
});
export {
- fetchAllReports,
- fetchOrCreateChatReport,
- fetchChatReportsByIDs,
fetchIOUReportByID,
addComment,
addAttachment,
@@ -1549,6 +1367,7 @@ export {
readNewestAction,
readOldestAction,
openReport,
+ navigateToAndOpenReport,
openPaymentDetailsPage,
updatePolicyRoomName,
clearPolicyRoomNameErrors,
diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js
index b5dd9d2b4cd0..1dd77c6eb5f0 100644
--- a/src/libs/deprecatedAPI.js
+++ b/src/libs/deprecatedAPI.js
@@ -36,18 +36,6 @@ function ChangePassword(parameters) {
return Network.post(commandName, parameters);
}
-/**
- * @param {object} parameters
- * @param {string} parameters.emailList
- * @returns {Promise}
- */
-function CreateChatReport(parameters) {
- const commandName = 'CreateChatReport';
- requireParameters(['emailList'],
- parameters, commandName);
- return Network.post(commandName, parameters);
-}
-
/**
* @param {Object} parameters
* @param {String} parameters.email
@@ -303,17 +291,6 @@ function BankAccount_SetupWithdrawal(parameters) {
);
}
-/**
- * @param {Object} parameters
- * @param {String} parameters.reportIDList
- * @returns {Promise}
- */
-function GetReportSummaryList(parameters) {
- const commandName = 'Get';
- requireParameters(['reportIDList'], parameters, commandName);
- return Network.post(commandName, {...parameters, returnValueList: 'reportSummaryList'});
-}
-
/**
* Transfer Wallet balance and takes either the bankAccoundID or fundID
* @param {Object} parameters
@@ -343,13 +320,11 @@ function GetStatementPDF(parameters) {
export {
BankAccount_SetupWithdrawal,
ChangePassword,
- CreateChatReport,
CreateLogin,
DeleteLogin,
Get,
GetStatementPDF,
GetIOUReport,
- GetReportSummaryList,
Graphite_Timer,
PayIOU,
PayWithWallet,
diff --git a/src/libs/models/BankAccount.js b/src/libs/models/BankAccount.js
index 56eb80cb49e4..709d83cf5419 100644
--- a/src/libs/models/BankAccount.js
+++ b/src/libs/models/BankAccount.js
@@ -1,17 +1,8 @@
import _ from 'underscore';
-import Onyx from 'react-native-onyx';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import lodashHas from 'lodash/has';
import CONST from '../../CONST';
-import ONYXKEYS from '../../ONYXKEYS';
-
-let currentUserLogin;
-
-Onyx.connect({
- key: ONYXKEYS.SESSION,
- callback: val => currentUserLogin = val && val.email,
-});
class BankAccount {
static STATE = {
@@ -33,17 +24,17 @@ class BankAccount {
* @returns {Number}
*/
getID() {
- return this.json.bankAccountID;
+ return this.json.methodID;
}
/**
- * Return the account number, which has been obfuscate by the back end
+ * Return the account number, which has been obfuscated by the back end
* example "XXXXXX3956"
*
* @returns {String}
*/
getMaskedAccountNumber() {
- return this.json.accountNumber;
+ return this.json.accountData.accountNumber;
}
/**
@@ -51,21 +42,21 @@ class BankAccount {
* @returns {String}
*/
getAddressName() {
- return this.json.addressName;
+ return this.json.accountData.addressName;
}
/**
* @returns {String}
*/
getProcessor() {
- return this.json.processor;
+ return this.json.accountData.processor;
}
/**
* @returns {String}
*/
getRoutingNumber() {
- return this.json.routingNumber;
+ return this.json.accountData.routingNumber;
}
/**
@@ -73,7 +64,7 @@ class BankAccount {
* @return {String[]}
*/
getSharees() {
- return this.json.sharees;
+ return this.json.accountData.sharees;
}
/**
@@ -81,7 +72,7 @@ class BankAccount {
* @private
*/
getState() {
- return this.json.state;
+ return this.json.accountData.state;
}
/**
@@ -112,11 +103,11 @@ class BankAccount {
* @returns {Boolean}
*/
isVerifying() {
- return Boolean(this.json.validating) || this.getState() === BankAccount.STATE.VERIFYING;
+ return this.getState() === BankAccount.STATE.VERIFYING;
}
/**
- * If the user didn't finish entering all his info.
+ * If the user didn't finish entering all their info.
* @returns {Boolean}
*/
isInSetup() {
@@ -130,29 +121,13 @@ class BankAccount {
return this.getState() === BankAccount.STATE.LOCKED;
}
- /**
- * If someone asked to share the bank account with me, and the request is still pending
- * @returns {Boolean}
- */
- isSharePending() {
- return Boolean(this.json.shareComplete === false && this.getOwner() !== currentUserLogin);
- }
-
- /**
- * Who shared this account with me?
- * @returns {String}
- */
- getOwner() {
- return this.json.ownedBy;
- }
-
/**
* Is it the account to use by default to receive money?
*
* @returns {Boolean}
*/
isDefaultCredit() {
- return this.json.defaultCredit === true;
+ return this.json.accountData.defaultCredit === true;
}
/**
@@ -161,15 +136,7 @@ class BankAccount {
* @returns {Boolean}
*/
isWithdrawal() {
- return this.json.allowDebit === true;
- }
-
- /**
- * Get when the user last updated their bank account.
- * @return {*|String}
- */
- getDateSigned() {
- return lodashGet(this.json, ['additionalData', 'dateSigned']) || '';
+ return this.json.accountData.allowDebit === true;
}
/**
@@ -204,7 +171,7 @@ class BankAccount {
* @returns {Boolean}
*/
isRiskChecked() {
- return Boolean(this.json.riskChecked);
+ return Boolean(this.json.accountData.riskChecked);
}
/**
@@ -220,7 +187,7 @@ class BankAccount {
* @returns {string}
*/
getCountry() {
- return lodashGet(this.json, ['additionalData', 'country'], CONST.COUNTRY.US);
+ return lodashGet(this.json, ['accountData', 'additionalData', 'country'], CONST.COUNTRY.US);
}
/**
@@ -228,7 +195,7 @@ class BankAccount {
* @returns {String}
*/
getCurrency() {
- return lodashGet(this.json, ['additionalData', 'currency'], 'USD');
+ return lodashGet(this.json, ['accountData', 'additionalData', 'currency'], 'USD');
}
/**
@@ -236,7 +203,7 @@ class BankAccount {
* @returns {String}
*/
getBankName() {
- return lodashGet(this.json, ['additionalData', 'bankName'], lodashGet(this.json, 'bankName'));
+ return lodashGet(this.json, ['accountData', 'additionalData', 'bankName'], '');
}
/**
@@ -244,7 +211,7 @@ class BankAccount {
* @returns {Boolean}
*/
hasInternationalWireDetails() {
- return lodashGet(this.json, ['additionalData', 'fieldsType'], 'local') === 'international';
+ return lodashGet(this.json, ['accountData', 'additionalData', 'fieldsType'], 'local') === 'international';
}
/**
@@ -252,7 +219,7 @@ class BankAccount {
* @returns {Object}
*/
getAdditionalData() {
- return this.json.additionalData || {};
+ return this.json.accountData.additionalData || {};
}
/**
@@ -277,7 +244,7 @@ class BankAccount {
* @return {Boolean}
*/
needsToUpgrade() {
- return !this.isInSetup() && !lodashHas(this.json, ['additionalData', 'beneficialOwners']);
+ return !this.isInSetup() && !lodashHas(this.json, ['accountData', 'additionalData', 'beneficialOwners']);
}
}
diff --git a/src/libs/shouldDelayFocus/index.android.js b/src/libs/shouldDelayFocus/index.android.js
new file mode 100644
index 000000000000..4de1e1fc7f3a
--- /dev/null
+++ b/src/libs/shouldDelayFocus/index.android.js
@@ -0,0 +1,2 @@
+// When using transitions on Android, we need to delay focusing the text inputs for the keyboard to open.
+export default true;
diff --git a/src/libs/shouldDelayFocus/index.js b/src/libs/shouldDelayFocus/index.js
new file mode 100644
index 000000000000..33136544dba2
--- /dev/null
+++ b/src/libs/shouldDelayFocus/index.js
@@ -0,0 +1 @@
+export default false;
diff --git a/src/pages/ConciergePage.js b/src/pages/ConciergePage.js
index efde8c1db55c..1793f651dde2 100644
--- a/src/pages/ConciergePage.js
+++ b/src/pages/ConciergePage.js
@@ -2,7 +2,6 @@ import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
-import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import Navigation from '../libs/Navigation/Navigation';
@@ -26,7 +25,7 @@ const propTypes = {
*/
const ConciergePage = (props) => {
if (_.has(props.session, 'authToken')) {
- Report.fetchOrCreateChatReport([props.session.email, CONST.EMAIL.CONCIERGE]);
+ Report.navigateToConciergeChat();
} else {
Navigation.navigate();
}
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index 17ad4f971393..7ca5afb9c857 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -196,7 +196,7 @@ class DetailsPage extends React.PureComponent {
Report.fetchOrCreateChatReport([this.props.session.email, details.login])}
+ onPress={() => Report.navigateToAndOpenReport([details.login])}
wrapperStyle={styles.breakAll}
shouldShowRightIcon
/>
diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index 6dc289234917..1a90a8a35189 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -4,6 +4,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
+import moment from 'moment';
import IdologyQuestions from './IdologyQuestions';
import ScreenWrapper from '../../components/ScreenWrapper';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
@@ -231,7 +232,7 @@ class AdditionalDetailsStep extends React.Component {
*/
clearDateErrorsAndSetValue(value) {
this.formHelper.clearErrors(this.props, ['dob', 'age']);
- Wallet.updateAdditionalDetailsDraft({dob: value});
+ Wallet.updateAdditionalDetailsDraft({dob: moment(value).format(CONST.DATE.MOMENT_FORMAT_STRING)});
}
/**
diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.js
index 29acf875cdae..42aeea32622c 100644
--- a/src/pages/EnablePayments/EnablePaymentsPage.js
+++ b/src/pages/EnablePayments/EnablePaymentsPage.js
@@ -77,8 +77,10 @@ class EnablePaymentsPage extends React.Component {
const currentStep = this.props.userWallet.currentStep || CONST.WALLET.STEP.ADDITIONAL_DETAILS;
return (
<>
- {currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS && }
- {currentStep === CONST.WALLET.STEP.ONFIDO && }
+ {(currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS || currentStep === CONST.WALLET.STEP.ADDITIONAL_DETAILS_KBA)
+ && }
+ {currentStep === CONST.WALLET.STEP.ONFIDO && this.props.walletAdditionalDetailsDraft
+ && }
{currentStep === CONST.WALLET.STEP.TERMS && }
{currentStep === CONST.WALLET.STEP.ACTIVATE && }
>
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index 8096bc697522..963ac5a9f524 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -8,7 +8,6 @@ import * as OptionsListUtils from '../libs/OptionsListUtils';
import ONYXKEYS from '../ONYXKEYS';
import styles from '../styles/styles';
import * as Report from '../libs/actions/Report';
-import * as ReportUtils from '../libs/ReportUtils';
import CONST from '../CONST';
import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions';
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
@@ -19,7 +18,6 @@ import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
import reportPropTypes from './reportPropTypes';
-import ROUTES from '../ROUTES';
const propTypes = {
/** Whether screen is used to create group chat */
@@ -131,23 +129,6 @@ class NewChatPage extends Component {
return sections;
}
- /**
- * This will find an existing chat, or create a new one if none exists, for the given user or set of users. It will then navigate to this chat.
- *
- * @param {Array} userLogins list of user logins.
- */
- getOrCreateChatReport(userLogins) {
- const formattedUserLogins = _.map(userLogins, login => OptionsListUtils.addSMSDomainIfPhoneNumber(login).toLowerCase());
- let newChat = {};
- const chat = ReportUtils.getChatByParticipants(formattedUserLogins);
- if (!chat) {
- newChat = ReportUtils.buildOptimisticChatReport(formattedUserLogins);
- }
- const reportID = chat ? chat.reportID : newChat.reportID;
- Report.openReport(reportID, newChat.participants, newChat);
- Navigation.navigate(ROUTES.getReportRoute(reportID));
- }
-
/**
* Removes a selected option from list if already selected. If not already selected add this option to the list.
* @param {Object} option
@@ -198,7 +179,7 @@ class NewChatPage extends Component {
* @param {Object} option
*/
createChat(option) {
- this.getOrCreateChatReport([option.login]);
+ Report.navigateToAndOpenReport([option.login]);
}
/**
@@ -210,7 +191,7 @@ class NewChatPage extends Component {
if (userLogins.length < 1) {
return;
}
- this.getOrCreateChatReport(userLogins);
+ Report.navigateToAndOpenReport(userLogins);
}
render() {
diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js
index d5f58071dc12..c42b0f7186f4 100644
--- a/src/pages/ReimbursementAccount/BankAccountManualStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js
@@ -17,6 +17,7 @@ import ONYXKEYS from '../../ONYXKEYS';
import exampleCheckImage from './exampleCheckImage';
import Form from '../../components/Form';
import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils';
+import shouldDelayFocus from '../../libs/shouldDelayFocus';
const propTypes = {
...withLocalizePropTypes,
@@ -92,6 +93,8 @@ class BankAccountManualStep extends React.Component {
source={exampleCheckImage(this.props.preferredLocale)}
/>
{
/>
{!props.user.validated && (
-
-
-
-
+
+
{props.translate('bankAccount.validateAccountError')}
diff --git a/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
new file mode 100644
index 000000000000..feeab0ec1fee
--- /dev/null
+++ b/src/pages/ReimbursementAccount/ContinueBankAccountSetup.js
@@ -0,0 +1,84 @@
+import lodashGet from 'lodash/get';
+import PropTypes from 'prop-types';
+import React from 'react';
+import {ScrollView} from 'react-native';
+import _ from 'underscore';
+import * as BankAccounts from '../../libs/actions/BankAccounts';
+import * as Expensicons from '../../components/Icon/Expensicons';
+import * as Illustrations from '../../components/Icon/Illustrations';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import Button from '../../components/Button';
+import compose from '../../libs/compose';
+import CONST from '../../CONST';
+import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
+import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
+import MenuItem from '../../components/MenuItem';
+import Navigation from '../../libs/Navigation/Navigation';
+import styles from '../../styles/styles';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import Section from '../../components/Section';
+import Text from '../../components/Text';
+import withPolicy from '../workspace/withPolicy';
+import WorkspaceResetBankAccountModal from '../workspace/WorkspaceResetBankAccountModal';
+
+const propTypes = {
+ continue: PropTypes.func.isRequired,
+
+ /** Policy values needed in the component */
+ policy: PropTypes.shape({
+ name: PropTypes.string,
+ }).isRequired,
+
+ ...withLocalizePropTypes,
+};
+
+const ContinueBankAccountSetup = props => (
+
+
+
+
+
+
+ {props.translate('workspace.bankAccount.youreAlmostDone')}
+
+
+
+
+
+
+
+
+);
+
+ContinueBankAccountSetup.propTypes = propTypes;
+ContinueBankAccountSetup.displayName = 'ContinueBankAccountSetup';
+
+export default compose(
+ withPolicy,
+ withLocalize,
+)(ContinueBankAccountSetup);
diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
index 4b532a8daf6c..1e671e3bb5aa 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js
@@ -11,6 +11,7 @@ import ONYXKEYS from '../../ONYXKEYS';
import ReimbursementAccountLoadingIndicator from '../../components/ReimbursementAccountLoadingIndicator';
import Navigation from '../../libs/Navigation/Navigation';
import CONST from '../../CONST';
+import BankAccount from '../../libs/models/BankAccount';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import compose from '../../libs/compose';
import styles from '../../styles/styles';
@@ -22,6 +23,7 @@ import networkPropTypes from '../../components/networkPropTypes';
// Steps
import BankAccountStep from './BankAccountStep';
import CompanyStep from './CompanyStep';
+import ContinueBankAccountSetup from './ContinueBankAccountSetup';
import RequestorStep from './RequestorStep';
import ValidationStep from './ValidationStep';
import ACHContractStep from './ACHContractStep';
@@ -72,6 +74,17 @@ const defaultProps = {
};
class ReimbursementAccountPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.continue = this.continue.bind(this);
+
+ const achData = lodashGet(this.props, 'reimbursementAccount.achData', {});
+ const hasInProgressVBBA = achData.bankAccountID && achData.state !== BankAccount.STATE.OPEN;
+ this.state = {
+ shouldShowContinueSetupButton: hasInProgressVBBA,
+ };
+ }
+
componentDidMount() {
this.fetchData();
}
@@ -155,6 +168,12 @@ class ReimbursementAccountPage extends React.Component {
BankAccounts.fetchFreePlanVerifiedBankAccount(stepToOpen !== CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT ? stepToOpen : '');
}
+ continue() {
+ this.setState({
+ shouldShowContinueSetupButton: false,
+ });
+ }
+
render() {
// The SetupWithdrawalAccount flow allows us to continue the flow from various points depending on where the
// user left off. This view will refer to the achData as the single source of truth to determine which route to
@@ -176,6 +195,15 @@ class ReimbursementAccountPage extends React.Component {
);
}
+ const hasInProgressVBBA = achData.bankAccountID && achData.state !== BankAccount.STATE.OPEN;
+ if (hasInProgressVBBA && this.state.shouldShowContinueSetupButton) {
+ return (
+
+ );
+ }
+
let errorComponent;
const userHasPhonePrimaryEmail = Str.endsWith(this.props.session.email, CONST.SMS.DOMAIN);
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index 81acad10eebc..b50e82b774a0 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -150,10 +150,7 @@ class SearchPage extends Component {
Navigation.navigate(ROUTES.getReportRoute(option.reportID));
});
} else {
- Report.fetchOrCreateChatReport([
- this.props.session.email,
- option.login,
- ]);
+ Report.navigateToAndOpenReport(option.login);
}
}
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 8bb7acdd0457..7d6828b01a3c 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -172,7 +172,7 @@ class ReportScreen extends React.Component {
return;
}
- Report.fetchChatReportsByIDs([reportIDFromPath], true);
+ Report.openReport(reportIDFromPath);
}
/**
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
index 1e6180e31cb6..7a59d3ac1935 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js
@@ -58,6 +58,7 @@ class BaseReportActionContextMenu extends React.Component {
selection: this.props.selection,
})}
description={contextAction.getDescription(this.props.selection, this.props.isSmallScreenWidth)}
+ autoReset={contextAction.autoReset}
/>
))}
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js
index 7d84a9b318de..66a16a1ed036 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.js
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js
@@ -157,6 +157,7 @@ export default [
}
},
getDescription: () => {},
+ autoReset: false,
},
{
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index fbec3639d87c..ab5aa6fba104 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -144,7 +144,9 @@ class ReportActionCompose extends React.Component {
},
maxLines: props.isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES,
value: props.comment,
- conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - 1),
+
+ // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions
+ conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - (this.props.isSmallScreenWidth ? 4 : 1)),
};
}
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 11c47f2fd391..eec8c56f0dcd 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -164,6 +164,65 @@ class SidebarLinks extends React.Component {
SidebarLinks.propTypes = propTypes;
SidebarLinks.defaultProps = defaultProps;
+/**
+ * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering
+ * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI.
+ * @param {Object} [report]
+ * @returns {Object|undefined}
+ */
+const reportSelector = report => report && ({
+ reportID: report.reportID,
+ participants: report.participants,
+ hasDraft: report.hasDraft,
+ isPinned: report.isPinned,
+ errorFields: {
+ addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom,
+ },
+ maxSequenceNumber: report.maxSequenceNumber,
+ lastReadSequenceNumber: report.lastReadSequenceNumber,
+ lastMessageText: report.lastMessageText,
+ lastMessageTimestamp: report.lastMessageTimestamp,
+ iouReportID: report.iouReportID,
+ hasOutstandingIOU: report.hasOutstandingIOU,
+ statusNum: report.statusNum,
+ stateNum: report.stateNum,
+ chatType: report.chatType,
+ policyID: report.policyID,
+});
+
+/**
+ * @param {Object} [personalDetails]
+ * @returns {Object|undefined}
+ */
+const personalDetailsSelector = personalDetails => _.reduce(personalDetails, (finalPersonalDetails, personalData, login) => {
+ // It's OK to do param-reassignment in _.reduce() because we absolutely know the starting state of finalPersonalDetails
+ // eslint-disable-next-line no-param-reassign
+ finalPersonalDetails[login] = {
+ login: personalData.login,
+ displayName: personalData.displayName,
+ firstName: personalData.firstName,
+ avatar: personalData.avatar,
+ };
+ return finalPersonalDetails;
+}, {});
+
+/**
+ * @param {Object} [reportActions]
+ * @returns {Object|undefined}
+ */
+const reportActionsSelector = reportActions => reportActions && _.map(reportActions, reportAction => ({
+ errors: reportAction.errors,
+}));
+
+/**
+ * @param {Object} [policy]
+ * @returns {Object|undefined}
+ */
+const policySelector = policy => policy && ({
+ type: policy.type,
+ name: policy.name,
+});
+
export default compose(
withLocalize,
withCurrentUserPersonalDetails,
@@ -176,9 +235,11 @@ export default compose(
// with 10,000 withOnyx() connections, it would have unknown performance implications.
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
+ selector: reportSelector,
},
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS,
+ selector: personalDetailsSelector,
},
priorityMode: {
key: ONYXKEYS.NVP_PRIORITY_MODE,
@@ -188,9 +249,11 @@ export default compose(
},
reportActions: {
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ selector: reportActionsSelector,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
+ selector: policySelector,
},
preferredLocale: {
key: ONYXKEYS.NVP_PREFERRED_LOCALE,
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index 49249a9c642a..c1118a3f37e5 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -171,7 +171,7 @@ class BaseSidebarScreen extends Component {
onSelected: () => Navigation.navigate(ROUTES.IOU_BILL),
},
] : []),
- ...(!Policy.isAdminOfFreePolicy(this.props.allPolicies) ? [
+ ...(!Policy.hasActiveFreePolicy(this.props.allPolicies) ? [
{
icon: Expensicons.NewWorkspace,
iconWidth: 46,
diff --git a/src/pages/settings/AppDownloadLinks.js b/src/pages/settings/AppDownloadLinks.js
index 35e72eef7dc6..55dcfead937a 100644
--- a/src/pages/settings/AppDownloadLinks.js
+++ b/src/pages/settings/AppDownloadLinks.js
@@ -17,6 +17,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/
import canUseTouchScreen from '../../libs/canUseTouchscreen';
import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu';
import * as ContextMenuActions from '../home/report/ContextMenu/ContextMenuActions';
+import PopoverReportActionContextMenu from '../home/report/ContextMenu/PopoverReportActionContextMenu';
const propTypes = {
...withLocalizePropTypes,
@@ -80,6 +81,9 @@ const AppDownloadLinksPage = (props) => {
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
+
{_.map(menuItems, item => (
@@ -111,14 +95,6 @@ class AddPayPalMePage extends React.Component {
}
}
-AddPayPalMePage.propTypes = propTypes;
-AddPayPalMePage.defaultProps = defaultProps;
+AddPayPalMePage.propTypes = {...withLocalizePropTypes};
-export default compose(
- withLocalize,
- withOnyx({
- payPalMeData: {
- key: ONYXKEYS.PAYPAL,
- },
- }),
-)(AddPayPalMePage);
+export default withLocalize(AddPayPalMePage);
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index f924e5a7ee8d..7691b02e9f43 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -227,7 +227,7 @@ class PaymentMethodList extends Component {
icon={Expensicons.CreditCard}
onPress={e => this.props.onPress(e)}
isDisabled={this.props.isLoadingPayments || isOffline}
- style={[styles.mh4, styles.mb4, styles.buttonCTA]}
+ style={[styles.mh4, styles.buttonCTA]}
iconStyles={[styles.buttonCTAIcon]}
key="addPaymentMethodButton"
success
diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
index ef6bf593440f..f9c36bd873fe 100644
--- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
@@ -269,7 +269,7 @@ class BasePaymentsPage extends React.Component {
onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS)}
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
-
+
{Permissions.canUseWallet(this.props.betas) && (
<>
@@ -308,7 +308,13 @@ class BasePaymentsPage extends React.Component {
>
{this.props.translate('paymentsPage.paymentMethodsTitle')}
- PaymentMethods.clearWalletError()} errors={this.props.userWallet.errors} errorRowStyles={[styles.ph6, styles.pv2]}>
+ PaymentMethods.clearWalletError()}
+ errors={this.props.userWallet.errors}
+ errorRowStyles={[styles.ph6]}
+ >
(
-
- Navigation.navigate(ROUTES.getWorkspaceInitialRoute(this.props.route.params.policyID))}
- shouldShowGetAssistanceButton
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_BANK_ACCOUNT}
- shouldShowBackButton
- />
-
-
-
- {this.props.translate('workspace.bankAccount.youreAlmostDone')}
-
-
-
-
-
-
-
-
- );
- }
-}
-
-WorkspaceBankAccountPage.propTypes = propTypes;
-WorkspaceBankAccountPage.defaultProps = defaultProps;
-
-export default compose(
- withLocalize,
- withOnyx({
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- }),
- withPolicy,
-)(WorkspaceBankAccountPage);
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index 4d58db2782fe..395707142f7a 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -55,7 +55,7 @@ const propTypes = {
...policyPropTypes,
...withLocalizePropTypes,
- ...networkPropTypes,
+ network: networkPropTypes.isRequired,
};
const defaultProps = policyDefaultProps;
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
index 477dc02f9642..9c0f18ad1a70 100644
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ b/src/pages/workspace/WorkspaceMembersPage.js
@@ -49,7 +49,7 @@ const propTypes = {
...policyPropTypes,
...withLocalizePropTypes,
...windowDimensionsPropTypes,
- ...networkPropTypes,
+ network: networkPropTypes.isRequired,
};
const defaultProps = policyDefaultProps;
@@ -323,8 +323,8 @@ class WorkspaceMembersPage extends React.Component {
confirmText={this.props.translate('common.remove')}
cancelText={this.props.translate('common.cancel')}
/>
-
-
+
+
-
+
item.login}
- showsVerticalScrollIndicator={false}
+ showsVerticalScrollIndicator
+ style={[styles.ph5, styles.pb5]}
/>
diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js
index e778ecf89339..9916619518c6 100644
--- a/src/pages/workspace/WorkspaceSettingsPage.js
+++ b/src/pages/workspace/WorkspaceSettingsPage.js
@@ -85,6 +85,7 @@ class WorkspaceSettingsPage extends React.Component {
style={[styles.mh5, styles.mt5, styles.flexGrow1]}
validate={this.validate}
onSubmit={this.submit}
+ enabledWhenOffline
>
({
...getBaseModalStyles(type, windowDimensions, popoverAnchorPosition, containerStyle),
+ shouldAddTopSafeAreaMargin: false,
shouldAddTopSafeAreaPadding: false,
});
diff --git a/tests/actions/ReimbursementAccountTest.js b/tests/actions/ReimbursementAccountTest.js
deleted file mode 100644
index de0217224b92..000000000000
--- a/tests/actions/ReimbursementAccountTest.js
+++ /dev/null
@@ -1,376 +0,0 @@
-import Onyx from 'react-native-onyx';
-import * as BankAccounts from '../../src/libs/actions/BankAccounts';
-import ONYXKEYS from '../../src/ONYXKEYS';
-import * as TestHelper from '../utils/TestHelper';
-import HttpUtils from '../../src/libs/HttpUtils';
-import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
-import CONST from '../../src/CONST';
-import BankAccount from '../../src/libs/models/BankAccount';
-
-const TEST_BANK_ACCOUNT_ID = 1;
-const TEST_BANK_ACCOUNT_CITY = 'Opa-locka';
-const TEST_BANK_ACCOUNT_STATE = 'FL';
-const TEST_BANK_ACCOUNT_STREET = '1234 Sesame Street';
-const TEST_BANK_ACCOUNT_ZIP = '33054';
-const TEST_BANK_ACCOUNT_NUMBER = '1111222233331111';
-const TEST_BANK_ACCOUNT_NUMBER_MASKED = '111122XXXXXX1111';
-const TEST_BANK_ACCOUNT_ROUTING_NUMBER = '011401533';
-const TEST_BANK_ACCOUNT_WEBSITE = 'https://www.test.com';
-
-const FREE_PLAN_NVP_RESPONSE = {
- jsonCode: 200,
- nameValuePairs: {
- expensify_freePlanBankAccountID: TEST_BANK_ACCOUNT_ID,
- },
-};
-
-HttpUtils.xhr = jest.fn();
-
-let reimbursementAccount;
-Onyx.connect({
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- callback: val => reimbursementAccount = val,
-});
-
-jest.mock('../../src/libs/Log');
-
-beforeAll(() => Onyx.init());
-
-beforeEach(() => Onyx.clear()
- .then(() => {
- TestHelper.signInWithTestUser();
- return waitForPromisesToResolve();
- }));
-
-describe('actions/BankAccounts', () => {
- // eslint-disable-next-line arrow-body-style
- it('should fetch the correct initial state for a user with no account in setup. And direct them to the correct steps after calling SetupWithdrawalAccount', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that should return nothing
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- }));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that should return no account
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- bankAccountList: [],
- }));
-
- // WHEN we fetch the bank account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN we should expect it to stop loading and bring us to the BankAccountStep
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT);
- expect(reimbursementAccount.achData.isInSetup).toBe(true);
-
- // WHEN we mock a successful call to SetupWithdrawalAccount with a manual account
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- }));
- BankAccounts.setupWithdrawalAccount({
- acceptTerms: true,
- country: 'US',
- currency: 'USD',
- accountNumber: TEST_BANK_ACCOUNT_NUMBER,
- fieldsType: 'local',
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- setupType: CONST.BANK_ACCOUNT.SUBSTEP.MANUAL,
- });
- return waitForPromisesToResolve();
- })
- .then(() => {
- // THEN we should advance to the CompanyStep and the enableCardAfterVerified param should be added
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.COMPANY);
- expect(reimbursementAccount.achData.enableCardAfterVerified).toBe(true);
- expect(reimbursementAccount.achData.setupType).toBe(CONST.BANK_ACCOUNT.SUBSTEP.MANUAL);
-
- // GIVEN another mock response to simulate the user completing the CompanyStep
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- achData: {
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- },
- }));
-
- // and a mock to SetNameValuePair call that updates the "free plan" bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- }));
-
- // WHEN we call setupWithdrawalAccount again with CompanyStep data
- BankAccounts.setupWithdrawalAccount({
- companyName: 'Alberta Bobbeth Charleson',
- companyPhone: '5165671515',
- companyTaxID: '123456789',
- incorporationDate: '2021-01-01',
- incorporationState: TEST_BANK_ACCOUNT_STATE,
- incorporationType: 'LLC',
- addressCity: TEST_BANK_ACCOUNT_CITY,
- addressState: TEST_BANK_ACCOUNT_STATE,
- addressStreet: TEST_BANK_ACCOUNT_STREET,
- addressZipCode: TEST_BANK_ACCOUNT_ZIP,
- hasNoConnectionToCannabis: true,
- website: TEST_BANK_ACCOUNT_WEBSITE,
- });
- return waitForPromisesToResolve();
- })
- .then(() => {
- // THEN we should advance to the RequestorStep
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
- });
- });
-
- it('fetch the correct step for account in setup that has completed the CompanyStep. Redirect to the ACHContract step after calling SetupWithdrawalAccount via RequestorStep', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that returns a bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve(FREE_PLAN_NVP_RESPONSE));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that should return a bank account in the list
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- bankAccountList: [{
- accountNumber: TEST_BANK_ACCOUNT_NUMBER_MASKED,
- additionalData: {
- currentStep: CONST.BANK_ACCOUNT.STEP.COMPANY,
- },
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.SETUP,
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- }],
- }));
-
- // WHEN we fetch the bank account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN we should to navigate to the RequestorStep
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
- expect(reimbursementAccount.achData.bankAccountID).toBe(TEST_BANK_ACCOUNT_ID);
- expect(reimbursementAccount.achData.isInSetup).toBe(true);
- expect(reimbursementAccount.achData.accountNumber).toBe(TEST_BANK_ACCOUNT_NUMBER_MASKED);
- expect(reimbursementAccount.achData.routingNumber).toBe(TEST_BANK_ACCOUNT_ROUTING_NUMBER);
- expect(reimbursementAccount.achData.state).toBe(BankAccount.STATE.SETUP);
-
- // GIVEN a mocked response for SetupWithdrawalAccount
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- achData: {
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- isOnfidoSetupComplete: true,
- },
- }));
-
- // And mock resonse to SetNameValuePair
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({jsonCode: 200}));
-
- // WHEN we call setupWithdrawalAccount on the RequestorStep
- BankAccounts.setupWithdrawalAccount({
- dob: '1980-01-01',
- firstName: 'Alberta',
- isControllingOfficer: true,
- lastName: 'Charleson',
- onfidoData: '',
- ssnLast4: '1234',
- requestorAddressCity: TEST_BANK_ACCOUNT_CITY,
- requestorAddressState: TEST_BANK_ACCOUNT_STATE,
- requestorAddressStreet: TEST_BANK_ACCOUNT_STREET,
- requestorAddressZipCode: TEST_BANK_ACCOUNT_ZIP,
- isOnfidoSetupComplete: false,
- });
- return waitForPromisesToResolve();
- })
- .then(() => {
- // THEN we should move to the ACHContract step and Onfido should be marked as complete
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT);
- expect(reimbursementAccount.achData.isOnfidoSetupComplete).toBe(true);
- });
- });
-
- it('should fetch the correct initial state for a user with an account in setup (bailed after RequestorStep and did NOT complete Onfido)', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that returns a bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve(FREE_PLAN_NVP_RESPONSE));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that should return a bank account that has completed both
- // the RequestorStep and CompanyStep, but not completed Onfido
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- bankAccountList: [{
- accountNumber: TEST_BANK_ACCOUNT_NUMBER_MASKED,
- additionalData: {
- currentStep: CONST.BANK_ACCOUNT.STEP.REQUESTOR,
- isOnfidoSetupComplete: false,
- },
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.SETUP,
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- }],
- }));
-
- // WHEN we fetch the bank account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN we should expect it redirect the user back to the RequestorStep because they still need to do Onfido
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
- });
- });
-
- it('should fetch the correct initial state for a user with an account in setup (bailed after RequestorStep - but completed Onfido)', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that returns a bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve(FREE_PLAN_NVP_RESPONSE));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that should return a bank account
- // that has completed both the RequestorStep and CompanyStep and has completed Onfido
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- bankAccountList: [{
- accountNumber: TEST_BANK_ACCOUNT_NUMBER_MASKED,
- additionalData: {
- currentStep: CONST.BANK_ACCOUNT.STEP.REQUESTOR,
- isOnfidoSetupComplete: true,
- },
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.SETUP,
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- }],
- }));
-
- // WHEN we fetch the bank account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN we should expect to be navigated to the ACHContractStep step
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT);
-
- HttpUtils.xhr.mockImplementation((command) => {
- // WHEN we mock a sucessful call to SetupWithdrawalAccount while on the ACHContractStep
- switch (command) {
- case 'BankAccount_SetupWithdrawal':
- return Promise.resolve({
- jsonCode: 200,
- achData: {
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- },
- });
-
- // And mock the response of Get&returnValueList=bankAccountList
- case 'Get':
- return Promise.resolve({
- jsonCode: 200,
- bankAccountList: [{
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.PENDING,
- }],
- });
- default:
- return Promise.resolve({jsonCode: 200});
- }
- });
-
- // WHEN we call setupWithdrawalAccount via the ACHContractStep
- BankAccounts.setupWithdrawalAccount({
- acceptTermsAndConditions: true,
- beneficialOwners: [],
- certifyTrueInformation: true,
- hasOtherBeneficialOwners: false,
- ownsMoreThan25Percent: true,
- });
- return waitForPromisesToResolve();
- })
- .then(() => {
- // THEN we should expect to have an account in the PENDING state and be brought to the ValidationStep
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.VALIDATION);
- expect(reimbursementAccount.achData.state).toBe(BankAccount.STATE.PENDING);
- });
- });
-
- it('should fetch the correct initial state for a user on the ACHContractStep in PENDING state', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that returns a bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve(FREE_PLAN_NVP_RESPONSE));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that should return a bank account that has completed both
- // the RequestorStep, CompanyStep, and ACHContractStep and is now PENDING
- HttpUtils.xhr
- .mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: [],
- bankAccountList: [{
- accountNumber: TEST_BANK_ACCOUNT_NUMBER_MASKED,
- additionalData: {
- currentStep: CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT,
- isOnfidoSetupComplete: true,
- },
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.PENDING,
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- }],
- }));
-
- // WHEN we fetch the account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN we should see that we are directed to the ValidationStep
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.achData.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.VALIDATION);
- expect(reimbursementAccount.achData.state).toBe(BankAccount.STATE.PENDING);
- });
- });
-
- it('should return the correct state when a user has reached the max validation attempts', () => {
- // GIVEN a mock response for a call to Get&returnValueList=nameValuePairs&name=expensify_freePlanBankAccountID that returns a bankAccountID
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve(FREE_PLAN_NVP_RESPONSE));
-
- // and a mock response for a call to Get&returnValueList=nameValuePairs,bankAccountList&nvpNames that
- // should return a bank account that has completed both the RequestorStep, CompanyStep, and ACHContractStep
- // and is now PENDING - but has attempted to validate too many times.
- HttpUtils.xhr.mockImplementationOnce(() => Promise.resolve({
- jsonCode: 200,
- nameValuePairs: {
- [`private_failedBankValidations_${TEST_BANK_ACCOUNT_ID}`]: 8,
- },
- bankAccountList: [{
- accountNumber: TEST_BANK_ACCOUNT_NUMBER_MASKED,
- additionalData: {
- currentStep: CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT,
- isOnfidoSetupComplete: true,
- },
- bankAccountID: TEST_BANK_ACCOUNT_ID,
- state: BankAccount.STATE.PENDING,
- routingNumber: TEST_BANK_ACCOUNT_ROUTING_NUMBER,
- }],
- }));
-
- // WHEN we fetch the account
- BankAccounts.fetchFreePlanVerifiedBankAccount();
- return waitForPromisesToResolve()
- .then(() => {
- // THEN it should have maxAttemptsReached set to true and show the correct data set in Onyx
- expect(reimbursementAccount.isLoading).toBe(false);
- expect(reimbursementAccount.error).toBe('');
- expect(reimbursementAccount.maxAttemptsReached).toBe(true);
- });
- });
-});
diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js
index 56f531da7751..cafe2d1a7cb9 100644
--- a/tests/ui/UnreadIndicatorsTest.js
+++ b/tests/ui/UnreadIndicatorsTest.js
@@ -129,7 +129,7 @@ function signInAndGetAppWithUnreadChat() {
// Simulate setting an unread report and personal details
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {
reportID: REPORT_ID,
- reportName: 'Chat Report',
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
maxSequenceNumber: 9,
lastReadSequenceNumber: 1,
lastMessageTimestamp: MOMENT_TEN_MINUTES_AGO.utc().valueOf(),
@@ -264,7 +264,7 @@ describe('Unread Indicators', () => {
const NEW_REPORT_CREATED_MOMENT = moment();
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, {
reportID: NEW_REPORT_ID,
- reportName: 'Chat Report',
+ reportName: CONST.REPORT.DEFAULT_REPORT_NAME,
maxSequenceNumber: 1,
lastReadSequenceNumber: 0,
lastMessageTimestamp: NEW_REPORT_CREATED_MOMENT.utc().valueOf(),
diff --git a/tests/unit/EmojiTest.js b/tests/unit/EmojiTest.js
index 5a66a57e970b..387d716a1f03 100644
--- a/tests/unit/EmojiTest.js
+++ b/tests/unit/EmojiTest.js
@@ -78,9 +78,14 @@ describe('EmojiTest', () => {
expect(EmojiUtils.containsOnlyEmojis('😄 👋')).toBe(true);
});
- it('replaces emoji codes with emojis inside a text', () => {
- const text = 'Hi :smile::wave:';
- expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄👋');
+ it('replaces an emoji code with an emoji and a space', () => {
+ const text = 'Hi :smile:';
+ expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄 ');
+ });
+
+ it('will not add a space after the last emoji if there is text after it', () => {
+ const text = 'Hi :smile::wave:no space after last emoji';
+ expect(EmojiUtils.replaceEmojis(text)).toBe('Hi 😄👋no space after last emoji');
});
it('suggests emojis when typing emojis prefix after colon', () => {
diff --git a/tests/unit/fetchFreePlanVerifiedBankAccountTest.js b/tests/unit/fetchFreePlanVerifiedBankAccountTest.js
deleted file mode 100644
index 634e6aae53e3..000000000000
--- a/tests/unit/fetchFreePlanVerifiedBankAccountTest.js
+++ /dev/null
@@ -1,199 +0,0 @@
-import CONST from '../../src/CONST';
-import * as fetchFreePlanVerifiedBankAccount from '../../src/libs/actions/ReimbursementAccount/fetchFreePlanVerifiedBankAccount';
-import BankAccount from '../../src/libs/models/BankAccount';
-
-describe('getCurrentStep', () => {
- it('Returns BankAccountStep when there is no step in storage, achData, bankAccount, etc', () => {
- // GIVEN a bank account that doesn't yet exist and no stepToOpen
- const nullBankAccount = null;
- const achData = {};
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep('', achData, nullBankAccount, false);
-
- // THEN it will be the BankAccountStep
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT);
- });
-
- it('Returns BankAccountStep when there is no step in storage or bankAccount but is achData', () => {
- // GIVEN a bank account that doesn't yet exist and no stepToOpen
- const nullBankAccount = null;
- const achData = {
- bankAccountInReview: false,
- domainLimit: 0,
- isInSetup: true,
- policyID: '',
- subStep: '',
- useOnfido: true,
- };
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep('', achData, nullBankAccount, false);
-
- // THEN it will be the BankAccountStep
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT);
- });
-
- it('Returns whatever step we give for stepToOpen', () => {
- // GIVEN a bank account that doesn't yet exist and has a stepToOpen
- const nullBankAccount = null;
- const achData = {};
- const stepToOpen = CONST.BANK_ACCOUNT.STEP.COMPANY;
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, nullBankAccount, false);
-
- // THEN it will be whatever we set the stepToOpen to be
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.COMPANY);
- });
-
- it('Returns the logical next step if we have a currentStep in achData', () => {
- // GIVEN a bank account that does exist and has no stepToOpen and "isInSetup"
- const bankAccount = new BankAccount({});
- const achData = {currentStep: CONST.BANK_ACCOUNT.STEP.COMPANY, isInSetup: true};
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, false);
-
- // THEN it will be the logical next step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
- });
-
- it('Returns the requestor step if we have to do onfido', () => {
- // GIVEN a bank account that does exist and has no stepToOpen and "isInSetup" and must do Onfido
- const bankAccount = new BankAccount({});
- const achData = {
- currentStep: CONST.BANK_ACCOUNT.STEP.REQUESTOR,
- isInSetup: true,
- isOnfidoSetupComplete: false,
- };
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, false);
-
- // THEN we will stay on the requestor step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.REQUESTOR);
- });
-
- it('Returns steps based on pending BankAccount if there is no current step in achData or device storage', () => {
- // GIVEN a pending bank account, but no currentStep in achData or device storage
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.PENDING,
- });
- const achData = {};
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, false);
-
- // THEN it will be the validation step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.VALIDATION);
- });
-
- it('Returns steps based on verifying BankAccount if there is no current step in achData or device storage', () => {
- // GIVEN a pending bank account, but no currentStep in achData or device storage
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.VERIFYING,
- });
- const achData = {};
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, false);
-
- // THEN it will be the validation step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.VALIDATION);
- });
-
- it('Returns step based on open BankAccount', () => {
- // GIVEN an open bank account that does not need to pass checks
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.OPEN,
- additionalData: {
- hasFullSSN: true,
- beneficialOwners: [{
- hasFullSSN: true,
- isRequestor: true,
- expectIDPA: {
- status: 'pass',
- },
- }],
- requestorAddressCity: 'Portland',
- },
- });
- const achData = {};
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, true);
-
- // THEN it will be the enable step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.ENABLE);
- });
-
- it('Returns step based on deleted BankAccount and no currentStep', () => {
- // GIVEN a deleted bank account
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.DELETED,
- });
- const achData = {};
- const stepToOpen = '';
-
- // WHEN we get the current step
- const currentStep = fetchFreePlanVerifiedBankAccount.getCurrentStep(stepToOpen, achData, bankAccount, true);
-
- // THEN it will be the bank account step
- expect(currentStep).toBe(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT);
- });
-});
-
-describe('buildACHData()', () => {
- it('Returns the correct shape for a bank account in setup', () => {
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.SETUP,
- });
- const achData = fetchFreePlanVerifiedBankAccount.buildACHData(bankAccount);
- expect(achData).toEqual({
- useOnfido: true,
- policyID: '',
- isInSetup: true,
- bankAccountInReview: false,
- domainLimit: 0,
- needsToUpgrade: false,
- subStep: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL,
- state: BankAccount.STATE.SETUP,
- validateCodeExpectedDate: '',
- });
- });
-
- it('Returns the correct shape for a verifying account', () => {
- const bankAccount = new BankAccount({
- state: BankAccount.STATE.VERIFYING,
- });
- const achData = fetchFreePlanVerifiedBankAccount.buildACHData(bankAccount);
- expect(achData).toEqual({
- useOnfido: true,
- policyID: '',
- isInSetup: false,
- bankAccountInReview: true,
- domainLimit: 0,
- needsToUpgrade: true,
- state: BankAccount.STATE.VERIFYING,
- validateCodeExpectedDate: '',
- });
- });
-
- it('Returns the correct shape for no account', () => {
- const bankAccount = undefined;
- const achData = fetchFreePlanVerifiedBankAccount.buildACHData(bankAccount);
- expect(achData).toEqual({
- useOnfido: true,
- policyID: '',
- isInSetup: true,
- bankAccountInReview: false,
- domainLimit: 0,
- });
- });
-});