diff --git a/.github/workflows/contributorChecklists.yml b/.github/workflows/contributorChecklists.yml index 692ba8944956..338ec6ba1e55 100644 --- a/.github/workflows/contributorChecklists.yml +++ b/.github/workflows/contributorChecklists.yml @@ -5,10 +5,10 @@ on: pull_request jobs: checklist: runs-on: ubuntu-latest - if: github.actor != 'OSBotify' && (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '- [')) + if: github.actor != 'OSBotify' steps: - name: contributorChecklist.js - uses: Expensify/App/.github/actions/javascript/contributorChecklist@andrew-checklist-3 + uses: Expensify/App/.github/actions/javascript/contributorChecklist@main with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CHECKLIST: 'contributor' diff --git a/.github/workflows/contributorPlusChecklists.yml b/.github/workflows/contributorPlusChecklists.yml index 5acffb511386..76dda02da067 100644 --- a/.github/workflows/contributorPlusChecklists.yml +++ b/.github/workflows/contributorPlusChecklists.yml @@ -1,14 +1,14 @@ name: Contributor+ Checklist -on: issue_comment +on: pull_request_review jobs: checklist: runs-on: ubuntu-latest - if: github.actor != 'OSBotify' && (contains(github.event.issue.pull_request.url, 'http') && github.event_name == 'issue_comment' && contains(github.event.comment.body, '- [')) + if: github.actor != 'OSBotify' steps: - name: contributorChecklist.js - uses: Expensify/App/.github/actions/javascript/contributorChecklist@andrew-checklist-3 + uses: Expensify/App/.github/actions/javascript/contributorChecklist@main with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CHECKLIST: 'contributorPlus' diff --git a/README.md b/README.md index c0f9e32884d9..fb3a256efd22 100644 --- a/README.md +++ b/README.md @@ -221,9 +221,9 @@ created to house a collection of items in plural form and using camelCase (eg: p - components: React native components that are re-used in several places. - libs: Library classes/functions, these are not React native components (ie: they are not UI) - pages: These are components that define pages in the app. The component that defines the page itself should be named -`Page` if there are components used only inside one page, they should live in its own directory named after the ``. +`Page` if there are components used only inside one page, they should live in its own directory named after the `` - styles: These files define styles used among components/pages -- contributingGuides: This is just a set of markdown files providing guides and insights to aid developers in learning how to contribute to this repo. +- contributingGuides: This is just a set of markdown files providing guides and insights to aid developers in learning how to contribute to this repo **Note:** There is also a directory called `/docs`, which houses the Expensify Help site. It's a static site that's built with Jekyll and hosted on GitHub Pages. diff --git a/android/app/build.gradle b/android/app/build.gradle index 7e22a8623b0e..04063749361a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001019707 - versionName "1.1.97-7" + versionCode 1001019904 + versionName "1.1.99-4" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 289ba96a2413..a19a074d3d49 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.97 + 1.1.99 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.97.7 + 1.1.99.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 11549d1d5412..890a0ad794b6 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.97 + 1.1.99 CFBundleSignature ???? CFBundleVersion - 1.1.97.7 + 1.1.99.4 diff --git a/package-lock.json b/package-lock.json index 50eaad994c61..b13378e833d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.1.97-7", + "version": "1.1.99-4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.1.97-7", + "version": "1.1.99-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 85672217086e..f9a67bfa8cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.97-7", + "version": "1.1.99-4", "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.", diff --git a/src/CONFIG.js b/src/CONFIG.js index a60b17e1e75b..4c03fa63c48e 100644 --- a/src/CONFIG.js +++ b/src/CONFIG.js @@ -9,6 +9,8 @@ import CONST from './CONST'; const ENVIRONMENT = lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV); const newExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/')); const expensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'EXPENSIFY_URL', 'https://www.expensify.com/')); +const stagingExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_EXPENSIFY_URL', 'https://staging.expensify.com/')); +const stagingSecureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_SECURE_EXPENSIFY_URL', 'https://staging-secure.expensify.com/')); const ngrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NGROK_URL', '')); const secureNgrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'SECURE_NGROK_URL', '')); const secureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet( @@ -46,12 +48,15 @@ export default { SECURE_EXPENSIFY_URL: secureURLRoot, NEW_EXPENSIFY_URL: newExpensifyURL, URL_API_ROOT: expensifyURLRoot, + STAGING_EXPENSIFY_URL: stagingExpensifyURL, + STAGING_SECURE_EXPENSIFY_URL: stagingSecureExpensifyUrl, PARTNER_NAME: lodashGet(Config, 'EXPENSIFY_PARTNER_NAME', 'chat-expensify-com'), PARTNER_PASSWORD: lodashGet(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'), EXPENSIFY_CASH_REFERER: 'ecash', CONCIERGE_URL: conciergeUrl, }, IS_IN_PRODUCTION: Platform.OS === 'web' ? process.env.NODE_ENV === 'production' : !__DEV__, + IS_IN_STAGING: ENVIRONMENT === CONST.ENVIRONMENT.STAGING, IS_USING_LOCAL_WEB: useNgrok || expensifyURLRoot.includes('dev'), PUSHER: { APP_KEY: lodashGet(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'), diff --git a/src/CONST.js b/src/CONST.js index 903f70bdaa56..a2dbc0ef9478 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -232,7 +232,6 @@ const CONST = { MANAGE_CARDS_URL: 'domain_companycards', FEES_URL: `${USE_EXPENSIFY_URL}/fees`, CFPB_PREPAID_URL: 'https://cfpb.gov/prepaid', - STAGING_SECURE_URL: 'https://staging-secure.expensify.com/', STAGING_NEW_EXPENSIFY_URL: 'https://staging.new.expensify.com', // Use Environment.getEnvironmentURL to get the complete URL with port number @@ -286,6 +285,9 @@ const CONST = { ANNOUNCE: '#announce', ADMINS: '#admins', }, + STATE: { + SUBMITTED: 'SUBMITTED', + }, STATE_NUM: { OPEN: 0, PROCESSING: 1, @@ -512,6 +514,7 @@ const CONST = { SVFG: 'svfg@expensify.com', INTEGRATION_TESTING_CREDS: 'integrationtestingcreds@expensify.com', ADMIN: 'admin@expensify.com', + GUIDES_DOMAIN: 'team.expensify.com', }, ENVIRONMENT: { @@ -667,6 +670,7 @@ const CONST = { FREE: 'free', PERSONAL: 'personal', CORPORATE: 'corporate', + TEAM: 'team', }, ROLE: { ADMIN: 'admin', diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js index 46cff9371656..84283915aba1 100644 --- a/src/components/ArrowKeyFocusManager.js +++ b/src/components/ArrowKeyFocusManager.js @@ -10,6 +10,9 @@ const propTypes = { PropTypes.node, ]).isRequired, + /** Array of disabled indexes. */ + disabledIndexes: PropTypes.arrayOf(PropTypes.number), + /** The current focused index. */ focusedIndex: PropTypes.number.isRequired, @@ -20,6 +23,10 @@ const propTypes = { onFocusedIndexChanged: PropTypes.func.isRequired, }; +const defaultProps = { + disabledIndexes: [], +}; + class ArrowKeyFocusManager extends Component { componentDidMount() { const arrowUpConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_UP; @@ -30,11 +37,14 @@ class ArrowKeyFocusManager extends Component { return; } - let newFocusedIndex = this.props.focusedIndex - 1; + const currentFocusedIndex = this.props.focusedIndex > 0 ? this.props.focusedIndex - 1 : this.props.maxIndex; + let newFocusedIndex = currentFocusedIndex; - // Wrap around to the bottom of the list - if (newFocusedIndex < 0) { - newFocusedIndex = this.props.maxIndex; + while (this.props.disabledIndexes.includes(newFocusedIndex)) { + newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : this.props.maxIndex; + if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled + return; // no-op + } } this.props.onFocusedIndexChanged(newFocusedIndex); @@ -45,11 +55,14 @@ class ArrowKeyFocusManager extends Component { return; } - let newFocusedIndex = this.props.focusedIndex + 1; + const currentFocusedIndex = this.props.focusedIndex < this.props.maxIndex ? this.props.focusedIndex + 1 : 0; + let newFocusedIndex = currentFocusedIndex; - // Wrap around to the top of the list - if (newFocusedIndex > this.props.maxIndex) { - newFocusedIndex = 0; + while (this.props.disabledIndexes.includes(newFocusedIndex)) { + newFocusedIndex = newFocusedIndex < this.props.maxIndex ? newFocusedIndex + 1 : 0; + if (newFocusedIndex === currentFocusedIndex) { // all indexes are disabled + return; // no-op + } } this.props.onFocusedIndexChanged(newFocusedIndex); @@ -72,5 +85,6 @@ class ArrowKeyFocusManager extends Component { } ArrowKeyFocusManager.propTypes = propTypes; +ArrowKeyFocusManager.defaultProps = defaultProps; export default ArrowKeyFocusManager; diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index f8c627adfd25..1bd517cfd642 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -12,6 +12,7 @@ import bankAccountPropTypes from './bankAccountPropTypes'; import cardPropTypes from './cardPropTypes'; import userWalletPropTypes from '../pages/EnablePayments/userWalletPropTypes'; import {policyPropTypes} from '../pages/workspace/withPolicy'; +import walletTermsPropTypes from '../pages/EnablePayments/walletTermsPropTypes'; import * as PolicyUtils from '../libs/PolicyUtils'; import * as PaymentMethods from '../libs/actions/PaymentMethods'; @@ -39,6 +40,9 @@ const propTypes = { /** The user's wallet (coming from Onyx) */ userWallet: userWalletPropTypes, + + /** Information about the user accepting the terms for payments */ + walletTerms: walletTermsPropTypes, }; const defaultProps = { @@ -49,6 +53,7 @@ const defaultProps = { bankAccountList: {}, cardList: {}, userWallet: {}, + walletTerms: {}, }; const AvatarWithIndicator = (props) => { @@ -73,6 +78,9 @@ const AvatarWithIndicator = (props) => { () => _.some(cleanPolicies, PolicyUtils.hasPolicyError), () => _.some(cleanPolicies, PolicyUtils.hasCustomUnitsError), () => _.some(cleanPolicyMembers, PolicyUtils.hasPolicyMemberError), + + // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) + () => !_.isEmpty(props.walletTerms.errors) && !props.walletTerms.chatReportID, ]; const shouldShowIndicator = _.some(errorCheckingMethods, errorCheckingMethod => errorCheckingMethod()); @@ -112,4 +120,7 @@ export default withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, })(AvatarWithIndicator); diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js index d8e1527189ad..a5f59ded8036 100644 --- a/src/components/KYCWall/BaseKYCWall.js +++ b/src/components/KYCWall/BaseKYCWall.js @@ -11,6 +11,7 @@ import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import ONYXKEYS from '../../ONYXKEYS'; import Log from '../../libs/Log'; import {propTypes, defaultProps} from './kycWallPropTypes'; +import * as Wallet from '../../libs/actions/Wallet'; // This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow // before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it @@ -35,6 +36,7 @@ class KYCWall extends React.Component { if (this.props.shouldListenForResize) { this.dimensionsSubscription = Dimensions.addEventListener('change', this.setMenuPosition); } + Wallet.setKYCWallSourceChatReportID(this.props.chatReportID); } componentWillUnmount() { diff --git a/src/components/KYCWall/kycWallPropTypes.js b/src/components/KYCWall/kycWallPropTypes.js index d2d5933fbc73..5886001c8c92 100644 --- a/src/components/KYCWall/kycWallPropTypes.js +++ b/src/components/KYCWall/kycWallPropTypes.js @@ -21,7 +21,10 @@ const propTypes = { isDisabled: PropTypes.bool, /** The user's wallet */ - userWallet: PropTypes.objectOf(userWalletPropTypes), + userWallet: userWalletPropTypes, + + /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ + chatReportID: PropTypes.number, }; const defaultProps = { @@ -29,6 +32,7 @@ const defaultProps = { popoverPlacement: 'top', shouldListenForResize: false, isDisabled: false, + chatReportID: 0, }; export {propTypes, defaultProps}; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index ec2588185de6..cbcb0faa415c 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -11,7 +11,7 @@ import Text from '../Text'; import compose from '../../libs/compose'; import CONST from '../../CONST'; import styles from '../../styles/styles'; -import withLocalize from '../withLocalize'; +import withLocalize, {withLocalizePropTypes} from '../withLocalize'; import TextInput from '../TextInput'; import ArrowKeyFocusManager from '../ArrowKeyFocusManager'; import KeyboardShortcut from '../../libs/KeyboardShortcut'; @@ -24,6 +24,7 @@ const propTypes = { shouldDelayFocus: PropTypes.bool, ...optionsSelectorPropTypes, + ...withLocalizePropTypes, }; const defaultProps = { @@ -144,6 +145,8 @@ class BaseOptionsSelector extends Component { */ flattenSections() { const allOptions = []; + this.disabledOptionsIndexes = []; + let index = 0; _.each(this.props.sections, (section, sectionIndex) => { _.each(section.data, (option, optionIndex) => { allOptions.push({ @@ -151,6 +154,10 @@ class BaseOptionsSelector extends Component { sectionIndex, index: optionIndex, }); + if (section.isDisabled || option.isDisabled) { + this.disabledOptionsIndexes.push(index); + } + index += 1; }); }); return allOptions; @@ -265,8 +272,9 @@ class BaseOptionsSelector extends Component { ) : ; return ( {} : this.updateFocusedIndex} > diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js index ece04dad7530..8187b608f7ed 100644 --- a/src/components/OptionsSelector/optionsSelectorPropTypes.js +++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import optionPropTypes from '../optionPropTypes'; -import {withLocalizePropTypes} from '../withLocalize'; import styles from '../../styles/styles'; const propTypes = { @@ -93,8 +92,6 @@ const propTypes = { /** Whether to show options list */ shouldShowOptions: PropTypes.bool, - - ...withLocalizePropTypes, }; const defaultProps = { diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js index b9d44dd53e67..ef40ce8be744 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/IOUPreview.js @@ -21,6 +21,9 @@ import Icon from '../Icon'; import CONST from '../../CONST'; import * as Expensicons from '../Icon/Expensicons'; import Text from '../Text'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import OfflineWithFeedback from '../OfflineWithFeedback'; +import walletTermsPropTypes from '../../pages/EnablePayments/walletTermsPropTypes'; const propTypes = { /** Additional logic for displaying the pay button */ @@ -75,6 +78,9 @@ const propTypes = { email: PropTypes.string, }).isRequired, + /** Information about the user accepting the terms for payments */ + walletTerms: walletTermsPropTypes, + ...withLocalizePropTypes, }; @@ -84,6 +90,7 @@ const defaultProps = { onPayButtonPressed: null, onPreviewPressed: () => {}, containerStyles: [], + walletTerms: {}, }; const IOUPreview = (props) => { @@ -124,10 +131,18 @@ const IOUPreview = (props) => { {reportIsLoading ? : ( - - - - + { + PaymentMethods.clearWalletTermsError(); + Report.clearIOUError(props.chatReportID); + }} + errorRowStyles={[styles.mbn1]} + > + + + {cachedTotal} @@ -137,48 +152,48 @@ const IOUPreview = (props) => { )} + + + - - - - - {isCurrentUserManager - ? ( - - {props.iouReport.hasOutstandingIOU - ? props.translate('iou.youowe', {owner: ownerName}) - : props.translate('iou.youpaid', {owner: ownerName})} - - ) - : ( - - {props.iouReport.hasOutstandingIOU - ? props.translate('iou.owesyou', {manager: managerName}) - : props.translate('iou.paidyou', {manager: managerName})} - - )} - {(isCurrentUserManager - && !props.shouldHidePayButton - && props.iouReport.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && ( - - + {props.iouReport.hasOutstandingIOU + ? props.translate('iou.youowe', {owner: ownerName}) + : props.translate('iou.youpaid', {owner: ownerName})} + + ) + : ( + + {props.iouReport.hasOutstandingIOU + ? props.translate('iou.owesyou', {manager: managerName}) + : props.translate('iou.paidyou', {manager: managerName})} + + )} + {(isCurrentUserManager + && !props.shouldHidePayButton + && props.iouReport.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && ( + - {props.translate('iou.pay')} - - - ))} - + + {props.translate('iou.pay')} + + + ))} + + )} @@ -201,5 +216,8 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, }), )(IOUPreview); diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 251a64d80c5c..316e356b51e3 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -27,12 +27,16 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, + /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ + chatReportID: PropTypes.number, + ...withLocalizePropTypes, }; const defaultProps = { currency: CONST.CURRENCY.USD, shouldShowPaypal: false, + chatReportID: 0, }; class SettlementButton extends React.Component { @@ -80,6 +84,7 @@ class SettlementButton extends React.Component { addBankAccountRoute={this.props.addBankAccountRoute} addDebitCardRoute={this.props.addDebitCardRoute} isDisabled={this.props.network.isOffline} + chatReportID={this.props.chatReportID} > {triggerKYCFlow => ( ( {/* Option to switch from using the staging secure endpoint or the production secure endpoint. This enables QA and internal testers to take advantage of sandbox environments for 3rd party services like Plaid and Onfido. */} - + User.setShouldUseSecureStaging(!props.user.shouldUseSecureStaging)} + isOn={lodashGet(props, 'user.shouldUseStagingServer', true)} + onToggle={() => User.setShouldUseStagingServer(!lodashGet(props, 'user.shouldUseStagingServer', true))} /> diff --git a/src/languages/en.js b/src/languages/en.js index 00d6f9e6f9d3..c70131508112 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -707,6 +707,8 @@ export default { activatedTitle: 'Wallet activated!', activatedMessage: 'Congrats, your wallet is set up and ready to make payments.', checkBackLater: 'We\'re still reviewing your information. Please check back later.', + continueToPayment: 'Continue to payment', + continueToTransfer: 'Continue to transfer', }, companyStep: { headerTitle: 'Company information', diff --git a/src/languages/es.js b/src/languages/es.js index 75554f7c91e8..68b50e078565 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -709,6 +709,8 @@ export default { activatedTitle: '¡Billetera activada!', activatedMessage: 'Felicidades, tu Billetera está configurada y lista para hacer pagos.', checkBackLater: 'Todavía estamos revisando tu información. Por favor, vuelva más tarde.', + continueToPayment: 'Continuar al pago', + continueToTransfer: 'Continuar a la transferencia', }, companyStep: { headerTitle: 'Información de la empresa', diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 3e93e318232e..4c8fe2ee6b30 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -1,14 +1,15 @@ import Onyx from 'react-native-onyx'; +import lodashGet from 'lodash/get'; import _ from 'underscore'; import CONFIG from '../CONFIG'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; import HttpsError from './Errors/HttpsError'; -let shouldUseSecureStaging = false; +let shouldUseStagingServer = false; Onyx.connect({ key: ONYXKEYS.USER, - callback: val => shouldUseSecureStaging = (val && _.isBoolean(val.shouldUseSecureStaging)) ? val.shouldUseSecureStaging : false, + callback: val => shouldUseStagingServer = lodashGet(val, 'shouldUseStagingServer', true), }); let shouldFailAllRequests = false; @@ -94,10 +95,11 @@ function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = formData.append(key, val); }); + let apiRoot = shouldUseSecure ? CONFIG.EXPENSIFY.SECURE_EXPENSIFY_URL : CONFIG.EXPENSIFY.URL_API_ROOT; - if (shouldUseSecure && shouldUseSecureStaging) { - apiRoot = CONST.STAGING_SECURE_URL; + if (CONFIG.IS_IN_STAGING && shouldUseStagingServer) { + apiRoot = shouldUseSecure ? CONFIG.EXPENSIFY.STAGING_SECURE_EXPENSIFY_URL : CONFIG.EXPENSIFY.STAGING_EXPENSIFY_URL; } return processHTTPRequest(`${apiRoot}api?command=${command}`, type, formData, data.canCancel); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 3454a6847847..a363dd84ba95 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -3,6 +3,7 @@ import Onyx, {withOnyx} from 'react-native-onyx'; import moment from 'moment'; import _ from 'underscore'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import * as StyleUtils from '../../../styles/StyleUtils'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import CONST from '../../../CONST'; @@ -86,6 +87,9 @@ const modalScreenListeners = { const propTypes = { ...windowDimensionsPropTypes, + + /** The current path as reported by the NavigationContainer */ + currentPath: PropTypes.string.isRequired, }; class AuthScreens extends React.Component { @@ -112,7 +116,6 @@ class AuthScreens extends React.Component { // Listen for report changes and fetch some data we need on initialization UnreadIndicatorUpdater.listenForReportChanges(); App.openApp(); - App.setUpPoliciesAndNavigate(this.props.session); Timing.end(CONST.TIMING.HOMEPAGE_INITIAL_RENDER); const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; @@ -130,6 +133,10 @@ class AuthScreens extends React.Component { } shouldComponentUpdate(nextProps) { + // we perform this check here instead of componentDidUpdate to skip an unnecessary re-render + if (this.props.currentPath !== nextProps.currentPath) { + App.setUpPoliciesAndNavigate(nextProps.session, nextProps.currentPath); + } return nextProps.isSmallScreenWidth !== this.props.isSmallScreenWidth; } diff --git a/src/libs/Navigation/AppNavigator/index.js b/src/libs/Navigation/AppNavigator/index.js index 6a7b910ebaef..b1f53844dcb5 100644 --- a/src/libs/Navigation/AppNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/index.js @@ -6,6 +6,9 @@ import AuthScreens from './AuthScreens'; const propTypes = { /** If we have an authToken this is true */ authenticated: PropTypes.bool.isRequired, + + /** The current path as reported by the NavigationContainer */ + currentPath: PropTypes.string.isRequired, }; const AppNavigator = props => ( @@ -13,7 +16,7 @@ const AppNavigator = props => ( ? ( // These are the protected screens and only accessible when an authToken is present - + ) : ( diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 4002c0101c35..57fd9ccd0305 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -28,6 +28,16 @@ const propTypes = { }; class NavigationRoot extends Component { + constructor(props) { + super(props); + + this.state = { + currentPath: '', + }; + + this.parseAndLogRoute = this.parseAndLogRoute.bind(this); + } + /** * Intercept navigation state changes and log it * @param {NavigationState} state @@ -47,6 +57,8 @@ class NavigationRoot extends Component { } UnreadIndicatorUpdater.throttledUpdatePageTitleAndUnreadCount(); + + this.setState({currentPath}); } render() { @@ -67,7 +79,7 @@ class NavigationRoot extends Component { enabled: false, }} > - + ); } diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 186e54ef8e49..c54690403d85 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -443,8 +443,14 @@ function getOptions(reports, personalDetails, activeReportID, { return; } - // We let Free Plan default rooms to be shown in the App - it's the one exception to the beta, otherwise do not show policy rooms in product - if (ReportUtils.isDefaultRoom(report) && !Permissions.canUseDefaultRooms(betas) && ReportUtils.getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE) { + // We create policy rooms for all policies, however we don't show them unless + // - It's a free plan workspace + // - The report includes guides participants (@team.expensify.com) for 1:1 Assigned + if (!Permissions.canUseDefaultRooms(betas) + && ReportUtils.isDefaultRoom(report) + && ReportUtils.getPolicyType(report, policies) !== CONST.POLICY.TYPE.FREE + && !ReportUtils.hasExpensifyGuidesEmails(logins) + ) { return; } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 8fd7ec3af880..e48939c5e7e9 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -12,6 +12,7 @@ import md5 from './md5'; import Navigation from './Navigation/Navigation'; import ROUTES from '../ROUTES'; import * as NumberUtils from './NumberUtils'; +import * as NumberFormatUtils from './NumberFormatUtils'; let sessionEmail; Onyx.connect({ @@ -203,6 +204,15 @@ function getPolicyType(report, policies) { return lodashGet(policies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'type'], ''); } +/** + * Returns true if there are any guides accounts (team.expensify.com) in emails + * @param {Array} emails + * @returns {Boolean} + */ +function hasExpensifyGuidesEmails(emails) { + return _.some(emails, email => Str.extractEmailDomain(email) === CONST.EMAIL.GUIDES_DOMAIN); +} + /** * Given a collection of reports returns the most recently accessed one * @@ -215,7 +225,9 @@ function findLastAccessedReport(reports, ignoreDefaultRooms, policies) { let sortedReports = sortReportsByLastVisited(reports); if (ignoreDefaultRooms) { - sortedReports = _.filter(sortedReports, report => !isDefaultRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE); + sortedReports = _.filter(sortedReports, report => !isDefaultRoom(report) + || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE + || hasExpensifyGuidesEmails(lodashGet(report, ['participants'], []))); } return _.last(sortedReports); @@ -560,6 +572,29 @@ function hasReportNameError(report) { return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {})); } +/* + * Builds an optimistic IOU report with a randomly generated reportID + */ +function buildOptimisticIOUReport(ownerEmail, recipientEmail, total, chatReportID, currency, locale) { + const formattedTotal = NumberFormatUtils.format(locale, + total, { + style: 'currency', + currency, + }); + return { + cachedTotal: formattedTotal, + chatReportID, + currency, + hasOutstandingIOU: true, + managerEmail: recipientEmail, + ownerEmail, + reportID: generateReportID(), + state: CONST.REPORT.STATE.SUBMITTED, + stateNum: 1, + total, + }; +} + /** * Builds an optimistic IOU reportAction object * @@ -648,6 +683,7 @@ export { isArchivedRoom, isConciergeChatReport, hasExpensifyEmails, + hasExpensifyGuidesEmails, canShowReportRecipientLocalTime, formatReportLastMessageText, chatIncludesConcierge, @@ -660,6 +696,7 @@ export { navigateToDetailsPage, generateReportID, hasReportNameError, + buildOptimisticIOUReport, buildOptimisticIOUReportAction, isUnread, }; diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index 9d648b489815..dceb2e87243d 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -321,6 +321,13 @@ function clearWalletError() { Onyx.merge(ONYXKEYS.USER_WALLET, {errors: null}); } +/** + * Clear any error(s) related to the user's wallet terms + */ +function clearWalletTermsError() { + Onyx.merge(ONYXKEYS.WALLET_TERMS, {errors: null}); +} + function deletePaymentCard(fundID) { API.write('DeletePaymentCard', { fundID, @@ -355,4 +362,5 @@ export { clearDeletePaymentMethodError, clearAddPaymentMethodError, clearWalletError, + clearWalletTermsError, }; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 64f190168720..7755bdbd6a9e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -129,7 +129,8 @@ function deletePolicy(policyID) { Growl.show(Localize.translateLocal('workspace.common.growlMessageOnDelete'), CONST.GROWL.SUCCESS, 3000); - // Removing the workspace data from Onyx as well + // Removing the workspace data from Onyx and local array as well + delete allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, null); }) .then(() => Report.fetchAllReports(false)) @@ -810,7 +811,7 @@ function createWorkspace() { expenseChatReportID, expenseChatData, expenseReportActionData, - } = Report.createOptimisticWorkspaceChats(policyID, workspaceName); + } = Report.buildOptimisticWorkspaceChats(policyID, workspaceName); // We need to use makeRequestWithSideEffects as we try to redirect to the policy right after creation // The policy hasn't been merged in Onyx data at this point, leading to an intermittent Not Found screen diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 105ca3964226..3d9d75cda781 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -585,7 +585,7 @@ function fetchAllReports( } /** - * Creates an optimistic chat report with a randomly generated reportID and as much information as we currently have + * Builds an optimistic chat report with a randomly generated reportID and as much information as we currently have * * @param {Array} participantList * @param {String} reportName @@ -596,7 +596,7 @@ function fetchAllReports( * @param {String} oldPolicyName * @returns {Object} */ -function createOptimisticChatReport( +function buildOptimisticChatReport( participantList, reportName = 'Chat Report', chatType = '', @@ -635,7 +635,7 @@ function createOptimisticChatReport( * @param {String} ownerEmail * @returns {Object} */ -function createOptimisticCreatedReportAction(ownerEmail) { +function buildOptimisticCreatedReportAction(ownerEmail) { return { 0: { actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -675,8 +675,8 @@ function createOptimisticCreatedReportAction(ownerEmail) { * @param {String} policyName * @returns {Object} */ -function createOptimisticWorkspaceChats(policyID, policyName) { - const announceChatData = createOptimisticChatReport( +function buildOptimisticWorkspaceChats(policyID, policyName) { + const announceChatData = buildOptimisticChatReport( [currentUserEmail], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, @@ -686,15 +686,15 @@ function createOptimisticWorkspaceChats(policyID, policyName) { policyName, ); const announceChatReportID = announceChatData.reportID; - const announceReportActionData = createOptimisticCreatedReportAction(announceChatData.ownerEmail); + const announceReportActionData = buildOptimisticCreatedReportAction(announceChatData.ownerEmail); - const adminsChatData = createOptimisticChatReport([currentUserEmail], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, null, false, policyName); + const adminsChatData = buildOptimisticChatReport([currentUserEmail], CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, policyID, null, false, policyName); const adminsChatReportID = adminsChatData.reportID; - const adminsReportActionData = createOptimisticCreatedReportAction(adminsChatData.ownerEmail); + const adminsReportActionData = buildOptimisticCreatedReportAction(adminsChatData.ownerEmail); - const expenseChatData = createOptimisticChatReport([currentUserEmail], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserEmail, true, policyName); + const expenseChatData = buildOptimisticChatReport([currentUserEmail], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, currentUserEmail, true, policyName); const expenseChatReportID = expenseChatData.reportID; - const expenseReportActionData = createOptimisticCreatedReportAction(expenseChatData.ownerEmail); + const expenseReportActionData = buildOptimisticCreatedReportAction(expenseChatData.ownerEmail); return { announceChatReportID, @@ -1559,6 +1559,15 @@ function viewNewReportAction(reportID, action) { }); } +/** + * Clear the errors associated with the IOUs of a given report. + * + * @param {Number} reportID + */ +function clearIOUError(reportID) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {errorFields: {iou: null}}); +} + // We are using this map to ensure actions are only handled once const handledReportActions = {}; Onyx.connect({ @@ -1631,9 +1640,10 @@ export { readOldestAction, openReport, openPaymentDetailsPage, - createOptimisticWorkspaceChats, - createOptimisticChatReport, - createOptimisticCreatedReportAction, + buildOptimisticWorkspaceChats, + buildOptimisticChatReport, + buildOptimisticCreatedReportAction, updatePolicyRoomName, clearPolicyRoomNameErrors, + clearIOUError, }; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index a7b71d9fd3b5..0d61d0ae0330 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -426,10 +426,10 @@ function updateChatPriorityMode(mode) { } /** - * @param {Boolean} shouldUseSecureStaging + * @param {Boolean} shouldUseStagingServer */ -function setShouldUseSecureStaging(shouldUseSecureStaging) { - Onyx.merge(ONYXKEYS.USER, {shouldUseSecureStaging}); +function setShouldUseStagingServer(shouldUseStagingServer) { + Onyx.merge(ONYXKEYS.USER, {shouldUseStagingServer}); } function clearUserErrorMessage() { @@ -484,7 +484,7 @@ export { isBlockedFromConcierge, subscribeToUserEvents, updatePreferredSkinTone, - setShouldUseSecureStaging, + setShouldUseStagingServer, clearUserErrorMessage, subscribeToExpensifyCardUpdates, updateFrequentlyUsedEmojis, diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 838f2e3b06ab..62e4a5da7592 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -96,6 +96,15 @@ function setWalletShouldShowFailedKYC(shouldShowFailedKYC) { Onyx.merge(ONYXKEYS.USER_WALLET, {shouldShowFailedKYC}); } +/** + * Save the ID of the chat whose IOU triggered showing the KYC wall. + * + * @param {Number} chatReportID + */ +function setKYCWallSourceChatReportID(chatReportID) { + Onyx.merge(ONYXKEYS.WALLET_TERMS, {chatReportID}); +} + /** * Transforms a list of Idology errors to a translated displayable error string. * @param {Array} idologyErrors @@ -452,6 +461,7 @@ function verifyIdentity(parameters) { * * @param {Object} parameters * @param {Boolean} parameters.hasAcceptedTerms + * @param {Number} parameters.chatReportID When accepting the terms of wallet to pay an IOU, indicates the parent chat ID of the IOU */ function acceptWalletTerms(parameters) { const optimisticData = [ @@ -485,7 +495,7 @@ function acceptWalletTerms(parameters) { }, ]; - API.write('AcceptWalletTerms', {hasAcceptedTerms: parameters.hasAcceptedTerms}, {optimisticData, successData, failureData}); + API.write('AcceptWalletTerms', {hasAcceptedTerms: parameters.hasAcceptedTerms, reportID: parameters.chatReportID}, {optimisticData, successData, failureData}); } /** @@ -542,4 +552,5 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, + setKYCWallSourceChatReportID, }; diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index a84eccb01108..24cb7370a48a 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -3,6 +3,7 @@ import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; import styles from '../styles/styles'; import Text from '../components/Text'; import ONYXKEYS from '../ONYXKEYS'; @@ -65,132 +66,142 @@ const getPhoneNumber = (details) => { return Str.removeSMSDomain(details.login); }; -const DetailsPage = (props) => { - const details = props.personalDetails[props.route.params.login]; - if (!details) { - // Personal details have not loaded yet - return ; +class DetailsPage extends React.PureComponent { + componentDidMount() { + if (lodashGet(this.props.route.params, 'login')) { + return; + } + + // Leave the page when the login information is not available + Navigation.dismissModal(); } - const isSMSLogin = Str.isSMSLogin(details.login); - // If we have a reportID param this means that we - // arrived here via the ParticipantsPage and should be allowed to navigate back to it - const shouldShowBackButton = Boolean(props.route.params.reportID); - const timezone = DateUtils.getLocalMomentFromTimestamp(props.preferredLocale, null, details.timezone.selected); - const GMTTime = `${timezone.toString().split(/[+-]/)[0].slice(-3)} ${timezone.zoneAbbr()}`; - const currentTime = Number.isNaN(Number(timezone.zoneAbbr())) ? timezone.zoneAbbr() : GMTTime; - const shouldShowLocalTime = !ReportUtils.hasExpensifyEmails([details.login]); + render() { + const details = this.props.personalDetails[lodashGet(this.props.route.params, 'login')]; + if (!details) { + // Personal details have not loaded yet + return ; + } + const isSMSLogin = Str.isSMSLogin(details.login); - let pronouns = details.pronouns; + // If we have a reportID param this means that we + // arrived here via the ParticipantsPage and should be allowed to navigate back to it + const shouldShowBackButton = Boolean(this.props.route.params.reportID); + const timezone = DateUtils.getLocalMomentFromTimestamp(this.props.preferredLocale, null, details.timezone.selected); + const GMTTime = `${timezone.toString().split(/[+-]/)[0].slice(-3)} ${timezone.zoneAbbr()}`; + const currentTime = Number.isNaN(Number(timezone.zoneAbbr())) ? timezone.zoneAbbr() : GMTTime; + const shouldShowLocalTime = !ReportUtils.hasExpensifyEmails([details.login]); - if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { - const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = props.translate(`pronouns.${localeKey}`); - } + let pronouns = details.pronouns; + + if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { + const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); + pronouns = this.props.translate(`pronouns.${localeKey}`); + } - return ( - - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - {details ? ( - - - - {({show}) => ( - - - + return ( + + Navigation.goBack()} + onCloseButtonPress={() => Navigation.dismissModal()} + /> + + {details ? ( + + + + {({show}) => ( + + + + )} + + {details.displayName && ( + + {isSMSLogin ? this.props.toLocalPhone(details.displayName) : details.displayName} + )} - - {details.displayName && ( - - {isSMSLogin ? props.toLocalPhone(details.displayName) : details.displayName} - + {details.login ? ( + + + {this.props.translate(isSMSLogin + ? 'common.phoneNumber' + : 'common.email')} + + + + + {isSMSLogin + ? this.props.toLocalPhone(getPhoneNumber(details)) + : details.login} + + + + + ) : null} + {pronouns ? ( + + + {this.props.translate('profilePage.preferredPronouns')} + + + {pronouns} + + + ) : null} + {shouldShowLocalTime && details.timezone ? ( + + + {this.props.translate('detailsPage.localTime')} + + + {timezone.format('LT')} + {' '} + {currentTime} + + + ) : null} + + {details.login !== this.props.session.email && ( + Report.fetchOrCreateChatReport([this.props.session.email, details.login])} + wrapperStyle={styles.breakAll} + shouldShowRightIcon + /> )} - {details.login ? ( - - - {props.translate(isSMSLogin - ? 'common.phoneNumber' - : 'common.email')} - - - - - {isSMSLogin - ? props.toLocalPhone(getPhoneNumber(details)) - : details.login} - - - - - ) : null} - {pronouns ? ( - - - {props.translate('profilePage.preferredPronouns')} - - - {pronouns} - - - ) : null} - {shouldShowLocalTime && details.timezone ? ( - - - {props.translate('detailsPage.localTime')} - - - {timezone.format('LT')} - {' '} - {currentTime} - - - ) : null} - - {details.login !== props.session.email && ( - Report.fetchOrCreateChatReport([props.session.email, details.login])} - wrapperStyle={styles.breakAll} - shouldShowRightIcon - /> - )} - - ) : null} - - - ); -}; + + ) : null} + + + ); + } +} DetailsPage.propTypes = propTypes; -DetailsPage.displayName = 'DetailsPage'; export default compose( withLocalize, diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js index 94454e8ce78a..5178e7ef5b03 100644 --- a/src/pages/EnablePayments/ActivateStep.js +++ b/src/pages/EnablePayments/ActivateStep.js @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; @@ -15,16 +15,25 @@ import defaultTheme from '../../styles/themes/default'; import FixedFooter from '../../components/FixedFooter'; import Button from '../../components/Button'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import walletTermsPropTypes from './walletTermsPropTypes'; const propTypes = { ...withLocalizePropTypes, /** The user's wallet */ - userWallet: PropTypes.objectOf(userWalletPropTypes), + userWallet: userWalletPropTypes, + + /** Information about the user accepting the terms for payments */ + walletTerms: walletTermsPropTypes, }; const defaultProps = { userWallet: {}, + walletTerms: { + chatReportID: 0, + }, }; class ActivateStep extends React.Component { @@ -35,6 +44,8 @@ class ActivateStep extends React.Component { } renderGoldWalletActivationStep() { + // The text of the "Continue" button depends on whether the action comes from an IOU (i.e. with an attached chat), or a balance transfer + const continueButtonText = this.props.walletTerms.chatReportID ? this.props.translate('activateStep.continueToPayment') : this.props.translate('activateStep.continueToTransfer'); return ( <> @@ -55,7 +66,7 @@ class ActivateStep extends React.Component {