From cff45ee5e15adcd382908a4276a490395d1db5df Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 13:20:40 +0800 Subject: [PATCH 001/337] fix iou CREATED and transaction action created time is the same --- src/libs/ReportUtils.js | 9 ++++++--- src/libs/actions/IOU.js | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 3c4944ef1a5e..8239634b2a8b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2067,6 +2067,7 @@ function getIOUReportActionMessage(iouReportID, type, total, comment, currency, * @param {Boolean} [isSendMoneyFlow] - Whether this is send money flow * @param {Object} [receipt] * @param {Boolean} [isOwnPolicyExpenseChat] - Whether this is an expense report create from the current user's policy expense chat + * @param {String} [created] - Action created time * @returns {Object} */ function buildOptimisticIOUReportAction( @@ -2082,6 +2083,7 @@ function buildOptimisticIOUReportAction( isSendMoneyFlow = false, receipt = {}, isOwnPolicyExpenseChat = false, + created = DateUtils.getDBTime(), ) { const IOUReportID = iouReportID || generateReportID(); @@ -2139,7 +2141,7 @@ function buildOptimisticIOUReportAction( ], reportActionID: NumberUtils.rand64(), shouldShow: true, - created: DateUtils.getDBTime(), + created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, whisperedToAccountIDs: !_.isEmpty(receipt) ? [currentUserAccountID] : [], @@ -2400,9 +2402,10 @@ function buildOptimisticChatReport( /** * Returns the necessary reportAction onyx data to indicate that the chat has been created optimistically * @param {String} emailCreatingAction + * @param {String} [created] - Action created time * @returns {Object} */ -function buildOptimisticCreatedReportAction(emailCreatingAction) { +function buildOptimisticCreatedReportAction(emailCreatingAction, created = DateUtils.getDBTime()) { return { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, @@ -2429,7 +2432,7 @@ function buildOptimisticCreatedReportAction(emailCreatingAction) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatar(currentUserAccountID)), - created: DateUtils.getDBTime(), + created, shouldShow: true, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 5b79fb6ad4bb..7ea1fbd1dff6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -459,8 +459,9 @@ function getMoneyRequestInformation( // 3. IOU action for the iouReport // 4. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat + const currentTime = DateUtils.getDBTime(); const optimisticCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); - const optimisticCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + const optimisticCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail, DateUtils.subtractMillisecondsFromDateTime(currentTime, 1)); const iouAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount, @@ -473,6 +474,8 @@ function getMoneyRequestInformation( false, false, receiptObject, + false, + currentTime, ); let reportPreviewAction = isNewIOUReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID); From d7a37cd79e82e263baf58e1053cdd3aec79fee5f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 13:32:46 +0800 Subject: [PATCH 002/337] fix iou CREATED and transaction action created time is the same --- src/libs/actions/IOU.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ea1fbd1dff6..a0787cfaca84 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -875,8 +875,9 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco // 3. IOU action for the iouReport // 4. REPORTPREVIEW action for the chatReport // Note: The CREATED action for the IOU report must be optimistically generated before the IOU action so there's no chance that it appears after the IOU action in the chat + const currentTime = DateUtils.getDBTime(); const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); - const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit); + const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit, DateUtils.subtractMillisecondsFromDateTime(currentTime, 1)); const oneOnOneIOUAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, @@ -886,6 +887,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco oneOnOneTransaction.transactionID, '', oneOnOneIOUReport.reportID, + undefined, + undefined, + undefined, + undefined, + currentTime, ); // Add optimistic personal details for new participants From 702c960fb6160656aa20eeb563a1392942660461 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 14:20:29 +0800 Subject: [PATCH 003/337] update test --- tests/actions/IOUTest.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index afb06cdb6fb3..902b669b287a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -92,7 +92,7 @@ describe('actions/IOU', () => { iouAction = iouActions[0]; // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(createdAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -199,6 +199,7 @@ describe('actions/IOU', () => { }; let iouReportID; let iouAction; + let iouCreatedAction; let transactionID; fetch.pause(); return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) @@ -247,10 +248,11 @@ describe('actions/IOU', () => { // The chat report should have a CREATED and an IOU action expect(_.size(allIOUReportActions)).toBe(2); + iouCreatedAction = _.find(allIOUReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); iouAction = _.find(allIOUReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(iouCreatedAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -582,7 +584,7 @@ describe('actions/IOU', () => { iouAction = iouActions[0]; // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + expect(Date.parse(createdAction.created)).toBeLessThan(Date.parse(iouAction.created)); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -994,17 +996,19 @@ describe('actions/IOU', () => { // Carlos DM should have two reportActions – the existing CREATED action and a pending IOU action expect(_.size(carlosReportActions)).toBe(2); + carlosIOUCreatedAction = _.find(carlosReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); carlosIOUAction = _.find(carlosReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(carlosIOUAction.originalMessage.IOUReportID).toBe(carlosIOUReport.reportID); expect(carlosIOUAction.originalMessage.amount).toBe(amount / 4); expect(carlosIOUAction.originalMessage.comment).toBe(comment); expect(carlosIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(carlosCreatedAction.created)).toBeLessThanOrEqual(Date.parse(carlosIOUAction.created)); + expect(Date.parse(carlosIOUCreatedAction.created)).toBeLessThan(Date.parse(carlosIOUAction.created)); // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action expect(_.size(julesReportActions)).toBe(3); expect(julesReportActions[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); + julesIOUCreatedAction = _.find(julesReportActions, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); julesIOUAction = _.find( julesReportActions, (reportAction) => @@ -1015,7 +1019,7 @@ describe('actions/IOU', () => { expect(julesIOUAction.originalMessage.amount).toBe(amount / 4); expect(julesIOUAction.originalMessage.comment).toBe(comment); expect(julesIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(julesCreatedAction.created)).toBeLessThanOrEqual(Date.parse(julesIOUAction.created)); + expect(Date.parse(julesIOUCreatedAction.created)).toBeLessThan(Date.parse(julesIOUAction.created)); // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action expect(_.size(vitReportActions)).toBe(2); @@ -1027,7 +1031,7 @@ describe('actions/IOU', () => { expect(vitIOUAction.originalMessage.amount).toBe(amount / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)); + expect(Date.parse(vitCreatedAction.created)).toBeLessThan(Date.parse(vitIOUAction.created)); // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT expect(_.size(groupReportActions)).toBe(2); From 9548e2a531605a72cc14ed6f39569032525c37bc Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 13 Sep 2023 14:26:41 +0800 Subject: [PATCH 004/337] fix var is not defined --- tests/actions/IOUTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 902b669b287a..683865a4058a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -851,9 +851,11 @@ describe('actions/IOU', () => { let carlosIOUReport; let carlosIOUAction; + let carlosIOUCreatedAction; let carlosTransaction; let julesIOUAction; + let julesIOUCreatedAction; let julesTransaction; let vitChatReport; From 70a4a14ed46fe07a66bddb5de76f52e7c816815a Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 14 Sep 2023 18:15:11 +0700 Subject: [PATCH 005/337] fix: warning - non-passive event listener --- src/components/Composer/index.js | 6 +++-- .../HTMLRenderers/PreRenderer/index.js | 6 +++-- .../hasPassiveEventListenerSupport/index.js | 22 +++++++++++++++++++ .../index.native.js | 10 +++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js create mode 100644 src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 44075a4ec1eb..9ab32b549917 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -19,6 +19,7 @@ import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileCom import CONST from '../../CONST'; import withNavigation from '../withNavigation'; import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; +import hasPassiveEventListenerSupport from '../../libs/DeviceCapabilities/hasPassiveEventListenerSupport'; const propTypes = { /** Maximum number of lines in the text input */ @@ -140,6 +141,8 @@ const getNextChars = (str, cursorPos) => { return substr.substring(0, spaceIndex); }; +const supportsPassive = hasPassiveEventListenerSupport(); + // Enable Markdown parsing. // On web we like to have the Text Input field always focused so the user can easily type a new chat function Composer({ @@ -339,7 +342,6 @@ function Composer({ } textInput.current.scrollTop += event.deltaY; - event.preventDefault(); event.stopPropagation(); }, []); @@ -384,7 +386,7 @@ function Composer({ if (textInput.current) { document.addEventListener('paste', handlePaste); - textInput.current.addEventListener('wheel', handleWheel); + textInput.current.addEventListener('wheel', handleWheel, supportsPassive ? {passive: true} : false); } return () => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js index efc9e432cba8..c9e6796902bd 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/index.js @@ -5,6 +5,9 @@ import htmlRendererPropTypes from '../htmlRendererPropTypes'; import BasePreRenderer from './BasePreRenderer'; import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import ControlSelection from '../../../../libs/ControlSelection'; +import hasPassiveEventListenerSupport from '../../../../libs/DeviceCapabilities/hasPassiveEventListenerSupport'; + +const supportsPassive = hasPassiveEventListenerSupport(); class PreRenderer extends React.Component { constructor(props) { @@ -18,7 +21,7 @@ class PreRenderer extends React.Component { if (!this.ref) { return; } - this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode); + this.ref.getScrollableNode().addEventListener('wheel', this.scrollNode, supportsPassive ? {passive: true} : false); } componentWillUnmount() { @@ -47,7 +50,6 @@ class PreRenderer extends React.Component { const isScrollingVertically = this.debouncedIsScrollingVertically(event); if (event.currentTarget === node && horizontalOverflow && !isScrollingVertically) { node.scrollLeft += event.deltaX; - event.preventDefault(); event.stopPropagation(); } } diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js new file mode 100644 index 000000000000..3f836b951abc --- /dev/null +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js @@ -0,0 +1,22 @@ +/** + * Allows us to identify whether the browser supports passive event listener. + * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. + * + * @returns {Boolean} + */ + +export default function hasPassiveEventListenerSupport() { + let supportsPassive = false; + try { + const opts = Object.defineProperty({}, 'passive', { + // eslint-disable-next-line getter-return + get() { + supportsPassive = true; + }, + }); + window.addEventListener('testPassive', null, opts); + window.removeEventListener('testPassive', null, opts); + // eslint-disable-next-line no-empty + } catch (e) {} + return supportsPassive; +} diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js new file mode 100644 index 000000000000..0e1bb5616040 --- /dev/null +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js @@ -0,0 +1,10 @@ +/** + * Allows us to identify whether the browser supports passive event listener. + * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. + * + * @returns {Boolean} + */ + +const hasPassiveEventListenerSupport = () => false; + +export default hasPassiveEventListenerSupport; From c6fd83e30562c2e5708679754935b5a448504c28 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 18 Sep 2023 12:17:53 +0700 Subject: [PATCH 006/337] update comment --- .../DeviceCapabilities/hasPassiveEventListenerSupport/index.js | 1 - .../hasPassiveEventListenerSupport/index.native.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js index 3f836b951abc..fdd329451321 100644 --- a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.js @@ -1,6 +1,5 @@ /** * Allows us to identify whether the browser supports passive event listener. - * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. * * @returns {Boolean} */ diff --git a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js index 0e1bb5616040..555d34063ea7 100644 --- a/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js +++ b/src/libs/DeviceCapabilities/hasPassiveEventListenerSupport/index.native.js @@ -1,6 +1,5 @@ /** * Allows us to identify whether the browser supports passive event listener. - * Because older browsers will interpret any object in the 3rd argument of an event listener as capture=true. * * @returns {Boolean} */ From e608638391f557facb96497e942266de89461daf Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 20 Sep 2023 23:32:53 +0200 Subject: [PATCH 007/337] Migrate policy utils lib --- src/libs/{PolicyUtils.js => PolicyUtils.ts} | 116 +++++++------------- src/types/onyx/Policy.ts | 1 + 2 files changed, 41 insertions(+), 76 deletions(-) rename src/libs/{PolicyUtils.js => PolicyUtils.ts} (50%) diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.ts similarity index 50% rename from src/libs/PolicyUtils.js rename to src/libs/PolicyUtils.ts index 164f284a4ef5..1f2abfa2b7a8 100644 --- a/src/libs/PolicyUtils.js +++ b/src/libs/PolicyUtils.ts @@ -1,72 +1,56 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; +import * as OnyxTypes from '../types/onyx'; + +type PolicyMemberList = Record; +type PolicyMembersCollection = Record; +type MemberEmailsToAccountIDs = Record; +type PersonalDetailsList = Record; /** * Filter out the active policies, which will exclude policies with pending deletion - * @param {Object} policies - * @returns {Array} */ -function getActivePolicies(policies) { - return _.filter(policies, (policy) => policy && policy.isPolicyExpenseChatEnabled && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { + return policies.filter((policy) => policy?.isPolicyExpenseChatEnabled && policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} - * - * @param {Object} policyMembers - * @returns {Boolean} */ -function hasPolicyMemberError(policyMembers) { - return _.some(policyMembers, (member) => !_.isEmpty(member.errors)); +function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { + return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyErrorFields(policy) { - return _.some(lodashGet(policy, 'errorFields', {}), (fieldErrors) => !_.isEmpty(fieldErrors)); +function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. - * - * @param {Object} policy - * @param {Object} policy.errors - * @param {Object} policy.errorFields - * @return {Boolean} */ -function hasPolicyError(policy) { - return !_.isEmpty(lodashGet(policy, 'errors', {})) ? true : hasPolicyErrorFields(policy); +function hasPolicyError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. - * - * @param {Object} policy - * @returns {Boolean} */ -function hasCustomUnitsError(policy) { - return !_.isEmpty(_.pick(lodashGet(policy, 'customUnits', {}), 'errors')); +function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { + return Object.keys(policy?.customUnits?.error ?? {}).length > 0; } /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. - * - * @param {Object} policy - * @param {String} policy.id - * @param {Object} policyMembersCollection - * @returns {String} */ -function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { - const policyMembers = lodashGet(policyMembersCollection, `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`, {}); +function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { + if (!(`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}` in policyMembersCollection)) return ''; + + const policyMembers = policyMembersCollection[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`]; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -79,84 +63,64 @@ function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { * If online, show the policy pending deletion only if there is an error. * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. - * @param {Object} policy - * @param {Boolean} isOffline - * @returns {Boolean} */ -function shouldShowPolicy(policy, isOffline) { +function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policy.errors)) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy?.errors).length > 0) ); } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyTeam(email) { +function isExpensifyTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } -/** - * @param {string} email - * @returns {boolean} - */ -function isExpensifyGuideTeam(email) { +function isExpensifyGuideTeam(email: string): boolean { const emailDomain = Str.extractEmailDomain(email); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } /** * Checks if the current user is an admin of the policy. - * - * @param {Object} policy - * @returns {Boolean} */ -const isPolicyAdmin = (policy) => lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Object} - * * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) { - const memberEmailsToAccountIDs = {}; - _.each(policyMembers, (member, accountID) => { - if (!_.isEmpty(member.errors)) { +function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; + Object.keys(policyMembers).forEach((accountID) => { + const member = policyMembers[accountID]; + if (Object.keys(member?.errors ?? {}).length > 0) { return; } const personalDetail = personalDetails[accountID]; - if (!personalDetail || !personalDetail.login) { + if (!personalDetail?.login) { return; } - memberEmailsToAccountIDs[personalDetail.login] = accountID; + memberEmailsToAccountIDs[personalDetail?.login] = accountID; }); return memberEmailsToAccountIDs; } /** * Get login list that we should not show in the workspace invite options - * - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Array} */ -function getIneligibleInvitees(policyMembers, personalDetails) { - const memberEmailsToExclude = [...CONST.EXPENSIFY_EMAILS]; - _.each(policyMembers, (policyMember, accountID) => { +function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { + const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; + Object.keys(policyMembers).forEach((accountID) => { + const policyMember = policyMembers[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policyMember.errors)) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = lodashGet(personalDetails, `[${accountID}].login`); + const memberEmail = personalDetails[accountID]?.login; if (!memberEmail) { return; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index cacbb5d15199..10ee569c93e6 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -32,6 +32,7 @@ type Policy = { isFromFullPolicy?: boolean; lastModified?: string; customUnits?: Record; + isPolicyExpenseChatEnabled: boolean; }; export default Policy; From 478559309312a717b3cb7e14f85187123f2f2608 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Wed, 27 Sep 2023 11:35:25 +0800 Subject: [PATCH 008/337] Remove the categories beta --- src/CONST.ts | 1 - src/components/MoneyRequestConfirmationList.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/Permissions.ts | 5 ----- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8aee1b9f1af4..67b62be473b5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -238,7 +238,6 @@ const CONST = { TASKS: 'tasks', THREADS: 'threads', CUSTOM_STATUS: 'customStatus', - NEW_DOT_CATEGORIES: 'newDotCategories', NEW_DOT_TAGS: 'newDotTags', }, BUTTON_STATES: { diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0d554ff0eca4..2a488e9810c2 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -194,7 +194,7 @@ function MoneyRequestConfirmationList(props) { const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(ReportUtils.getReport(props.reportID))), [props.reportID]); // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && Permissions.canUseCategories(props.betas) && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)); + const shouldShowCategories = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories)); // Fetches the first tag list of the policy const policyTag = PolicyUtils.getTag(props.policyTags); diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index ec91fb292257..3bd6fdc5673d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -106,7 +106,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should const policyTagsList = lodashGet(policyTag, 'tags', {}); // Flags for showing categories and tags - const shouldShowCategory = isPolicyExpenseChat && Permissions.canUseCategories(betas) && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); + const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories))); const shouldShowTag = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList))); const shouldShowBillable = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true)); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 05322472a407..ff96717adf62 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -49,10 +49,6 @@ function canUseCustomStatus(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.CUSTOM_STATUS) || canUseAllBetas(betas); } -function canUseCategories(betas: Beta[]): boolean { - return betas?.includes(CONST.BETAS.NEW_DOT_CATEGORIES) || canUseAllBetas(betas); -} - function canUseTags(betas: Beta[]): boolean { return betas?.includes(CONST.BETAS.NEW_DOT_TAGS) || canUseAllBetas(betas); } @@ -74,7 +70,6 @@ export default { canUsePolicyRooms, canUseTasks, canUseCustomStatus, - canUseCategories, canUseTags, canUseLinkPreviews, }; From 7e4e58f3465a9fbc17ac4e752a829d17d147140b Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Wed, 27 Sep 2023 16:03:50 +0800 Subject: [PATCH 009/337] Fix JS error --- src/components/OptionRow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js index 8bc016faa6b5..66a716698f2f 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.js @@ -212,7 +212,7 @@ class OptionRow extends Component { accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={this.props.hoverStyle} - needsOffscreenAlphaCompositing={this.props.option.icons.length >= 2} + needsOffscreenAlphaCompositing={this.props.option.icons && this.props.option.icons.length >= 2} > From f38e0e5b64aebafcc94c0f4c06b2f3c46dbe5ee4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Sep 2023 11:09:34 +0200 Subject: [PATCH 010/337] Initial work --- src/ONYXKEYS.ts | 2 +- src/libs/ReportActionsUtils.js | 2 +- src/libs/{SidebarUtils.js => SidebarUtils.ts} | 63 ++++++++----------- 3 files changed, 29 insertions(+), 38 deletions(-) rename src/libs/{SidebarUtils.js => SidebarUtils.ts} (91%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6649a33fe15e..51c5fb202f00 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -380,7 +380,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; - [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportAction; + [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: Record; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: string; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 67c44784eeb2..142f072728b2 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -334,7 +334,7 @@ function isReportActionDeprecated(reportAction, key) { * and supported type, it's not deleted and also not closed. * * @param {Object} reportAction - * @param {String} key + * @param {String | Number} key * @returns {Boolean} */ function shouldReportActionBeVisible(reportAction, key) { diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.ts similarity index 91% rename from src/libs/SidebarUtils.js rename to src/libs/SidebarUtils.ts index df676f23ebc7..e1251f9f50bc 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.ts @@ -13,9 +13,11 @@ import * as CollectionUtils from './CollectionUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import ReportAction from '../types/onyx/ReportAction'; + +const visibleReportActionItems: Record = {}; +const lastReportActions: Record = {}; -const visibleReportActionItems = {}; -const lastReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -24,38 +26,37 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); - const actionsArray = ReportActionsUtils.getSortedReportActions(_.toArray(actions)); - lastReportActions[reportID] = _.last(actionsArray); + const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); + lastReportActions[reportID] = actionsArray[actionsArray.length - 1]; // The report is only visible if it is the last action not deleted that // does not match a closed or created state. - const reportActionsForDisplay = _.filter( - actionsArray, + const reportActionsForDisplay = actionsArray.filter( (reportAction, actionKey) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, actionKey) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); - visibleReportActionItems[reportID] = _.last(reportActionsForDisplay); + visibleReportActionItems[reportID] = reportActionsForDisplay[reportActionsForDisplay.length - 1]; }, }); // Session can remain stale because the only way for the current user to change is to // sign out and sign in, which would clear out all the Onyx // data anyway and cause SidebarLinks to rerender. -let currentUserAccountID; +let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - if (!val) { + callback: (session) => { + if (!session) { return; } - currentUserAccountID = val.accountID; + currentUserAccountID = session.accountID; }, }); -let resolveSidebarIsReadyPromise; +let resolveSidebarIsReadyPromise: (args?: unknown[]) => void; let sidebarIsReadyPromise = new Promise((resolve) => { resolveSidebarIsReadyPromise = resolve; @@ -71,11 +72,11 @@ function isSidebarLoadedReady() { return sidebarIsReadyPromise; } -function compareStringDates(stringA, stringB) { - if (stringA < stringB) { +function compareStringDates(a: string, b: string) { + if (a < b) { return -1; } - if (stringA > stringB) { + if (a > b) { return 1; } return 0; @@ -89,7 +90,7 @@ function setIsSidebarLoadedReady() { const reportIDsCache = new Map(); // Function to set a key-value pair while maintaining the maximum key limit -function setWithLimit(map, key, value) { +function setWithLimit(map: Map, key: TKey, value: TValue) { if (map.size >= 5) { // If the map has reached its limit, remove the first (oldest) key-value pair const firstKey = map.keys().next().value; @@ -102,18 +103,11 @@ function setWithLimit(map, key, value) { let hasInitialReportActions = false; /** - * @param {String} currentReportId - * @param {Object} allReportsDict - * @param {Object} betas - * @param {String[]} policies - * @param {String} priorityMode - * @param {Object} allReportActions - * @returns {String[]} An array of reportIDs sorted in the proper order + * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - // eslint-disable-next-line es/no-optional-chaining [currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value) => { /** @@ -216,13 +210,13 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p /** * Gets all the data necessary for rendering an OptionRowLHN component * - * @param {Object} report - * @param {Object} reportActions - * @param {Object} personalDetails - * @param {String} preferredLocale - * @param {Object} [policy] - * @param {Object} parentReportAction - * @returns {Object} + * @param report + * @param reportActions + * @param personalDetails + * @param preferredLocale + * @param [policy] + * @param parentReportAction + * @returns */ function getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction) { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for @@ -331,14 +325,11 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, } : null; } - let lastMessageText = - hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; + let lastMessageText = hasMultipleParticipants && lastActorDetails?.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { - const archiveReason = - (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || - CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const archiveReason = lastReportActions[report.reportID]?.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report, false, policy), From 10922f70c17b5fd84aed450c968a4b9b326a4ec4 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 27 Sep 2023 23:31:35 +0530 Subject: [PATCH 011/337] fix: ignore other actions if navigation in progress in worksspace setting options --- src/components/MenuItemList.js | 13 +++++- src/pages/workspace/WorkspaceInitialPage.js | 45 +++++++++++---------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 90f863ba2bc7..03dbe81c82c4 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -4,18 +4,25 @@ import PropTypes from 'prop-types'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; +import { CONTEXT_MENU_TYPES } from '../pages/home/report/ContextMenu/ContextMenuActions'; +import useSingleExecution from '../hooks/useSingleExecution'; +import useWaitForNavigation from '../hooks/useWaitForNavigation'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution: PropTypes.bool, }; const defaultProps = { menuItems: [], + shouldUseSingleExecution: false, }; function MenuItemList(props) { let popoverAnchor; + const { isExecuting, singleExecution } = useSingleExecution(); + const waitForNavigate = useWaitForNavigation(); /** * Handle the secondary interaction for a menu item. @@ -35,12 +42,14 @@ function MenuItemList(props) { <> {_.map(props.menuItems, (menuItemProps) => ( secondaryInteraction(menuItemProps.link, e) : undefined} ref={(el) => (popoverAnchor = el)} shouldBlockSelection={Boolean(menuItemProps.link)} // eslint-disable-next-line react/jsx-props-no-spreading {...menuItemProps} + disabled={menuItemProps.disabled || isExecuting} + onPress={props.shouldUseSingleExecution ? singleExecution(waitForNavigate(menuItemProps.onPress)) : menuItemProps.onPress} /> ))} diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 567aef1274e1..962b8bfd056e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,8 +1,8 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useState} from 'react'; -import {View, ScrollView} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { useCallback, useEffect, useState } from 'react'; +import { View, ScrollView } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import PropTypes from 'prop-types'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; @@ -13,12 +13,11 @@ import ConfirmModal from '../../components/ConfirmModal'; import * as Expensicons from '../../components/Icon/Expensicons'; import ScreenWrapper from '../../components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import MenuItem from '../../components/MenuItem'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import {policyPropTypes, policyDefaultProps} from './withPolicy'; +import { policyPropTypes, policyDefaultProps } from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import reportPropTypes from '../reportPropTypes'; import * as Policy from '../../libs/actions/Policy'; @@ -31,6 +30,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as ReportUtils from '../../libs/ReportUtils'; import withWindowDimensions from '../../components/withWindowDimensions'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import MenuItemList from '../../components/MenuItemList'; const propTypes = { ...policyPropTypes, @@ -171,12 +171,12 @@ function WorkspaceInitialPage(props) { }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), + text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS }), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ADMINS), }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), + text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE }), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE), }, ]; @@ -186,7 +186,7 @@ function WorkspaceInitialPage(props) { includeSafeAreaPaddingBottom={false} testID={WorkspaceInitialPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({ safeAreaPaddingBottomStyle }) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy)} @@ -250,19 +250,22 @@ function WorkspaceInitialPage(props) { )} - {_.map(menuItems, (item) => ( - item.action()} - shouldShowRightIcon - brickRoadIndicator={item.brickRoadIndicator} - /> - ))} + ({ + key: item.translationKey, + disabled: hasPolicyCreationError, + interactive: !hasPolicyCreationError, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + brickRoadIndicator: item.brickRoadIndicator, + })) + } + shouldUseSingleExecution + /> From d21e914f4ae6f0006efe4e47392899a3394dda91 Mon Sep 17 00:00:00 2001 From: isabelastisser <31258516+isabelastisser@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:20:08 -0400 Subject: [PATCH 012/337] Create Security.md This is a new article that needs to be added to ExpensifyHelp. --- .../new-expensify/getting-started/Security.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/articles/new-expensify/getting-started/Security.md diff --git a/docs/articles/new-expensify/getting-started/Security.md b/docs/articles/new-expensify/getting-started/Security.md new file mode 100644 index 000000000000..58bdcb3cf134 --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Security.md @@ -0,0 +1,53 @@ +--- +title: Security + +--- + + +# Overview + +We take security seriously. Our measures align with what banks use to protect sensitive financial data. We regularly test and update our security to stay ahead of any threats. Plus, we're checked daily by McAfee for extra reassurance against hackers. You can verify our security strength below or on the McAfee SECURE site . + +Discover how Expensify safeguards your information below! + +## The Gold Standard of Security + +Expensify follows the highest standard of security, known as the Payment Card Industry Data Security Standard. This standard is used by major companies like PayPal, Visa, and banks to protect online credit card information. It covers many aspects of how systems work together securely. You can learn more about it on the PCI-DSS website. And, Expensify is also compliant with SSAE 16! + + +## Data and Password Encryption + +When you press 'enter,' your data transforms into a secret code, making it super secure. This happens whether it's moving between your browser and our servers or within our server network. In tech talk, we use HTTPS+TLS for all web connections, ensuring your information is encrypted at every stage of the journey. This means your data is always protected! + +## Account Safety + +Protecting your data on our servers is our top priority. We've taken strong measures to ensure your data is safe when it travels between you and us and when it's stored on our servers. +In our first year, we focused on creating a super-reliable, geographically redundant, and PCI compliant data center. This means your data stays safe, and our systems stay up and running. +We use a dual-control key, which only our servers know about. This key is split into two parts and stored in separate secure places, managed by different Expensify employees. +With this setup, sensitive data stays secure and can't be accessed outside our secure servers. + +## Our Commitment to GDPR + +The General Data Protection Regulation (GDPR), introduced by the European Commission, is a set of rules to strengthen and unify data protection for individuals in the European Union (EU). It also addresses the transfer of personal data outside the EU. This regulation applies not only to EU-based organizations but also to those outside the EU that handle the data of EU citizens. The compliance deadline for GDPR was May 25, 2018. + +Our commitment to protecting the privacy of our customer’s data includes: + +- Being active participants in the EU-US Privacy Shield and Swiss-US Privacy Shield Frameworks. +- Undergoing annual SSAE-18 SOC 1 Type 2 audit by qualified, independent third-party auditors. +- Maintaining PCI-DSS compliance. +- Leveraging third-party experts to conduct yearly penetration tests. +- All employees and contractors are subject to background checks (refreshed. annually), sign non-disclosure agreements, and are subject to ongoing security and privacy training. + + +We have worked diligently to ensure we comply with GDPR. Here are some key changes we made: + + +- **Enhanced Security and Data Privacy**: We've strengthened our security measures and carefully reviewed our privacy policies to align with GDPR requirements. +- **Dedicated Data Protection Officer**: We've appointed a dedicated Data Protection Officer who can be reached at privacy@expensify.com for any privacy-related inquiries. +- **Vendor Agreements**: We've signed Data Processing Addendums (DPAs) with all our vendors to ensure your data is handled safely during onward transfers. +- **Transparency**: You can find details about the sub-processors we use on our website. +- **Privacy Shield Certification**: We maintain certifications for the E.U.-U.S. Privacy Shield and the Swiss-U.S. Privacy Shield, which help secure international data transfers. +- **GDPR Compliance**: We have a Data Processing Addendum that outlines the terms to meet GDPR requirements. You can request a copy by contacting concierge@expensify.com. +- **User Control**: Our product tools allow users to export data, manage preferences, and close accounts anytime. + +**Disclaimer**: Please note that the information on this page is for informational purposes only and is not intended as legal advice. It's essential to consult with legal and professional counsel to understand how GDPR may apply to your specific situation. From 8113ebf244d214dc2e9d9d02ebb8d93fb88377a1 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 29 Sep 2023 12:24:07 +0200 Subject: [PATCH 013/337] Type fixes --- src/libs/PolicyUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 1f2abfa2b7a8..5b4ed6fcd208 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -96,11 +96,11 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers).forEach((accountID) => { - const member = policyMembers[accountID]; + const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {}).length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -115,12 +115,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers).forEach((accountID) => { - const policyMember = policyMembers[accountID]; + const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } - const memberEmail = personalDetails[accountID]?.login; + const memberEmail = personalDetails?.[accountID]?.login; if (!memberEmail) { return; } From 837bd88068113e148404d989f4331e8bfef0d7ec Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 2 Oct 2023 10:58:11 +0200 Subject: [PATCH 014/337] Migrate PolicyUtils to TS --- src/libs/PolicyUtils.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index a901384ef668..9b45318e67eb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -14,7 +14,9 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return policies.filter((policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return (policies ?? []).filter( + (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); } /** @@ -22,14 +24,14 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { - return Object.values(policyMembers).some((member) => Object.keys(member?.errors ?? {}).length > 0); + return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { - return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors).length > 0); + return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** @@ -87,19 +89,19 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { return ( policy && - policy.isPolicyExpenseChatEnabled && - policy.role === CONST.POLICY.ROLE.ADMIN && - (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors).length > 0) + policy?.isPolicyExpenseChatEnabled && + policy?.role === CONST.POLICY.ROLE.ADMIN && + (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) ); } function isExpensifyTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } function isExpensifyGuideTeam(email: string): boolean { - const emailDomain = Str.extractEmailDomain(email); + const emailDomain = Str.extractEmailDomain(email ?? ''); return emailDomain === CONST.EMAIL.GUIDES_DOMAIN; } @@ -115,9 +117,9 @@ const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CO */ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; - if (Object.keys(member?.errors ?? {}).length > 0) { + if (Object.keys(member?.errors ?? {})?.length > 0) { return; } const personalDetail = personalDetails[accountID]; @@ -134,7 +136,7 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person */ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; - Object.keys(policyMembers).forEach((accountID) => { + Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { @@ -154,11 +156,11 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: * Gets the tag from policy tags, defaults to the first if no key is provided. */ function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey] ?? {}; } @@ -167,11 +169,11 @@ function getTag(policyTags: Record, tagKey?: keyof * Gets the first tag name from policy tags. */ function getTagListName(policyTags: Record) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } - const policyTagKeys = Object.keys(policyTags)[0] ?? []; + const policyTagKeys = Object.keys(policyTags ?? {})[0] ?? []; return policyTags?.[policyTagKeys]?.name ?? ''; } @@ -180,17 +182,17 @@ function getTagListName(policyTags: Record) { * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ function getTagList(policyTags: Record>, tagKey: string) { - if (Object.keys(policyTags).length === 0) { + if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } - const policyTagKey = tagKey ?? Object.keys(policyTags)[0]; + const policyTagKey = tagKey ?? Object.keys(policyTags ?? {})[0]; return policyTags?.[policyTagKey]?.tags ?? {}; } function isPendingDeletePolicy(policy: OnyxTypes.Policy): boolean { - return policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } export { From f42364d124e12225486364263fec5eef0b7c31b2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 2 Oct 2023 12:34:47 +0200 Subject: [PATCH 015/337] [TS migration] Migrate 'SidebarUtils.js' lib to TypeScript --- src/libs/PersonalDetailsUtils.js | 4 +- src/libs/ReportUtils.js | 16 +- src/libs/SidebarUtils.ts | 238 +++++++++++++++++++++--------- src/types/onyx/PersonalDetails.ts | 2 + src/types/onyx/Report.ts | 7 +- 5 files changed, 186 insertions(+), 81 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index a401dea4b911..98bf171812fc 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -17,8 +17,8 @@ Onyx.connect({ }); /** - * @param {Object} passedPersonalDetails - * @param {Array} pathToDisplayName + * @param {Object | Null} passedPersonalDetails + * @param {Array | String} pathToDisplayName * @param {String} [defaultValue] optional default display name value * @returns {String} */ diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 475c1a8bcb8a..da2ef0f04ebf 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -108,9 +108,9 @@ function getPolicyType(report, policies) { /** * Get the policy name from a given report * @param {Object} report - * @param {String} report.policyID - * @param {String} report.oldPolicyName - * @param {String} report.policyName + * @param {String} [report.policyID] + * @param {String} [report.oldPolicyName] + * @param {String} [report.policyName] * @param {Boolean} [returnEmptyIfNotFound] * @param {Object} [policy] * @returns {String} @@ -363,7 +363,7 @@ function isUserCreatedPolicyRoom(report) { /** * Whether the provided report is a Policy Expense chat. * @param {Object} report - * @param {String} report.chatType + * @param {String} [report.chatType] * @returns {Boolean} */ function isPolicyExpenseChat(report) { @@ -389,7 +389,7 @@ function isControlPolicyExpenseReport(report) { /** * Whether the provided report is a chat room * @param {Object} report - * @param {String} report.chatType + * @param {String} [report.chatType] * @returns {Boolean} */ function isChatRoom(report) { @@ -578,8 +578,8 @@ function findLastAccessedReport(reports, ignoreDomainRooms, policies, isFirstTim /** * Whether the provided report is an archived room * @param {Object} report - * @param {Number} report.stateNum - * @param {Number} report.statusNum + * @param {Number} [report.stateNum] + * @param {Number} [report.statusNum] * @returns {Boolean} */ function isArchivedRoom(report) { @@ -2912,7 +2912,7 @@ function shouldHideReport(report, currentReportId) { * filter out the majority of reports before filtering out very specific minority of reports. * * @param {Object} report - * @param {String} currentReportId + * @param {String | Null} currentReportId * @param {Boolean} isInGSDMode * @param {String[]} betas * @param {Object} policies diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index e1251f9f50bc..938c0f7f0a26 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,8 +1,7 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; +import {ValueOf} from 'type-fest'; import ONYXKEYS from '../ONYXKEYS'; import * as ReportUtils from './ReportUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; @@ -14,6 +13,11 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import ReportAction from '../types/onyx/ReportAction'; +import Beta from '../types/onyx/Beta'; +import Policy from '../types/onyx/Policy'; +import Report from '../types/onyx/Report'; +import {PersonalDetails} from '../types/onyx'; +import * as OnyxCommon from '../types/onyx/OnyxCommon'; const visibleReportActionItems: Record = {}; const lastReportActions: Record = {}; @@ -68,11 +72,11 @@ function resetIsSidebarLoadedReadyPromise() { }); } -function isSidebarLoadedReady() { +function isSidebarLoadedReady(): Promise { return sidebarIsReadyPromise; } -function compareStringDates(a: string, b: string) { +function compareStringDates(a: string, b: string): 0 | 1 | -1 { if (a < b) { return -1; } @@ -87,7 +91,7 @@ function setIsSidebarLoadedReady() { } // Define a cache object to store the memoized results -const reportIDsCache = new Map(); +const reportIDsCache = new Map(); // Function to set a key-value pair while maintaining the maximum key limit function setWithLimit(map: Map, key: TKey, value: TValue) { @@ -105,11 +109,18 @@ let hasInitialReportActions = false; /** * @returns An array of reportIDs sorted in the proper order */ -function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) { +function getOrderedReportIDs( + currentReportId: string | null, + allReports: Record, + betas: Beta[], + policies: Record, + priorityMode: ValueOf, + allReportActions: Record, +): string[] { // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( - [currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], - (key, value) => { + [currentReportId, allReports, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + (key, value: unknown) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, * which we don't need to store in a cacheKey @@ -117,13 +128,15 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p if (key === 'participantAccountIDs' || key === 'participants' || key === 'lastMessageText') { return undefined; } + return value; }, ); // Check if the result is already in the cache - if (reportIDsCache.has(cachedReportsKey) && hasInitialReportActions) { - return reportIDsCache.get(cachedReportsKey); + const cachedIDs = reportIDsCache.get(cachedReportsKey); + if (cachedIDs && hasInitialReportActions) { + return cachedIDs; } // This is needed to prevent caching when Onyx is empty for a second render @@ -131,7 +144,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReportsDict); + const allReportsDictValues = Object.values(allReports); // Filter out all the reports that shouldn't be displayed const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true)); @@ -152,7 +165,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); + report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReports); }); // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: @@ -165,11 +178,11 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p // 5. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - const pinnedReports = []; - const outstandingIOUReports = []; - const draftReports = []; - const nonArchivedReports = []; - const archivedReports = []; + const pinnedReports: Report[] = []; + const outstandingIOUReports: Report[] = []; + const draftReports: Report[] = []; + const nonArchivedReports: Report[] = []; + const archivedReports: Report[] = []; reportsToDisplay.forEach((report) => { if (report.isPinned) { pinnedReports.push(report); @@ -185,47 +198,121 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p }); // Sort each group of reports accordingly - pinnedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - outstandingIOUReports.sort((a, b) => b.iouReportAmount - a.iouReportAmount || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - draftReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); + pinnedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); + outstandingIOUReports.sort((a, b) => { + const compareAmounts = a?.iouReportAmount && b?.iouReportAmount ? b.iouReportAmount - a.iouReportAmount : 0; + const compareDisplayNames = a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0; + return compareAmounts || compareDisplayNames; + }); + draftReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); if (isInDefaultMode) { - nonArchivedReports.sort( - (a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) || a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()), - ); + nonArchivedReports.sort((a, b) => { + const compareDates = a?.lastVisibleActionCreated && b?.lastVisibleActionCreated ? compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) : 0; + const compareDisplayNames = a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0; + return compareDates || compareDisplayNames; + }); // For archived reports ensure that most recent reports are at the top by reversing the order - archivedReports.sort((a, b) => compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated)); + archivedReports.sort((a, b) => (a?.lastVisibleActionCreated && b?.lastVisibleActionCreated ? compareStringDates(b.lastVisibleActionCreated, a.lastVisibleActionCreated) : 0)); } else { - nonArchivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); - archivedReports.sort((a, b) => a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase())); + nonArchivedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); + archivedReports.sort((a, b) => (a?.displayName && b?.displayName ? a.displayName.toLowerCase().localeCompare(b.displayName.toLowerCase()) : 0)); } // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [].concat(pinnedReports, outstandingIOUReports, draftReports, nonArchivedReports, archivedReports).map((report) => report.reportID); + const LHNReports = [...pinnedReports, ...outstandingIOUReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); setWithLimit(reportIDsCache, cachedReportsKey, LHNReports); return LHNReports; } +type OptionData = { + text?: string | null; + alternateText?: string | null; + pendingAction?: OnyxCommon.PendingAction | null; + allReportErrors?: OnyxCommon.Errors | null; + brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; + icons?: Icon[] | null; + tooltipText?: string | null; + ownerAccountID?: number | null; + subtitle?: string | null; + participantsList?: PersonalDetails[] | null; + login?: string | null; + accountID?: number | null; + managerID?: number | null; + reportID?: string | null; + policyID?: string | null; + status?: string | null; + type?: string | null; + stateNum?: ValueOf | null; + statusNum?: ValueOf | null; + phoneNumber?: string | null; + isUnread?: boolean | null; + isUnreadWithMention?: boolean | null; + hasDraftComment?: boolean | null; + keyForList?: string | null; + searchText?: string | null; + isPinned?: boolean | null; + hasOutstandingIOU?: boolean | null; + iouReportID?: string | null; + isIOUReportOwner?: boolean | null; + iouReportAmount?: number | null; + isChatRoom?: boolean | null; + isArchivedRoom?: boolean | null; + shouldShowSubscript?: boolean | null; + isPolicyExpenseChat?: boolean | null; + isMoneyRequestReport?: boolean | null; + isExpenseRequest?: boolean | null; + isWaitingOnBankAccount?: boolean | null; + isLastMessageDeletedParentAction?: boolean | null; + isAllowedToComment?: boolean | null; + isThread?: boolean | null; + isTaskReport?: boolean | null; + isWaitingForTaskCompleteFromAssignee?: boolean | null; + parentReportID?: string | null; + notificationPreference?: string | number | null; + displayNamesWithTooltips?: DisplayNamesWithTooltip[] | null; +}; + +type DisplayNamesWithTooltip = { + displayName?: string; + avatar?: string; + login?: string; + accountID?: number; + pronouns?: string; +}; + +type ActorDetails = { + displayName?: string; + accountID?: number; +}; + +type Icon = { + source?: string; + id?: number; + type?: string; + name?: string; +}; + /** * Gets all the data necessary for rendering an OptionRowLHN component - * - * @param report - * @param reportActions - * @param personalDetails - * @param preferredLocale - * @param [policy] - * @param parentReportAction - * @returns */ -function getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction) { +function getOptionData( + report: Report, + reportActions: Record, + personalDetails: Record, + preferredLocale: ValueOf, + policy: Policy, + parentReportAction: ReportAction, +): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do // a null check here and return early. if (!report || !personalDetails) { return; } - const result = { + + const result: OptionData = { text: null, alternateText: null, pendingAction: null, @@ -263,10 +350,14 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, isWaitingOnBankAccount: false, isLastMessageDeletedParentAction: false, isAllowedToComment: true, + isThread: null, + isTaskReport: null, + isWaitingForTaskCompleteFromAssignee: null, + parentReportID: null, + notificationPreference: null, }; - - const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs, personalDetails)); - const personalDetail = participantPersonalDetailList[0] || {}; + const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); + const personalDetail = participantPersonalDetailList[0] ?? {}; result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); @@ -280,8 +371,8 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; - result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); - result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; + result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; @@ -294,30 +385,30 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); + result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; - result.parentReportID = report.parentReportID || null; + result.parentReportID = report.parentReportID; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference || null; + result.notificationPreference = report.notificationPreference; result.isAllowedToComment = !ReportUtils.shouldDisableWriteActions(report); const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); - const login = Str.removeSMSDomain(lodashGet(personalDetail, 'login', '')); - const status = lodashGet(personalDetail, 'status', ''); + const login = Str.removeSMSDomain(personalDetail?.login ?? ''); + const status = personalDetail?.status ?? ''; const formattedLogin = Str.isSMSLogin(login) ? LocalePhoneNumber.formatPhoneNumber(login) : login; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); + const displayNamesWithTooltips: DisplayNamesWithTooltip[] = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action if that action is valid // to get data from. - let lastActorDetails = personalDetails[report.lastActorAccountID] || null; + let lastActorDetails: ActorDetails | null = report.lastActorAccountID ? personalDetails[report.lastActorAccountID] : null; if (!lastActorDetails && visibleReportActionItems[report.reportID]) { - const lastActorDisplayName = lodashGet(visibleReportActionItems[report.reportID], 'person[0].text'); + const lastActorDisplayName = visibleReportActionItems[report.reportID]?.person?.[0]?.text; lastActorDetails = lastActorDisplayName ? { displayName: lastActorDisplayName, @@ -328,22 +419,25 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, let lastMessageText = hasMultipleParticipants && lastActorDetails?.accountID && Number(lastActorDetails.accountID) !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; - if (result.isArchivedRoom) { - const archiveReason = lastReportActions[report.reportID]?.originalMessage?.reason || CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const reportAction = lastReportActions[report.reportID]; + if (result.isArchivedRoom && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + const archiveReason = reportAction?.originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; + lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report, false, policy), }); } if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) { const lastAction = visibleReportActionItems[report.reportID]; - if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - const newName = lodashGet(lastAction, 'originalMessage.newName', ''); + + if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + const newName = lastAction?.originalMessage?.newName ?? ''; result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); - } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) { + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) { result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}: ${report.reportName}`; - } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) { + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) { result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}: ${report.reportName}`; } else { result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); @@ -354,19 +448,23 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, // We also add a fullstop after the final name, the word "and" before the final name and commas between all previous names. lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistory') + - _.map(displayNamesWithTooltips, ({displayName, pronouns}, index) => { - const formattedText = _.isEmpty(pronouns) ? displayName : `${displayName} (${pronouns})`; - - if (index === displayNamesWithTooltips.length - 1) { - return `${formattedText}.`; - } - if (index === displayNamesWithTooltips.length - 2) { - return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`; - } - if (index < displayNamesWithTooltips.length - 2) { - return `${formattedText},`; - } - }).join(' '); + displayNamesWithTooltips + .map(({displayName, pronouns}, index) => { + const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`; + + if (index === displayNamesWithTooltips.length - 1) { + return `${formattedText}.`; + } + if (index === displayNamesWithTooltips.length - 2) { + return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`; + } + if (index < displayNamesWithTooltips.length - 2) { + return `${formattedText},`; + } + + return ''; + }) + .join(' '); } result.alternateText = lastMessageText || formattedLogin; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 64911dbfecb1..e1c944a95c40 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -40,6 +40,8 @@ type PersonalDetails = { /** Whether timezone is automatically set */ automatic?: boolean; }; + + status?: string; }; export default PersonalDetails; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 88caa683305d..b029e3996963 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -55,7 +55,7 @@ type Report = { reportName?: string; /** ID of the report */ - reportID?: string; + reportID: string; /** The state that the report is currently in */ stateNum?: ValueOf; @@ -83,6 +83,11 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + iouReportAmount?: number; + isWaitingOnBankAccount?: boolean; + isLastMessageDeletedParentAction?: boolean; + iouReportID?: string; + pendingFields?: Record; }; export default Report; From 08da8c014b6c53e8042bbae41731fdd891619c62 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 3 Oct 2023 10:01:07 +0200 Subject: [PATCH 016/337] Self review the code --- src/libs/SidebarUtils.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 938c0f7f0a26..d3bb9f6491d3 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -350,11 +350,6 @@ function getOptionData( isWaitingOnBankAccount: false, isLastMessageDeletedParentAction: false, isAllowedToComment: true, - isThread: null, - isTaskReport: null, - isWaitingForTaskCompleteFromAssignee: null, - parentReportID: null, - notificationPreference: null, }; const participantPersonalDetailList: PersonalDetails[] = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(report.participantAccountIDs ?? [], personalDetails)); const personalDetail = participantPersonalDetailList[0] ?? {}; @@ -387,9 +382,9 @@ function getOptionData( result.keyForList = String(report.reportID); result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; - result.parentReportID = report.parentReportID; + result.parentReportID = report.parentReportID ?? null; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; - result.notificationPreference = report.notificationPreference; + result.notificationPreference = report.notificationPreference ?? null; result.isAllowedToComment = !ReportUtils.shouldDisableWriteActions(report); const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; From bbcdbd146d234e201c05f74eaf40842b5a8c3811 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 3 Oct 2023 19:17:51 +0200 Subject: [PATCH 017/337] Changes after review of PolicyUtils --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 50 ++++++++++++++++++---------------- src/types/onyx/PolicyMember.ts | 3 ++ src/types/onyx/PolicyTag.ts | 3 ++ src/types/onyx/index.ts | 6 ++-- 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a1afc4fef2c1..b01ffbc37141 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -373,7 +373,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; - [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; + [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9b45318e67eb..c0bb7e539bec 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,21 +1,24 @@ +import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; -import * as OnyxTypes from '../types/onyx'; +import {PersonalDetails, Policy, PolicyMembers, PolicyTags} from '../types/onyx'; -type PolicyMemberList = Record; -type PolicyMembersCollection = Record; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; +type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. */ -function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { - return (policies ?? []).filter( - (policy) => policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, +function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { + if (!policies) { + return; + } + return (Object.values(policies) ?? []).filter( + (policy): policy is Policy => + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } @@ -23,28 +26,28 @@ function getActivePolicies(policies: OnyxTypes.Policy[]): OnyxTypes.Policy[] { * Checks if we have any errors stored within the POLICY_MEMBERS. Determines whether we should show a red brick road error or not. * Data structure: {accountID: {role:'user', errors: []}, accountID2: {role:'admin', errors: [{1231312313: 'Unable to do X'}]}, ...} */ -function hasPolicyMemberError(policyMembers: PolicyMemberList): boolean { +function hasPolicyMemberError(policyMembers: OnyxEntry): boolean { return Object.values(policyMembers ?? {}).some((member) => Object.keys(member?.errors ?? {}).length > 0); } /** * Check if the policy has any error fields. */ -function hasPolicyErrorFields(policy: OnyxTypes.Policy): boolean { +function hasPolicyErrorFields(policy: OnyxEntry): boolean { return Object.keys(policy?.errorFields ?? {}).some((fieldErrors) => Object.keys(fieldErrors ?? {}).length > 0); } /** * Check if the policy has any errors, and if it doesn't, then check if it has any error fields. */ -function hasPolicyError(policy: OnyxTypes.Policy): boolean { +function hasPolicyError(policy: OnyxEntry): boolean { return Object.keys(policy?.errors ?? {}).length > 0 ? true : hasPolicyErrorFields(policy); } /** * Checks if we have any errors stored within the policy custom units. */ -function hasCustomUnitsError(policy: OnyxTypes.Policy): boolean { +function hasCustomUnitsError(policy: OnyxEntry): boolean { return Object.keys(policy?.customUnits?.errors ?? {}).length > 0; } @@ -71,8 +74,8 @@ function getUnitRateValue(customUnitRate: UnitRate, toLocaleDigit: (arg: string) /** * Get the brick road indicator status for a policy. The policy has an error status if there is a policy member error, a custom unit error or a field error. */ -function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembersCollection: PolicyMembersCollection): string { - const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`] ?? {}; +function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMembersCollection: OnyxCollection): string { + const policyMembers = policyMembersCollection?.[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy?.id}`] ?? {}; if (hasPolicyMemberError(policyMembers) || hasCustomUnitsError(policy) || hasPolicyErrorFields(policy)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -86,8 +89,9 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxTypes.Policy, policyMembe * Note: Using a local ONYXKEYS.NETWORK subscription will cause a delay in * updating the screen. Passing the offline status from the component. */ -function shouldShowPolicy(policy: OnyxTypes.Policy, isOffline: boolean): boolean { +function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( + policy !== null && policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && @@ -108,21 +112,21 @@ function isExpensifyGuideTeam(email: string): boolean { /** * Checks if the current user is an admin of the policy. */ -const isPolicyAdmin = (policy: OnyxTypes.Policy): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; +const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ -function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): MemberEmailsToAccountIDs { +function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { const memberEmailsToAccountIDs: Record = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { return; } - const personalDetail = personalDetails[accountID]; + const personalDetail = personalDetails?.[accountID]; if (!personalDetail?.login) { return; } @@ -134,12 +138,12 @@ function getMemberAccountIDsForWorkspace(policyMembers: PolicyMemberList, person /** * Get login list that we should not show in the workspace invite options */ -function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: PersonalDetailsList): string[] { +function getIneligibleInvitees(policyMembers: OnyxEntry, personalDetails: OnyxEntry): string[] { const memberEmailsToExclude: string[] = [...CONST.EXPENSIFY_EMAILS]; Object.keys(policyMembers ?? {}).forEach((accountID) => { const policyMember = policyMembers?.[accountID]; // Policy members that are pending delete or have errors are not valid and we should show them in the invite options (don't exclude them). - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { + if (policyMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policyMember?.errors ?? {}).length > 0) { return; } const memberEmail = personalDetails?.[accountID]?.login; @@ -155,7 +159,7 @@ function getIneligibleInvitees(policyMembers: PolicyMemberList, personalDetails: /** * Gets the tag from policy tags, defaults to the first if no key is provided. */ -function getTag(policyTags: Record, tagKey?: keyof typeof policyTags) { +function getTag(policyTags: OnyxEntry, tagKey?: keyof typeof policyTags) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -168,7 +172,7 @@ function getTag(policyTags: Record, tagKey?: keyof /** * Gets the first tag name from policy tags. */ -function getTagListName(policyTags: Record) { +function getTagListName(policyTags: OnyxEntry) { if (Object.keys(policyTags ?? {})?.length === 0) { return ''; } @@ -181,7 +185,7 @@ function getTagListName(policyTags: Record) { /** * Gets the tags of a policy for a specific key. Defaults to the first tag if no key is provided. */ -function getTagList(policyTags: Record>, tagKey: string) { +function getTagList(policyTags: OnyxCollection, tagKey: string) { if (Object.keys(policyTags ?? {})?.length === 0) { return {}; } @@ -191,7 +195,7 @@ function getTagList(policyTags: Record): boolean { return policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } diff --git a/src/types/onyx/PolicyMember.ts b/src/types/onyx/PolicyMember.ts index 055465020c36..60836b276ea0 100644 --- a/src/types/onyx/PolicyMember.ts +++ b/src/types/onyx/PolicyMember.ts @@ -14,4 +14,7 @@ type PolicyMember = { pendingAction?: OnyxCommon.PendingAction; }; +type PolicyMembers = Record; + export default PolicyMember; +export type {PolicyMembers}; diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index fe6bee3a1f31..7807dcc00433 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -10,4 +10,7 @@ type PolicyTag = { 'GL Code': string; }; +type PolicyTags = Record; + export default PolicyTag; +export type {PolicyTags}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e50925e7adf2..9067229a17d9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,7 +32,7 @@ import WalletTransfer from './WalletTransfer'; import MapboxAccessToken from './MapboxAccessToken'; import {OnyxUpdatesFromServer, OnyxUpdateEvent} from './OnyxUpdatesFromServer'; import Download from './Download'; -import PolicyMember from './PolicyMember'; +import PolicyMember, {PolicyMembers} from './PolicyMember'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; import Report from './Report'; @@ -45,7 +45,7 @@ import Form, {AddDebitCardForm} from './Form'; import RecentWaypoint from './RecentWaypoint'; import RecentlyUsedCategories from './RecentlyUsedCategories'; import RecentlyUsedTags from './RecentlyUsedTags'; -import PolicyTag from './PolicyTag'; +import PolicyTag, {PolicyTags} from './PolicyTag'; export type { Account, @@ -98,4 +98,6 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, PolicyTag, + PolicyTags, + PolicyMembers, }; From 5baad2f40b71200062ab99250082953f6b178881 Mon Sep 17 00:00:00 2001 From: Sophie Pinto-Raetz <42940078+sophiepintoraetz@users.noreply.github.com> Date: Thu, 5 Oct 2023 02:57:33 +0000 Subject: [PATCH 018/337] Create Domain-Settings-Overview.md This is my PR for my help article to be added to the new site. --- .../Domain-Settings-Overview.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md new file mode 100644 index 000000000000..de06410baf1c --- /dev/null +++ b/docs/articles/new-expensify/workspace-and-domain-settings/Domain-Settings-Overview.md @@ -0,0 +1,148 @@ +--- +title: The title of the post, page, or document +description: Want to gain greater control over your company settings in Expensify? Read on to find out more about our Domains feature and how it can help you save time and effort when managing your company expenses. +--- + + +# Overview + + +# How to claim a domain + + +# How to verify a domain + + +# Domain settings + + +# FAQ + From 9f88be898f03c5e7875ea926152eae2aa3c8946f Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:46:40 -0400 Subject: [PATCH 019/337] Update and rename Get-The-Card.md to Request-the-Card.md Renaming file + adding employee-specific "request the expensify card" resource --- .../expensify-card/Get-The-Card.md | 5 -- .../expensify-card/Request-the-Card.md | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) delete mode 100644 docs/articles/expensify-classic/expensify-card/Get-The-Card.md create mode 100644 docs/articles/expensify-classic/expensify-card/Request-the-Card.md diff --git a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md b/docs/articles/expensify-classic/expensify-card/Get-The-Card.md deleted file mode 100644 index e5233a3732a3..000000000000 --- a/docs/articles/expensify-classic/expensify-card/Get-The-Card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Get the Card -description: Get the Card ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md new file mode 100644 index 000000000000..fa8c81e289dc --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -0,0 +1,49 @@ +--- +title: Request the Card +description: Details on requesting the Expensify Card as an employee +--- +# Overview + +Once your organization is approved for the Expensify Card, you can request a card! + +This article covers how to request, activate, and replace your physical and virtual Expensify Cards. + +# How to get your first Expensify Card + +An admin in your organization must first enable the Expensify Cards before you can receive a card. After that, an admin may assign you a card by setting a limit. You can think of setting a card limit as “unlocking” access to the card. + +If you haven’t been assigned a limit yet, look for the task on your account's homepage that says, “Ask your admin for the card!”. This task allows you to message your admin team to make that request. + +Once you’re assigned a card limit, we’ll notify you via email to let you know you can request a card. A link within the notification email will take you to your account’s homepage, where you can provide your shipping address for the physical card. Enter your address, and we’ll ship the card to arrive within 3-5 business days. + +Once your physical card arrives in the mail, activate it in Expensify by entering the last four digits of the card in the activation task on your account’s homepage. + +# Virtual Card + +Once assigned a limit, a virtual card is available immediately. You can view the virtual card details via **Settings > Account > Credit Card Import > Show Details**. Feel free to begin transacting with the virtual card while your physical card is in transit – your virtual card and physical card share a limit. + +Please note that you must enable 2-factor authentication on your account if you want to have the option to dispute transactions made on your virtual card. + +# Notifications + +To stay up-to-date on your card’s limit and spending activity, download the Expensify mobile app and enable push notifications. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We’ll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase. + +# How to request a replacement Expensify Card + +You can request a new card anytime if your Expensify Card is lost, stolen, or damaged. From your Expensify account on the web, head to **Settings > Account > Credit Card Import** and click **Request a New Card**. Confirm the shipping information, complete the prompts, and your new card will arrive in 2 - 3 business days. + +Selecting the “lost” or “stolen” options will deactivate your current card to prevent potentially fraudulent activity. However, choosing the “damaged” option will leave your current card active so you can use it while the new one is shipped to you. + +If you need to cancel your Expensify Card and cannot access the website or mobile app, call our interactive voice recognition phone service (available 24/7). Call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally). + +It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. + +# FAQ + +## What if I haven’t received my card after multiple weeks? + +Reach out to support, and we can locate a tracking number for the card. If the card shows as delivered, but you still haven’t received it, you’ll need to confirm your address and order a new one. + +## I’m self-employed. Can I set up the Expensify Card as an individual? + +Yep! As long as you have a business bank account and have registered your company with the IRS, you are eligible to use the Expensify Card as an individual business owner. From f73d60100b4fc965ba780113338b9eed28517a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Fri, 6 Oct 2023 10:55:32 +0200 Subject: [PATCH 020/337] first commit --- src/pages/settings/InitialSettingsPage.js | 27 ++++++++-------- .../settings/Wallet/ExpensifyCardPage.js | 31 +++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d81c9d057174..61b2b5f838eb 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,11 +1,11 @@ import lodashGet from 'lodash/get'; -import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; -import {View} from 'react-native'; +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { View } from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import {withNetwork} from '../../components/OnyxProvider'; +import { withNetwork } from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -18,11 +18,11 @@ import MenuItem from '../../components/MenuItem'; import themeColors from '../../styles/themes/default'; import SCREENS from '../../SCREENS'; import ROUTES from '../../ROUTES'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import withLocalize, { withLocalizePropTypes } from '../../components/withLocalize'; import compose from '../../libs/compose'; import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; @@ -37,7 +37,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import { CONTEXT_MENU_TYPES } from '../home/report/ContextMenu/ContextMenuActions'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; @@ -128,12 +128,13 @@ const defaultProps = { allPolicyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; +window._navigate = () => Navigation.navigate('/settings/wallet/card/Expensify'); function InitialSettingsPage(props) { - const {isExecuting, singleExecution} = useSingleExecution(); + const { isExecuting, singleExecution } = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); - const {translate} = useLocalize(); + const { translate } = useLocalize(); const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -179,10 +180,10 @@ function InitialSettingsPage(props) { const policyBrickRoadIndicator = !_.isEmpty(props.reimbursementAccount.errors) || - _.chain(props.policies) - .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) - .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) - .value() + _.chain(props.policies) + .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) + .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) + .value() ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null; const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 026e8147d79f..54088b40c0c9 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, {useState} from 'react'; -import {ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { useState } from 'react'; +import { ScrollView, View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; @@ -18,6 +18,8 @@ import styles from '../../../styles/styles'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; +// eslint-disable-next-line rulesdir/no-api-in-views +import * as API from '../../../libs/API'; const propTypes = { /* Onyx Props */ @@ -39,15 +41,17 @@ const defaultProps = { function ExpensifyCardPage({ cardList, route: { - params: {domain}, + params: { domain }, }, }) { - const {translate} = useLocalize(); + const { translate } = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; const [shouldShowCardDetails, setShouldShowCardDetails] = useState(false); + const [details, setDetails] = useState({}); + const [loading, setLoading] = useState(false); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -57,6 +61,14 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { setShouldShowCardDetails(true); + setLoading(true); + // eslint-disable-next-line + API.makeRequestWithSideEffects('RevealVirtualCardDetails') + .then((val) => { + setDetails(val); + setLoading(false); + }) + .catch(console.log); }; return ( @@ -64,7 +76,7 @@ function ExpensifyCardPage({ includeSafeAreaPaddingBottom={false} testID={ExpensifyCardPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( + {({ safeAreaPaddingBottomStyle }) => ( <> ) : ( Date: Fri, 6 Oct 2023 17:27:21 +0200 Subject: [PATCH 021/337] move to reducer and error handling --- .../settings/Wallet/ExpensifyCardPage.js | 42 ++++++++------ .../settings/Wallet/revealCardDetailsUtils.ts | 57 +++++++++++++++++++ 2 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/pages/settings/Wallet/revealCardDetailsUtils.ts diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 54088b40c0c9..8d42ce149e16 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; -import React, { useState } from 'react'; -import { ScrollView, View } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useReducer} from 'react'; +import {ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; @@ -20,6 +20,8 @@ import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; // eslint-disable-next-line rulesdir/no-api-in-views import * as API from '../../../libs/API'; +import CONST from '../../../CONST'; +import * as revealCardDetailsUtils from './revealCardDetailsUtils'; const propTypes = { /* Onyx Props */ @@ -41,17 +43,15 @@ const defaultProps = { function ExpensifyCardPage({ cardList, route: { - params: { domain }, + params: {domain}, }, }) { - const { translate } = useLocalize(); + const {translate} = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [shouldShowCardDetails, setShouldShowCardDetails] = useState(false); - const [details, setDetails] = useState({}); - const [loading, setLoading] = useState(false); + const [{loading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -60,15 +60,19 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - setShouldShowCardDetails(true); - setLoading(true); + dispatch({type: 'START'}); // eslint-disable-next-line API.makeRequestWithSideEffects('RevealVirtualCardDetails') - .then((val) => { - setDetails(val); - setLoading(false); + .then((response) => { + if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + dispatch({type: 'FAIL', payload: response.message}); + return; + } + dispatch({type: 'SUCCESS', payload: response}); }) - .catch(console.log); + .catch((err) => { + dispatch({type: 'FAIL', payload: err.message}); + }); }; return ( @@ -76,7 +80,7 @@ function ExpensifyCardPage({ includeSafeAreaPaddingBottom={false} testID={ExpensifyCardPage.displayName} > - {({ safeAreaPaddingBottomStyle }) => ( + {({safeAreaPaddingBottomStyle}) => ( <> {!_.isEmpty(virtualCard) && ( <> - {shouldShowCardDetails ? ( + {details.pan ? ( ) : ( } /> diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts new file mode 100644 index 000000000000..86300e1058b6 --- /dev/null +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -0,0 +1,57 @@ +type State = { + details: { + pan: string; + expiration: string; + cvv: string; + privatePersonalDetails: { + address: { + street: string; + street2: string; + city: string; + state: string; + zip: string; + country: string; + }; + }; + }; + loading: boolean; + error: string; +}; + +type Action = {type: 'START'} | {type: 'SUCCESS'; payload: State['details']} | {type: 'FAIL'; payload: string}; + +const initialState: State = { + details: { + pan: '', + expiration: '', + cvv: '', + privatePersonalDetails: { + address: { + street: '', + street2: '', + city: '', + state: '', + zip: '', + country: '', + }, + }, + }, + loading: false, + error: '', +}; + +const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'START': + return {...state, loading: true}; + case 'SUCCESS': + return {details: action.payload, loading: false, error: ''}; + case 'FAIL': { + return {...state, error: action.payload, loading: false}; + } + default: + return state; + } +}; + +export {initialState, reducer}; From 334061b6f87f81addacb2f4795df22f09405661b Mon Sep 17 00:00:00 2001 From: Corinne Ofstad Date: Fri, 6 Oct 2023 11:16:19 -0500 Subject: [PATCH 022/337] Update Business-Bank-Accounts-USD.md Adding US Business Bank Account Aricle --- .../Business-Bank-Accounts-USD.md | 147 +++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md index 375b00d62eac..29667f14397d 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md +++ b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/business-bank-accounts/Business-Bank-Accounts-USD.md @@ -1,5 +1,148 @@ --- title: Business Bank Accounts - USD -description: Business Bank Accounts - USD +description: How to add/remove Business Bank Accounts (US) --- -## Resource Coming Soon! +# Overview +Adding a verified business bank account unlocks a myriad of features and automation in Expensify. +Once you connect your business bank account, you can: +- Pay employee expense reports via direct deposit (US) +- Settle company bills via direct transfer +- Accept invoice payments through direct transfer +- Access the Expensify Card +# How to add a verified business bank account +To connect a business bank account to Expensify, follow the below steps: +1. Go to **Settings > Account > Payments** +2. Click **Add Verified Bank Account** +3. Click **Log into your bank** +4. Click **Continue** +5. When you hit the **Plaid** screen, you'll be shown a list of compatible banks that offer direct online login access +6. Login to the business bank account +- If the bank is not listed, click the X to go back to the connection type +- Here you’ll see the option to **Connect Manually** +- Enter your account and routing numbers +7. Enter your bank login credentials. +- If your bank requires additional security measures, you will be directed to obtain and enter a security code +- If you have more than one account available to choose from, you will be directed to choose the desired account +Next, to verify the bank account, you’ll enter some details about the business as well as some personal information. +## Enter company information +This is where you’ll add the legal business name as well as several other company details. +### Company address +The company address must: +- Be located in the US +- Be a physical location +If you input a maildrop address (PO box, UPS Store, etc.), the address will likely be flagged for review and adding the bank account to Expensify will be delayed. +### Tax Identification Number +This is the identification number that was assigned to the business by the IRS. +### Company website +A company website is required to use most of Expensify’s payment features. When adding the website of the business, format it as, https://www.domain.com. +### Industry Classification Code +You can locate a list of Industry Classification Codes here. +## Enter personal information +Whoever is connecting the bank account to Expensify, must enter their details under the Requestor Information section: +- The address must be a physical address +- The address must be located in the US +- The SSN must be US-issued +This does not need to be a signor on the bank account. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review and adding the bank account to Expensify will be delayed. +## Upload ID +After entering your personal details, you’ll be prompted to click a link or scan a QR code so that you can do the following: +1. Upload the front and back of your ID +2. Use your device to take a selfie and record a short video of yourself +It’s required that your ID is: +- Issued in the US +- Unexpired +## Additional Information +Check the appropriate box under **Additional Information**, accept the agreement terms, and verify that all of the information is true and accurate: +- A Beneficial Owner refers to an **individual** who owns 25% or more of the business. +- If you or another **individual** owns 25% or more of the business, please check the appropriate box +- If someone else owns 25% or more of the business, you will be prompted to provide their personal information +If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section. +# How to validate the bank account +The account you set up can be found under **Settings > Account > Payment > Bank Accounts** section in either **Verifying** or **Pending** status. +If it is **Verifying**, then this means we sent you a message and need more information from you. Please check your Concierge chat which should include a message with specific details about what we require to move forward. +If it is **Pending**, then in 1-2 business days Expensify will administer 3 test transactions to your bank account. Please check your Concierge chat for further instructions. If you do not see these test transactions +After these transactions (2 withdrawals and 1 deposit) have been processed in your account, visit your Expensify Inbox, where you'll see a prompt to input the transaction amounts. +Once you've finished these steps, your business bank account is ready to use in Expensify! +# How to share a verified bank account +Only admins with access to the verified bank account can reimburse employees or pay vendor bills. To grant another admin access to the bank account in Expensify, go to **Settings > Account > Payments > Bank Accounts** and click **"Share"**. Enter their email address, and they will receive instructions from us. Please note, they must be a policy admin on a policy you also have access to in order to share the bank account with them. +When a bank account is shared, it must be revalidated with three new microtransactions to ensure the shared admin has access. This process takes 1-2 business days. Once received, the shared admin can enter the transactions via their Expensify account's Inbox tab. + +Note: A report is shared with all individuals with access to the same business bank account in Expensify for audit purposes. + + +# How to remove access to a verified bank account +This step is important when accountants and staff leave your business. +To remove an admin's access to a shared bank account, go to **Settings > Account > Payments > Shared Business Bank Accounts**. +You'll find a list of individuals who have access to the bank account. Next to each user, you'll see the option to Unshare the bank account. +# How to delete a verified bank account +If you need to delete a bank account from Expensify, run through the following steps: +1. Head to **Settings > Account > Payments** +2. Click the red **Delete** button under the corresponding bank account + +Be cautious, as if it hasn't been shared with someone else, the next user will need to set it up from the beginning. + +If the bank account is set as the settlement account for your Expensify Cards, you’ll need to designate another bank account as your settlement account under **Settings > Domains > Company Cards > Settings** before this account can be deleted. +# Deep Dive + +## Verified bank account requirements + +To add a business bank account to issue reimbursements via ACH (US), to pay invoices (US) or utilize the Expensify Card: +- You must enter a physical address for yourself, any Beneficial Owner (if one exists), and the business associated with the bank account. We **cannot** accept a PO Box or MailDrop location. +- If you are adding the bank account to Expensify, you must add it from **your** Expensify account settings. +- If you are adding a bank account to Expensify, we are required by law to verify your identity. Part of this process requires you to verify a US issued photo ID. For utilizing features related to US ACH, your idea must be issued by the United States. You and any Beneficial Owner (if one exists), must also have a US address +- You must have a valid website for your business to utilize the Expensify Card, or to pay invoices with Expensify. + +## Locked bank account +When you reimburse a report, you authorize Expensify to withdraw the funds from your account. If your bank rejects Expensify’s withdrawal request, your verified bank account is locked until the issue is resolved. + +Withdrawal requests can be rejected due to insufficient funds, or if the bank account has not been enabled for direct debit. +If you need to enable direct debits from your verified bank account, your bank will require the following details: +- The ACH CompanyIDs (1270239450 and 4270239450) +- The ACH Originator Name (Expensify) +To request to unlock the bank account, click **Fix** on your bank account under **Settings > Account > Payments > Bank Accounts**. +This sends a request to our support team to review exactly why the bank account was locked. +Please note, unlocking a bank account can take 4-5 business days to process. +## Error adding ID to Onfido +Expensify is required by both our sponsor bank and federal law to verify the identity of the individual that is initiating the movement of money. We use Onfido to confirm that the person adding a payment method is genuine and not impersonating someone else. + +If you get a generic error message that indicates, "Something's gone wrong", please go through the following steps: + +1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. +2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" +3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). +4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. +5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. +6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. +7. If possible, try these steps on another device +8. If you have another phone available, try to follow these steps on that device +If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. + +# FAQ +## What is a Beneficial Owner? + +A Beneficial Owner refers to an **individual** who owns 25% or more of the business. If no individual owns 25% or more of the business, the company does not have a Beneficial Owner. + + +## What do I do if the Beneficial Owner section only asks for personal details, but our business is owned by another company? + + +Please only indicate you have a Beneficial Owner, if it is an individual that owns 25% or more of the business. + +## Why can’t I input my address or upload my ID? + + +Are you entering a US address? When adding a verified business bank account in Expensify, the individual adding the account, and any beneficial owner (if one exists) are required to have a US address, US photo ID, and a US SSN. If you do not meet these requirements, you’ll need to have another admin add the bank account, and then share access with you once verified. + + +## Why am I being asked for documentation when adding my bank account? +When a bank account is added to Expensify, we complete a series of checks to verify the information provided to us. We conduct these checks to comply with both our sponsor bank's requirements and federal government regulations, specifically the Bank Secrecy Act / Anti-Money Laundering (BSA / AML) laws. Expensify also has anti-fraud measures in place. +If automatic verification fails, we may request manual verification, which could involve documents such as address verification for your business, a letter from your bank confirming bank account ownership, etc. + +If you have any questions regarding the documentation request you received, please contact Concierge and they will be happy to assist. + + +## I don’t see all three microtransactions I need to validate my bank account. What should I do? + +It's a good idea to wait till the end of that second business day. If you still don’t see them, please reach out to your bank and ask them to whitelist our ACH ID's **1270239450** and **4270239450**. Expensify’s ACH Originator Name is "Expensify". + +Make sure to reach out to your Account Manager or to Concierge once you have done so and our team will be able to re-trigger those 3 transactions! + From df10ff714fa27a6348a53d2a842fff69d5ad7d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:46:19 +0200 Subject: [PATCH 023/337] removed unnecessary helper function --- src/pages/settings/InitialSettingsPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 61b2b5f838eb..fc7997a3d0b6 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -128,7 +128,6 @@ const defaultProps = { allPolicyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; -window._navigate = () => Navigation.navigate('/settings/wallet/card/Expensify'); function InitialSettingsPage(props) { const { isExecuting, singleExecution } = useSingleExecution(); From 36fb11a5c21d9390ba13b3df51fbd60c316204b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:46:51 +0200 Subject: [PATCH 024/337] formatting --- src/pages/settings/InitialSettingsPage.js | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index fc7997a3d0b6..d81c9d057174 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,11 +1,11 @@ import lodashGet from 'lodash/get'; -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import { View } from 'react-native'; +import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; +import {View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import { withOnyx } from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import { withNetwork } from '../../components/OnyxProvider'; +import {withNetwork} from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -18,11 +18,11 @@ import MenuItem from '../../components/MenuItem'; import themeColors from '../../styles/themes/default'; import SCREENS from '../../SCREENS'; import ROUTES from '../../ROUTES'; -import withLocalize, { withLocalizePropTypes } from '../../components/withLocalize'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from '../../components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; @@ -37,7 +37,7 @@ import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursemen import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; -import { CONTEXT_MENU_TYPES } from '../home/report/ContextMenu/ContextMenuActions'; +import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; import * as CurrencyUtils from '../../libs/CurrencyUtils'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import useLocalize from '../../hooks/useLocalize'; @@ -130,10 +130,10 @@ const defaultProps = { }; function InitialSettingsPage(props) { - const { isExecuting, singleExecution } = useSingleExecution(); + const {isExecuting, singleExecution} = useSingleExecution(); const waitForNavigate = useWaitForNavigation(); const popoverAnchor = useRef(null); - const { translate } = useLocalize(); + const {translate} = useLocalize(); const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -179,10 +179,10 @@ function InitialSettingsPage(props) { const policyBrickRoadIndicator = !_.isEmpty(props.reimbursementAccount.errors) || - _.chain(props.policies) - .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) - .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) - .value() + _.chain(props.policies) + .filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) + .some((policy) => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, props.allPolicyMembers)) + .value() ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : null; const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); From bfdb546f3d1c5ef7a5fed8f1b4fafd785f46b4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 10:52:05 +0200 Subject: [PATCH 025/337] var name change to isLoading --- src/pages/settings/Wallet/ExpensifyCardPage.js | 6 +++--- src/pages/settings/Wallet/revealCardDetailsUtils.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 8d42ce149e16..7a7a3a742845 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -51,7 +51,7 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [{loading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); + const [{isLoading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -119,8 +119,8 @@ function ExpensifyCardPage({ medium text={translate('cardPage.cardDetails.revealDetails')} onPress={handleRevealDetails} - isDisabled={loading} - isLoading={loading} + isDisabled={isLoading} + isLoading={isLoading} /> } /> diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts index 86300e1058b6..5e2eb355acea 100644 --- a/src/pages/settings/Wallet/revealCardDetailsUtils.ts +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -14,7 +14,7 @@ type State = { }; }; }; - loading: boolean; + isLoading: boolean; error: string; }; @@ -36,18 +36,18 @@ const initialState: State = { }, }, }, - loading: false, + isLoading: false, error: '', }; -const reducer = (state: State, action: Action) => { +const reducer = (state: State, action: Action): State => { switch (action.type) { case 'START': - return {...state, loading: true}; + return {...state, isLoading: true}; case 'SUCCESS': - return {details: action.payload, loading: false, error: ''}; + return {details: action.payload, isLoading: false, error: ''}; case 'FAIL': { - return {...state, error: action.payload, loading: false}; + return {...state, error: action.payload, isLoading: false}; } default: return state; From 9a8962c1210888f8c91515ec24dae0d39fb27a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 11:12:47 +0200 Subject: [PATCH 026/337] better linting --- src/pages/settings/Wallet/ExpensifyCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 7a7a3a742845..09b0bd53bb7e 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -61,7 +61,7 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { dispatch({type: 'START'}); - // eslint-disable-next-line + // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects('RevealVirtualCardDetails') .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { From c7dd8fa76a6757efa2580a6540524a6f35dc8963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 12:43:06 +0200 Subject: [PATCH 027/337] moved action types to const --- src/pages/settings/Wallet/ExpensifyCardPage.js | 8 ++++---- .../settings/Wallet/revealCardDetailsUtils.ts | 16 +++++++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 09b0bd53bb7e..3e2d8632228a 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -60,18 +60,18 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - dispatch({type: 'START'}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects('RevealVirtualCardDetails') .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: 'FAIL', payload: response.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: response.message}); return; } - dispatch({type: 'SUCCESS', payload: response}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.success, payload: response}); }) .catch((err) => { - dispatch({type: 'FAIL', payload: err.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: err.message}); }); }; diff --git a/src/pages/settings/Wallet/revealCardDetailsUtils.ts b/src/pages/settings/Wallet/revealCardDetailsUtils.ts index 5e2eb355acea..57e70fa41462 100644 --- a/src/pages/settings/Wallet/revealCardDetailsUtils.ts +++ b/src/pages/settings/Wallet/revealCardDetailsUtils.ts @@ -1,3 +1,9 @@ +const ACTION_TYPES = { + START: 'START', + SUCCESS: 'SUCCESS', + FAIL: 'FAIL', +} as const; + type State = { details: { pan: string; @@ -18,7 +24,7 @@ type State = { error: string; }; -type Action = {type: 'START'} | {type: 'SUCCESS'; payload: State['details']} | {type: 'FAIL'; payload: string}; +type Action = {type: typeof ACTION_TYPES.START} | {type: typeof ACTION_TYPES.SUCCESS; payload: State['details']} | {type: typeof ACTION_TYPES.FAIL; payload: string}; const initialState: State = { details: { @@ -42,11 +48,11 @@ const initialState: State = { const reducer = (state: State, action: Action): State => { switch (action.type) { - case 'START': + case ACTION_TYPES.START: return {...state, isLoading: true}; - case 'SUCCESS': + case ACTION_TYPES.SUCCESS: return {details: action.payload, isLoading: false, error: ''}; - case 'FAIL': { + case ACTION_TYPES.FAIL: { return {...state, error: action.payload, isLoading: false}; } default: @@ -54,4 +60,4 @@ const reducer = (state: State, action: Action): State => { } }; -export {initialState, reducer}; +export {initialState, reducer, ACTION_TYPES}; From 76ef509679e64746faa53336fca77f796ec5c10e Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 9 Oct 2023 17:11:13 +0530 Subject: [PATCH 028/337] added fix for all security, workspace and aboutpage --- src/components/MenuItemList.js | 7 +- src/pages/settings/AboutPage/AboutPage.js | 55 +++++++++----- src/pages/settings/InitialSettingsPage.js | 6 +- .../settings/Security/SecuritySettingsPage.js | 31 ++++---- src/pages/workspace/WorkspaceInitialPage.js | 74 ++++++++++--------- 5 files changed, 98 insertions(+), 75 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 03dbe81c82c4..48190a4f4d05 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import { CONTEXT_MENU_TYPES } from '../pages/home/report/ContextMenu/ContextMenuActions'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; import useSingleExecution from '../hooks/useSingleExecution'; import useWaitForNavigation from '../hooks/useWaitForNavigation'; @@ -21,8 +21,7 @@ const defaultProps = { function MenuItemList(props) { let popoverAnchor; - const { isExecuting, singleExecution } = useSingleExecution(); - const waitForNavigate = useWaitForNavigation(); + const {isExecuting, singleExecution} = useSingleExecution(); /** * Handle the secondary interaction for a menu item. @@ -49,7 +48,7 @@ function MenuItemList(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...menuItemProps} disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(waitForNavigate(menuItemProps.onPress)) : menuItemProps.onPress} + onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} /> ))} diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 78a300a38057..14cd7be5ef25 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import React from 'react'; +import PropTypes from 'prop-types'; import {ScrollView, View} from 'react-native'; import DeviceInfo from 'react-native-device-info'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; @@ -23,10 +24,15 @@ import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportAc import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; import * as Environment from '../../../libs/Environment/Environment'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import MenuItemList from '../../../components/MenuItemList'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; const propTypes = { ...withLocalizePropTypes, ...windowDimensionsPropTypes, + isShortcutsModalOpen: PropTypes.bool, }; function getFlavor() { @@ -42,13 +48,12 @@ function getFlavor() { function AboutPage(props) { let popoverAnchor; + const waitForNavigate = useWaitForNavigation(); const menuItems = [ { translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', icon: Expensicons.Link, - action: () => { - Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS); - }, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), }, { translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', @@ -79,7 +84,7 @@ function AboutPage(props) { action: Report.navigateToConciergeChat, }, ]; - + console.log('props.isShortcutsModalOpen: ', props.isShortcutsModalOpen); return ( {props.translate('initialSettingsPage.aboutPage.description')} - {_.map(menuItems, (item) => ( - item.action()} - shouldBlockSelection={Boolean(item.link)} - onSecondaryInteraction={ - !_.isEmpty(item.link) ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) : undefined - } - ref={(el) => (popoverAnchor = el)} - shouldShowRightIcon - /> - ))} + ({ + key: item.translationKey, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + disabled: props.isShortcutsModalOpen, + onPress: item.action, + shouldShowRightIcon: true, + onSecondaryInteraction: !_.isEmpty(item.link) + ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) + : undefined, + ref: (el) => (popoverAnchor = el), + shouldBlockSelection: Boolean(item.link), + }))} + shouldUseSingleExecution + /> @@ -357,7 +358,8 @@ function InitialSettingsPage(props) { diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index 704ea17422bd..d6f1d2b8fb86 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -15,6 +15,8 @@ import compose from '../../../libs/compose'; import ONYXKEYS from '../../../ONYXKEYS'; import IllustratedHeaderPageLayout from '../../../components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '../../../components/LottieAnimations'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import MenuItemList from '../../../components/MenuItemList'; const propTypes = { ...withLocalizePropTypes, @@ -33,18 +35,18 @@ const defaultProps = { }; function SecuritySettingsPage(props) { + const waitForNavigate = useWaitForNavigation(); + const menuItems = [ { translationKey: 'twoFactorAuth.headerTitle', icon: Expensicons.Shield, - action: () => Navigation.navigate(ROUTES.SETTINGS_2FA), + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), }, { translationKey: 'closeAccountPage.closeAccount', icon: Expensicons.ClosedSign, - action: () => { - Navigation.navigate(ROUTES.SETTINGS_CLOSE); - }, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), }, ]; @@ -58,16 +60,17 @@ function SecuritySettingsPage(props) { > - {_.map(menuItems, (item) => ( - item.action()} - shouldShowRightIcon - /> - ))} + ({ + key: item.translationKey, + title: props.translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + }))} + shouldUseSingleExecution + /> diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 962b8bfd056e..8e79b46c615a 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,8 +1,8 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React, { useCallback, useEffect, useState } from 'react'; -import { View, ScrollView } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useEffect, useState} from 'react'; +import {View, ScrollView} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; @@ -17,7 +17,7 @@ import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import compose from '../../libs/compose'; import Avatar from '../../components/Avatar'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import { policyPropTypes, policyDefaultProps } from './withPolicy'; +import {policyPropTypes, policyDefaultProps} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import reportPropTypes from '../reportPropTypes'; import * as Policy from '../../libs/actions/Policy'; @@ -31,6 +31,10 @@ import * as ReportUtils from '../../libs/ReportUtils'; import withWindowDimensions from '../../components/withWindowDimensions'; import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import MenuItemList from '../../components/MenuItemList'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import {is} from 'core-js/core/object'; +import MenuItem from '../../components/MenuItem'; const propTypes = { ...policyPropTypes, @@ -69,6 +73,8 @@ function WorkspaceInitialPage(props) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = Boolean(policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); + const waitForNavigate = useWaitForNavigation(); + const {singleExecution, isExecuting} = useSingleExecution(); /** * Call the delete policy and hide the modal @@ -117,48 +123,49 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: () => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: () => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: () => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id))), error: hasCustomUnitsError, }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: () => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: () => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: () => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: () => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id)), + action: waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id))), brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, - action: () => + action: waitForNavigate(() => policy.outputCurrency === CONST.CURRENCY.USD ? ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '')) : setIsCurrencyModalOpen(true), + ), brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, ]; @@ -171,12 +178,12 @@ function WorkspaceInitialPage(props) { }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS }), + text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ADMINS), }, { icon: Expensicons.Hashtag, - text: props.translate('workspace.common.goToRoom', { roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE }), + text: props.translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), onSelected: () => goToRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE), }, ]; @@ -186,7 +193,7 @@ function WorkspaceInitialPage(props) { includeSafeAreaPaddingBottom={false} testID={WorkspaceInitialPage.displayName} > - {({ safeAreaPaddingBottomStyle }) => ( + {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy)} @@ -213,9 +220,9 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)} + onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -233,9 +240,9 @@ function WorkspaceInitialPage(props) { {!_.isEmpty(policy.name) && ( openEditor(policy.id)} + onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} accessibilityLabel={props.translate('workspace.common.settings')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -250,22 +257,19 @@ function WorkspaceInitialPage(props) { )} - ({ - key: item.translationKey, - disabled: hasPolicyCreationError, - interactive: !hasPolicyCreationError, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - onPress: item.action, - shouldShowRightIcon: true, - brickRoadIndicator: item.brickRoadIndicator, - })) - } - shouldUseSingleExecution - /> + {_.map(menuItems, (item) => ( + + ))} From 7d8fda9bc9759d542899b4e6c95374f1c8475125 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 9 Oct 2023 17:17:57 +0530 Subject: [PATCH 029/337] remove console log --- src/pages/settings/AboutPage/AboutPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 14cd7be5ef25..88900dcbf64a 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -84,7 +84,7 @@ function AboutPage(props) { action: Report.navigateToConciergeChat, }, ]; - console.log('props.isShortcutsModalOpen: ', props.isShortcutsModalOpen); + return ( Date: Mon, 9 Oct 2023 17:27:07 +0530 Subject: [PATCH 030/337] fix linters --- src/components/MenuItemList.js | 9 ++- src/pages/settings/AboutPage/AboutPage.js | 42 ++++++------ src/pages/settings/InitialSettingsPage.js | 64 +++++++++---------- .../settings/Security/SecuritySettingsPage.js | 21 +++--- src/pages/workspace/WorkspaceInitialPage.js | 50 +++++++-------- 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index 48190a4f4d05..f59c31dc6fda 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -1,12 +1,11 @@ +import PropTypes from 'prop-types'; import React from 'react'; import _ from 'underscore'; -import PropTypes from 'prop-types'; +import useSingleExecution from '../hooks/useSingleExecution'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; +import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; -import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; -import useSingleExecution from '../hooks/useSingleExecution'; -import useWaitForNavigation from '../hooks/useWaitForNavigation'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index 88900dcbf64a..ec3325bf1ed7 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,33 +1,32 @@ -import _ from 'underscore'; -import React from 'react'; import PropTypes from 'prop-types'; +import React from 'react'; import {ScrollView, View} from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; -import Navigation from '../../../libs/Navigation/Navigation'; -import ROUTES from '../../../ROUTES'; -import styles from '../../../styles/styles'; -import Text from '../../../components/Text'; -import TextLink from '../../../components/TextLink'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import Logo from '../../../../assets/images/new-expensify.svg'; +import pkg from '../../../../package.json'; import CONST from '../../../CONST'; +import ONYXKEYS from '../../../ONYXKEYS'; +import ROUTES from '../../../ROUTES'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import * as Expensicons from '../../../components/Icon/Expensicons'; +import MenuItemList from '../../../components/MenuItemList'; import ScreenWrapper from '../../../components/ScreenWrapper'; +import Text from '../../../components/Text'; +import TextLink from '../../../components/TextLink'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import MenuItem from '../../../components/MenuItem'; -import Logo from '../../../../assets/images/new-expensify.svg'; -import pkg from '../../../../package.json'; -import * as Report from '../../../libs/actions/Report'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import * as Environment from '../../../libs/Environment/Environment'; +import Navigation from '../../../libs/Navigation/Navigation'; +import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; import * as Link from '../../../libs/actions/Link'; +import * as Report from '../../../libs/actions/Report'; import compose from '../../../libs/compose'; -import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; +import styles from '../../../styles/styles'; import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; -import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; -import * as Environment from '../../../libs/Environment/Environment'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; -import MenuItemList from '../../../components/MenuItemList'; -import {withOnyx} from 'react-native-onyx'; -import ONYXKEYS from '../../../ONYXKEYS'; +import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; const propTypes = { ...withLocalizePropTypes, @@ -35,6 +34,10 @@ const propTypes = { isShortcutsModalOpen: PropTypes.bool, }; +const defaultProps = { + isShortcutsModalOpen: false, +}; + function getFlavor() { const bundleId = DeviceInfo.getBundleId(); if (bundleId.includes('dev')) { @@ -161,6 +164,7 @@ function AboutPage(props) { } AboutPage.propTypes = propTypes; +AboutPage.defaultProps = defaultProps; AboutPage.displayName = 'AboutPage'; export default compose( diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 5ed498fcb3d0..619aba06e026 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,49 +1,49 @@ import lodashGet from 'lodash/get'; -import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; -import {View} from 'react-native'; import PropTypes from 'prop-types'; -import _ from 'underscore'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import {withNetwork} from '../../components/OnyxProvider'; -import styles from '../../styles/styles'; -import Text from '../../components/Text'; -import * as Session from '../../libs/actions/Session'; +import _ from 'underscore'; +import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import Tooltip from '../../components/Tooltip'; +import ROUTES from '../../ROUTES'; +import SCREENS from '../../SCREENS'; import Avatar from '../../components/Avatar'; -import Navigation from '../../libs/Navigation/Navigation'; +import ConfirmModal from '../../components/ConfirmModal'; +import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; +import HeaderPageLayout from '../../components/HeaderPageLayout'; import * as Expensicons from '../../components/Icon/Expensicons'; import MenuItem from '../../components/MenuItem'; -import themeColors from '../../styles/themes/default'; -import SCREENS from '../../SCREENS'; -import ROUTES from '../../ROUTES'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import compose from '../../libs/compose'; -import CONST from '../../CONST'; -import Permissions from '../../libs/Permissions'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; -import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import {withNetwork} from '../../components/OnyxProvider'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import Text from '../../components/Text'; +import Tooltip from '../../components/Tooltip'; import bankAccountPropTypes from '../../components/bankAccountPropTypes'; import cardPropTypes from '../../components/cardPropTypes'; -import * as Wallet from '../../libs/actions/Wallet'; -import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import useLocalize from '../../hooks/useLocalize'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import Navigation from '../../libs/Navigation/Navigation'; +import Permissions from '../../libs/Permissions'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import ConfirmModal from '../../components/ConfirmModal'; import * as ReportUtils from '../../libs/ReportUtils'; +import * as UserUtils from '../../libs/UserUtils'; import * as Link from '../../libs/actions/Link'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import * as Session from '../../libs/actions/Session'; +import * as Wallet from '../../libs/actions/Wallet'; +import compose from '../../libs/compose'; +import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; +import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import * as UserUtils from '../../libs/UserUtils'; -import policyMemberPropType from '../policyMemberPropType'; -import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; -import * as CurrencyUtils from '../../libs/CurrencyUtils'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; -import useLocalize from '../../hooks/useLocalize'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import HeaderPageLayout from '../../components/HeaderPageLayout'; +import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import policyMemberPropType from '../policyMemberPropType'; const propTypes = { /* Onyx Props */ diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index d6f1d2b8fb86..d759e1a7fd53 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -1,22 +1,21 @@ -import _ from 'underscore'; +import PropTypes from 'prop-types'; import React from 'react'; -import {View, ScrollView} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import Navigation from '../../../libs/Navigation/Navigation'; +import _ from 'underscore'; +import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; import SCREENS from '../../../SCREENS'; -import styles from '../../../styles/styles'; import * as Expensicons from '../../../components/Icon/Expensicons'; -import themeColors from '../../../styles/themes/default'; -import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; -import MenuItem from '../../../components/MenuItem'; -import compose from '../../../libs/compose'; -import ONYXKEYS from '../../../ONYXKEYS'; import IllustratedHeaderPageLayout from '../../../components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '../../../components/LottieAnimations'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; import MenuItemList from '../../../components/MenuItemList'; +import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; +import Navigation from '../../../libs/Navigation/Navigation'; +import compose from '../../../libs/compose'; +import styles from '../../../styles/styles'; +import themeColors from '../../../styles/themes/default'; const propTypes = { ...withLocalizePropTypes, diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 8e79b46c615a..c2df7da8b729 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -1,40 +1,38 @@ -import _ from 'underscore'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; -import {View, ScrollView} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import PropTypes from 'prop-types'; -import Navigation from '../../libs/Navigation/Navigation'; +import _ from 'underscore'; +import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; -import styles from '../../styles/styles'; -import Tooltip from '../../components/Tooltip'; -import Text from '../../components/Text'; +import Avatar from '../../components/Avatar'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '../../components/ConfirmModal'; +import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import * as Expensicons from '../../components/Icon/Expensicons'; +import MenuItem from '../../components/MenuItem'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '../../components/ScreenWrapper'; +import Text from '../../components/Text'; +import Tooltip from '../../components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import HeaderWithBackButton from '../../components/HeaderWithBackButton'; -import compose from '../../libs/compose'; -import Avatar from '../../components/Avatar'; -import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import {policyPropTypes, policyDefaultProps} from './withPolicy'; -import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -import reportPropTypes from '../reportPropTypes'; -import * as Policy from '../../libs/actions/Policy'; +import withWindowDimensions from '../../components/withWindowDimensions'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import Navigation from '../../libs/Navigation/Navigation'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import CONST from '../../CONST'; +import * as ReportUtils from '../../libs/ReportUtils'; +import * as Policy from '../../libs/actions/Policy'; import * as ReimbursementAccount from '../../libs/actions/ReimbursementAccount'; -import ONYXKEYS from '../../ONYXKEYS'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import compose from '../../libs/compose'; +import styles from '../../styles/styles'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import * as ReportUtils from '../../libs/ReportUtils'; -import withWindowDimensions from '../../components/withWindowDimensions'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; -import MenuItemList from '../../components/MenuItemList'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import {is} from 'core-js/core/object'; -import MenuItem from '../../components/MenuItem'; +import reportPropTypes from '../reportPropTypes'; +import {policyDefaultProps, policyPropTypes} from './withPolicy'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; const propTypes = { ...policyPropTypes, From ccf8f4e213c15aa88f1fc584538e4a63263b11d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Mon, 9 Oct 2023 16:12:50 +0200 Subject: [PATCH 031/337] action types fix and cardID added to api call --- src/pages/settings/Wallet/ExpensifyCardPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 3e2d8632228a..fe16b9014e19 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -62,16 +62,16 @@ function ExpensifyCardPage({ const handleRevealDetails = () => { dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealVirtualCardDetails') + API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID: virtualCard.cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: response.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: response.message}); return; } - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.success, payload: response}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.SUCCESS, payload: response}); }) .catch((err) => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.fail, payload: err.message}); + dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: err.message}); }); }; From ef623a4c603bd4b4909f5f740e0d310d047cee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 10 Oct 2023 13:10:23 +0200 Subject: [PATCH 032/337] address refactoring --- .../settings/Wallet/ExpensifyCardPage.js | 2 +- .../settings/Wallet/revealCardDetailsUtils.ts | 32 ++++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index fe16b9014e19..892aea0495da 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -104,7 +104,7 @@ function ExpensifyCardPage({ pan={details.pan} expiration={details.expiration} cvv={details.cvv} - privatePersonalDetails={details.privatePersonalDetails} + privatePersonalDetails={{address: details.address}} /> ) : ( Date: Wed, 11 Oct 2023 01:03:30 +0530 Subject: [PATCH 033/337] added requested changes --- src/components/MenuItemList.js | 2 +- src/pages/settings/AboutPage/AboutPage.js | 147 +++++++++--------- src/pages/settings/InitialSettingsPage.js | 64 ++++---- .../settings/Security/SecuritySettingsPage.js | 49 +++--- src/pages/workspace/WorkspaceInitialPage.js | 7 +- 5 files changed, 138 insertions(+), 131 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index f59c31dc6fda..c7e4a2b7c9ca 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -40,7 +40,7 @@ function MenuItemList(props) { <> {_.map(props.menuItems, (menuItemProps) => ( secondaryInteraction(menuItemProps.link, e) : undefined} ref={(el) => (popoverAnchor = el)} shouldBlockSelection={Boolean(menuItemProps.link)} diff --git a/src/pages/settings/AboutPage/AboutPage.js b/src/pages/settings/AboutPage/AboutPage.js index ec3325bf1ed7..97e41f433615 100644 --- a/src/pages/settings/AboutPage/AboutPage.js +++ b/src/pages/settings/AboutPage/AboutPage.js @@ -1,32 +1,32 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import _ from 'underscore'; +import React, {useMemo, useRef} from 'react'; import {ScrollView, View} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import Logo from '../../../../assets/images/new-expensify.svg'; -import pkg from '../../../../package.json'; +import PropTypes from 'prop-types'; +import DeviceInfo from 'react-native-device-info'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; +import styles from '../../../styles/styles'; +import Text from '../../../components/Text'; +import TextLink from '../../../components/TextLink'; import CONST from '../../../CONST'; import ONYXKEYS from '../../../ONYXKEYS'; -import ROUTES from '../../../ROUTES'; -import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import * as Expensicons from '../../../components/Icon/Expensicons'; -import MenuItemList from '../../../components/MenuItemList'; import ScreenWrapper from '../../../components/ScreenWrapper'; -import Text from '../../../components/Text'; -import TextLink from '../../../components/TextLink'; +import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; -import useWaitForNavigation from '../../../hooks/useWaitForNavigation'; -import * as Environment from '../../../libs/Environment/Environment'; -import Navigation from '../../../libs/Navigation/Navigation'; -import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; -import * as Link from '../../../libs/actions/Link'; +import MenuItemList from '../../../components/MenuItemList'; +import Logo from '../../../../assets/images/new-expensify.svg'; +import pkg from '../../../../package.json'; import * as Report from '../../../libs/actions/Report'; +import * as Link from '../../../libs/actions/Link'; import compose from '../../../libs/compose'; -import styles from '../../../styles/styles'; -import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '../../home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../../home/report/ContextMenu/ContextMenuActions'; +import * as KeyboardShortcuts from '../../../libs/actions/KeyboardShortcuts'; +import * as Environment from '../../../libs/Environment/Environment'; const propTypes = { ...withLocalizePropTypes, @@ -50,43 +50,59 @@ function getFlavor() { } function AboutPage(props) { - let popoverAnchor; + const {translate, isShortcutsModalOpen} = props; + const popoverAnchor = useRef(null); const waitForNavigate = useWaitForNavigation(); - const menuItems = [ - { - translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', - icon: Expensicons.Link, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', - icon: Expensicons.Keyboard, - action: KeyboardShortcuts.showKeyboardShortcutModal, - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewTheCode', - icon: Expensicons.Eye, - iconRight: Expensicons.NewWindow, - action: () => { - Link.openExternalLink(CONST.GITHUB_URL); + + const menuItems = useMemo(() => { + const baseMenuItems = [ + { + translationKey: 'initialSettingsPage.aboutPage.appDownloadLinks', + icon: Expensicons.Link, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_APP_DOWNLOAD_LINKS)), }, - link: CONST.GITHUB_URL, - }, - { - translationKey: 'initialSettingsPage.aboutPage.viewOpenJobs', - icon: Expensicons.MoneyBag, - iconRight: Expensicons.NewWindow, - action: () => { - Link.openExternalLink(CONST.UPWORK_URL); + { + translationKey: 'initialSettingsPage.aboutPage.viewKeyboardShortcuts', + icon: Expensicons.Keyboard, + action: KeyboardShortcuts.showKeyboardShortcutModal, }, - link: CONST.UPWORK_URL, - }, - { - translationKey: 'initialSettingsPage.aboutPage.reportABug', - icon: Expensicons.Bug, - action: Report.navigateToConciergeChat, - }, - ]; + { + translationKey: 'initialSettingsPage.aboutPage.viewTheCode', + icon: Expensicons.Eye, + iconRight: Expensicons.NewWindow, + action: () => { + Link.openExternalLink(CONST.GITHUB_URL); + }, + link: CONST.GITHUB_URL, + }, + { + translationKey: 'initialSettingsPage.aboutPage.viewOpenJobs', + icon: Expensicons.MoneyBag, + iconRight: Expensicons.NewWindow, + action: () => { + Link.openExternalLink(CONST.UPWORK_URL); + }, + link: CONST.UPWORK_URL, + }, + { + translationKey: 'initialSettingsPage.aboutPage.reportABug', + icon: Expensicons.Bug, + action: Report.navigateToConciergeChat, + }, + ]; + return _.map(baseMenuItems, (item) => ({ + key: item.translationKey, + title: translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + disabled: isShortcutsModalOpen, + onPress: item.action, + shouldShowRightIcon: true, + onSecondaryInteraction: !_.isEmpty(item.link) ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) : undefined, + ref: popoverAnchor, + shouldBlockSelection: Boolean(item.link), + })); + }, [isShortcutsModalOpen, translate, waitForNavigate]); return ( ( <> Navigation.goBack(ROUTES.SETTINGS)} /> @@ -113,24 +129,11 @@ function AboutPage(props) { > v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`} - {props.translate('initialSettingsPage.aboutPage.description')} + {translate('initialSettingsPage.aboutPage.description')} ({ - key: item.translationKey, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - disabled: props.isShortcutsModalOpen, - onPress: item.action, - shouldShowRightIcon: true, - onSecondaryInteraction: !_.isEmpty(item.link) - ? (e) => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, item.link, popoverAnchor) - : undefined, - ref: (el) => (popoverAnchor = el), - shouldBlockSelection: Boolean(item.link), - }))} + menuItems={menuItems} shouldUseSingleExecution /> @@ -139,19 +142,19 @@ function AboutPage(props) { style={[styles.chatItemMessageHeaderTimestamp]} numberOfLines={1} > - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase1')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase2')} {' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase3')}{' '} - {props.translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} + {translate('initialSettingsPage.readTheTermsAndPrivacy.phrase4')} . diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 619aba06e026..5ed498fcb3d0 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -1,49 +1,49 @@ import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; import _ from 'underscore'; -import CONST from '../../CONST'; -import ONYXKEYS from '../../ONYXKEYS'; -import ROUTES from '../../ROUTES'; -import SCREENS from '../../SCREENS'; -import Avatar from '../../components/Avatar'; -import ConfirmModal from '../../components/ConfirmModal'; +import {withOnyx} from 'react-native-onyx'; import CurrentUserPersonalDetailsSkeletonView from '../../components/CurrentUserPersonalDetailsSkeletonView'; -import HeaderPageLayout from '../../components/HeaderPageLayout'; -import * as Expensicons from '../../components/Icon/Expensicons'; -import MenuItem from '../../components/MenuItem'; -import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import {withNetwork} from '../../components/OnyxProvider'; -import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import styles from '../../styles/styles'; import Text from '../../components/Text'; +import * as Session from '../../libs/actions/Session'; +import ONYXKEYS from '../../ONYXKEYS'; import Tooltip from '../../components/Tooltip'; -import bankAccountPropTypes from '../../components/bankAccountPropTypes'; -import cardPropTypes from '../../components/cardPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import useLocalize from '../../hooks/useLocalize'; -import useSingleExecution from '../../hooks/useSingleExecution'; -import useWaitForNavigation from '../../hooks/useWaitForNavigation'; -import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import Avatar from '../../components/Avatar'; import Navigation from '../../libs/Navigation/Navigation'; +import * as Expensicons from '../../components/Icon/Expensicons'; +import MenuItem from '../../components/MenuItem'; +import themeColors from '../../styles/themes/default'; +import SCREENS from '../../SCREENS'; +import ROUTES from '../../ROUTES'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import compose from '../../libs/compose'; +import CONST from '../../CONST'; import Permissions from '../../libs/Permissions'; -import * as PolicyUtils from '../../libs/PolicyUtils'; -import * as ReportUtils from '../../libs/ReportUtils'; -import * as UserUtils from '../../libs/UserUtils'; -import * as Link from '../../libs/actions/Link'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../components/withCurrentUserPersonalDetails'; import * as PaymentMethods from '../../libs/actions/PaymentMethods'; -import * as Session from '../../libs/actions/Session'; +import bankAccountPropTypes from '../../components/bankAccountPropTypes'; +import cardPropTypes from '../../components/cardPropTypes'; import * as Wallet from '../../libs/actions/Wallet'; -import compose from '../../libs/compose'; -import styles from '../../styles/styles'; -import themeColors from '../../styles/themes/default'; import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; +import * as PolicyUtils from '../../libs/PolicyUtils'; +import ConfirmModal from '../../components/ConfirmModal'; +import * as ReportUtils from '../../libs/ReportUtils'; +import * as Link from '../../libs/actions/Link'; +import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; -import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; -import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; +import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; +import PressableWithoutFeedback from '../../components/Pressable/PressableWithoutFeedback'; +import useLocalize from '../../hooks/useLocalize'; +import useSingleExecution from '../../hooks/useSingleExecution'; +import useWaitForNavigation from '../../hooks/useWaitForNavigation'; +import HeaderPageLayout from '../../components/HeaderPageLayout'; const propTypes = { /* Onyx Props */ diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js index d759e1a7fd53..dac4ed4f872f 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.js +++ b/src/pages/settings/Security/SecuritySettingsPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -34,24 +34,36 @@ const defaultProps = { }; function SecuritySettingsPage(props) { + const {translate} = props; const waitForNavigate = useWaitForNavigation(); - const menuItems = [ - { - translationKey: 'twoFactorAuth.headerTitle', - icon: Expensicons.Shield, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), - }, - { - translationKey: 'closeAccountPage.closeAccount', - icon: Expensicons.ClosedSign, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), - }, - ]; + const menuItems = useMemo(() => { + const baseMenuItems = [ + { + translationKey: 'twoFactorAuth.headerTitle', + icon: Expensicons.Shield, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA)), + }, + { + translationKey: 'closeAccountPage.closeAccount', + icon: Expensicons.ClosedSign, + action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), + }, + ]; + + return _.map(baseMenuItems, (item) => ({ + key: item.translationKey, + title: translate(item.translationKey), + icon: item.icon, + iconRight: item.iconRight, + onPress: item.action, + shouldShowRightIcon: true, + })); + }, [translate, waitForNavigate]); return ( Navigation.goBack(ROUTES.SETTINGS)} shouldShowBackButton illustration={LottieAnimations.Safe} @@ -60,14 +72,7 @@ function SecuritySettingsPage(props) { ({ - key: item.translationKey, - title: props.translate(item.translationKey), - icon: item.icon, - iconRight: item.iconRight, - onPress: item.action, - shouldShowRightIcon: true, - }))} + menuItems={menuItems} shouldUseSingleExecution /> diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index c2df7da8b729..7ab5279cc885 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -159,11 +159,10 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, - action: waitForNavigate(() => + action: () => policy.outputCurrency === CONST.CURRENCY.USD - ? ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '')) + ? waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRoute().replace(/\?.*/, '', waitForNavigate)))() : setIsCurrencyModalOpen(true), - ), brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, ]; @@ -258,7 +257,7 @@ function WorkspaceInitialPage(props) { {_.map(menuItems, (item) => ( Date: Wed, 11 Oct 2023 01:50:51 +0530 Subject: [PATCH 034/337] added comment why execption made for mapping menuitems --- src/components/MenuItemList.js | 6 +++--- src/pages/workspace/WorkspaceInitialPage.js | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js index c7e4a2b7c9ca..d809efc7cb90 100644 --- a/src/components/MenuItemList.js +++ b/src/components/MenuItemList.js @@ -1,11 +1,11 @@ -import PropTypes from 'prop-types'; import React from 'react'; import _ from 'underscore'; +import PropTypes from 'prop-types'; import useSingleExecution from '../hooks/useSingleExecution'; -import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; -import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; import MenuItem from './MenuItem'; import menuItemPropTypes from './menuItemPropTypes'; +import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions'; const propTypes = { /** An array of props that are pass to individual MenuItem components */ diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 7ab5279cc885..97e5d5990de2 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -254,6 +254,10 @@ function WorkspaceInitialPage(props) { )} + {/* + Ideally we should use MenuList component for MenuItems with singleExecution/Navigation Actions. + But Here we need to have a `isExecuting` for profile details click actions also so we are directly mapping menuItems. + */} {_.map(menuItems, (item) => ( Date: Tue, 10 Oct 2023 16:07:59 -0700 Subject: [PATCH 035/337] Create Tips-And-Tricks.md --- .../getting-started/Tips-And-Tricks.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md diff --git a/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md b/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md new file mode 100644 index 000000000000..e4fd2d4bc44c --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/Tips-And-Tricks.md @@ -0,0 +1,72 @@ +--- +title: Tips and Tricks +description: How to get started with setup tips for your Expensify account +--- + +# Overview +In this article, we'll outline helpful tips for using Expensify, such as keyboard shortcuts and text formatting. + +# How to Format Text in Expensify +You can use basic markdown in report comments to emphasize or clarify your sentiments. This includes italicizing, bolding, and strikethrough for text, as well as adding basic hyperlinks. +Formatting is consistent across both web and mobile applications, with three markdown options available for your report comments: +- **Bold:** Place an asterisk on either side (*bold*) +- **Italicize:** Place an underscore on either side (_italic_) +- **Strikethrough:** Place a tilde on either side (~strikethrough~) + +# How to Use Keyboard Shortcuts +Keyboard shortcuts can speed things up and simplify tasks. Expensify offers several shortcuts for your convenience. Let's explore them! +- **Shift + ?** - Opens the keyboard shortcuts dialog +- **Shift + G** - Prompts you for a reportID to open the report page for a specific report +- **ESC** - Closes any shortcut dialog window +- **Ctrl+Enter** - Submit a comment on a report from the comment field in the Report History & Comments section. +- **Shift + P** - Takes you to the report’s policy when you’re on a report +- **Shift + →** - Go to the next report +- **Shift + ←** - Go to the previous report +- **Shift + R** - Reloads the current page + +# How to Create a Copy of a Report +If you have identical monthly expenses and want to copy them easily, visit your Reports page, check the box next to the report you would like to duplicate, and click "Copy" to duplicate all expenses (excluding receipt images). +If you prefer, you can create a standard template for certain expenses: +1. Go to the Reports page. +2. Click "New Report." +3. Assign an easily searchable name to the report. +4. Click the green '+' button to add an expense. +5. Choose "New Expense". +6. Select the type of expense (e.g., regular expense, distance, time, etc.). +7. Enter the expense details, code, and any relevant description. +8. Click "Save." +**Pro Tip:** If you use Scheduled Submit, place the template report under your individual workspace to avoid accidental submission. When you're ready to use it, check the report box, copy it, and make necessary changes to the name and workspace. + +# How to Enable Location Access on Web +If you’d like to use features that rely on your current location you will need to enable location permissions for Expensify. You can find instructions for how to enable location settings on the three most common web browsers below. If your browser is not in the list then please do a web search for your browser and “enable location settings”. + +## Chrome +1. Open Chrome +2. At the top right, click the three-dot Menu > Settings +3. Click “Privacy and Security” and then “Site Settings” +4. Click Location +5. Check the “Not allowed to see your location” list to make sure expensify.com and new.expensify.com are not listed. If they are, click the delete icon next to them to allow location access + +## Firefox +1. Open Firefox +2. In the URL bar enter “about:preferences” +3. On the left hand side select “Privacy & Security” +4. Scroll down to Permissions +5. Click on Settings next to Location +6. If location access is blocked for expensify.com or new.expensify.com, you can update it here to allow access + +## Safari +1. In the top menu bar click Safari +2. Then select Settings > Websites +3. Click Location on the left hand side +4. If expensify.com or new.expensify.com have “Deny” set as their access, update it to “Ask” or “Allow” + +# Which browser works best with Expensify? +We recommend using Google Chrome, but you can use Expensify on most major browsers, such as: +- [Google Chrome](www.google.com/chrome/) +- [Mozilla Firefox](www.mozilla.com/firefox) +- [Microsoft Edge](www.microsoft.com/edge) +- [Microsoft Internet Explorer](www.microsoft.com/ie). Please note: Microsoft has discontinued support and security updates for all versions below Version 11. This means those older versions may not work well. Due to the lack of security updates for the older versions, parts of our site may not be accessible. Please update your IE version or choose a different browser. +- [Apple Safari (Apple devices only)](www.apple.com/safari) +- [Opera](www.opera.com) +It's always best practice to make sure you have the most recent updates for your browser and keep your operating system up to date. From 3c41ef5638447698d902e9285a8505e5254d6b92 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 11 Oct 2023 11:43:12 +0200 Subject: [PATCH 036/337] Fixes in onyxkeys --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b01ffbc37141..495b9c4595f9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -370,7 +370,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; [ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; - [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTag; + [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index c0bb7e539bec..7afac2ce5b67 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -13,10 +13,7 @@ type UnitRate = {rate: number}; * These are policies that we can use to create reports with in NewDot. */ function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { - if (!policies) { - return; - } - return (Object.values(policies) ?? []).filter( + return (Object.values(policies ?? {}) ?? []).filter( (policy): policy is Policy => policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); @@ -91,8 +88,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb */ function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean { return ( - policy !== null && - policy && + !!policy && policy?.isPolicyExpenseChatEnabled && policy?.role === CONST.POLICY.ROLE.ADMIN && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0) @@ -120,7 +116,7 @@ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === C * We only return members without errors. Otherwise, the members with errors would immediately be removed before the user has a chance to read the error. */ function getMemberAccountIDsForWorkspace(policyMembers: OnyxEntry, personalDetails: OnyxEntry): MemberEmailsToAccountIDs { - const memberEmailsToAccountIDs: Record = {}; + const memberEmailsToAccountIDs: MemberEmailsToAccountIDs = {}; Object.keys(policyMembers ?? {}).forEach((accountID) => { const member = policyMembers?.[accountID]; if (Object.keys(member?.errors ?? {})?.length > 0) { From 045068c1e89f10b93f67f386f186b2689ea44b6d Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 13:35:14 +0200 Subject: [PATCH 037/337] [TS migration] Migrate 'useDragAndDrop.js' hook and 'withEnvironment.js' HOC --- ...withEnvironment.js => withEnvironment.tsx} | 39 ++++++++++--------- .../{useEnvironment.js => useEnvironment.ts} | 10 ++++- src/pages/ShareCodePage.js | 7 +++- 3 files changed, 34 insertions(+), 22 deletions(-) rename src/components/{withEnvironment.js => withEnvironment.tsx} (54%) rename src/hooks/{useEnvironment.js => useEnvironment.ts} (59%) diff --git a/src/components/withEnvironment.js b/src/components/withEnvironment.tsx similarity index 54% rename from src/components/withEnvironment.js rename to src/components/withEnvironment.tsx index 3aa9b86e82c8..6fe84860bb53 100644 --- a/src/components/withEnvironment.js +++ b/src/components/withEnvironment.tsx @@ -1,21 +1,26 @@ -import React, {createContext, useState, useEffect, forwardRef, useContext, useMemo} from 'react'; -import PropTypes from 'prop-types'; +import React, {ComponentType, RefAttributes, ReactNode, createContext, useState, useEffect, forwardRef, useContext, useMemo} from 'react'; +import {ValueOf} from 'type-fest'; import * as Environment from '../libs/Environment/Environment'; import CONST from '../CONST'; import getComponentDisplayName from '../libs/getComponentDisplayName'; -const EnvironmentContext = createContext(null); +type EnvironmentProviderProps = { + /** Actual content wrapped by this component */ + children: ReactNode; +}; -const environmentPropTypes = { +type EnvironmentContextValue = { /** The string value representing the current environment */ - environment: PropTypes.string.isRequired, + environment: ValueOf; /** The string value representing the URL of the current environment */ - environmentURL: PropTypes.string.isRequired, + environmentURL: string; }; -function EnvironmentProvider({children}) { - const [environment, setEnvironment] = useState(CONST.ENVIRONMENT.PRODUCTION); +const EnvironmentContext = createContext(null); + +function EnvironmentProvider({children}: EnvironmentProviderProps) { + const [environment, setEnvironment] = useState>(CONST.ENVIRONMENT.PRODUCTION); const [environmentURL, setEnvironmentURL] = useState(CONST.NEW_EXPENSIFY_URL); useEffect(() => { @@ -24,7 +29,7 @@ function EnvironmentProvider({children}) { }, []); const contextValue = useMemo( - () => ({ + (): EnvironmentContextValue => ({ environment, environmentURL, }), @@ -35,14 +40,11 @@ function EnvironmentProvider({children}) { } EnvironmentProvider.displayName = 'EnvironmentProvider'; -EnvironmentProvider.propTypes = { - /** Actual content wrapped by this component */ - children: PropTypes.node.isRequired, -}; -export default function withEnvironment(WrappedComponent) { - const WithEnvironment = forwardRef((props, ref) => { - const {environment, environmentURL} = useContext(EnvironmentContext); +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function withEnvironment(WrappedComponent: ComponentType) { + const WithEnvironment: ComponentType>> = forwardRef((props, ref) => { + const {environment, environmentURL} = useContext(EnvironmentContext) ?? {}; return ( & { + isProduction: boolean; + isDevelopment: boolean; +}; + +export default function useEnvironment(): UseEnvironment { + const {environment, environmentURL} = useContext(EnvironmentContext) ?? {}; return { environment, environmentURL, diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index e6d36ebc7070..f0f0ef7d09fd 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -1,6 +1,7 @@ import React from 'react'; import {View, ScrollView} from 'react-native'; import _ from 'underscore'; +import PropTypes from 'prop-types'; import ScreenWrapper from '../components/ScreenWrapper'; import HeaderWithBackButton from '../components/HeaderWithBackButton'; import Navigation from '../libs/Navigation/Navigation'; @@ -20,16 +21,18 @@ import CONST from '../CONST'; import ContextMenuItem from '../components/ContextMenuItem'; import * as UserUtils from '../libs/UserUtils'; import ROUTES from '../ROUTES'; -import withEnvironment, {environmentPropTypes} from '../components/withEnvironment'; +import withEnvironment from '../components/withEnvironment'; import * as Url from '../libs/Url'; const propTypes = { /** The report currently being looked at */ report: reportPropTypes, + /** The string value representing the URL of the current environment */ + environmentURL: PropTypes.string.isRequired, + ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, - ...environmentPropTypes, }; const defaultProps = { From 70c8123e40e181c694755a7bfec0a90e53f8b856 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 14:30:58 +0200 Subject: [PATCH 038/337] Update getComponentDisplayName to accept types with generics --- src/components/withEnvironment.tsx | 2 +- src/libs/getComponentDisplayName.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/withEnvironment.tsx b/src/components/withEnvironment.tsx index 6fe84860bb53..35e4d291da34 100644 --- a/src/components/withEnvironment.tsx +++ b/src/components/withEnvironment.tsx @@ -56,7 +56,7 @@ export default function withEnvironment(component: ComponentType): string { return component.displayName ?? component.name ?? 'Component'; } From ce478db6504993f6c6216803bc1d5f7ef2e48790 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 11 Oct 2023 14:39:02 +0200 Subject: [PATCH 039/337] Put EnvironmentValue into a separate type --- src/components/withEnvironment.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/withEnvironment.tsx b/src/components/withEnvironment.tsx index 35e4d291da34..5476c0562109 100644 --- a/src/components/withEnvironment.tsx +++ b/src/components/withEnvironment.tsx @@ -9,9 +9,11 @@ type EnvironmentProviderProps = { children: ReactNode; }; +type EnvironmentValue = ValueOf; + type EnvironmentContextValue = { /** The string value representing the current environment */ - environment: ValueOf; + environment: EnvironmentValue; /** The string value representing the URL of the current environment */ environmentURL: string; @@ -20,7 +22,7 @@ type EnvironmentContextValue = { const EnvironmentContext = createContext(null); function EnvironmentProvider({children}: EnvironmentProviderProps) { - const [environment, setEnvironment] = useState>(CONST.ENVIRONMENT.PRODUCTION); + const [environment, setEnvironment] = useState(CONST.ENVIRONMENT.PRODUCTION); const [environmentURL, setEnvironmentURL] = useState(CONST.NEW_EXPENSIFY_URL); useEffect(() => { From bb6cd818ebb3b9963c67da198870e8ca5350d19b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 11 Oct 2023 16:17:31 +0200 Subject: [PATCH 040/337] migrate useReportScrollManager and ReportScreenContext to TypeScript --- .../{index.native.js => index.native.ts} | 17 ++++++++++------- .../{index.js => index.ts} | 18 ++++++++++-------- src/pages/home/ReportScreenContext.js | 6 ------ src/pages/home/ReportScreenContext.ts | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 21 deletions(-) rename src/hooks/useReportScrollManager/{index.native.js => index.native.ts} (56%) rename src/hooks/useReportScrollManager/{index.js => index.ts} (58%) delete mode 100644 src/pages/home/ReportScreenContext.js create mode 100644 src/pages/home/ReportScreenContext.ts diff --git a/src/hooks/useReportScrollManager/index.native.js b/src/hooks/useReportScrollManager/index.native.ts similarity index 56% rename from src/hooks/useReportScrollManager/index.native.js rename to src/hooks/useReportScrollManager/index.native.ts index d44a40222ca5..e40ff049ca12 100644 --- a/src/hooks/useReportScrollManager/index.native.js +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -1,27 +1,30 @@ import {useContext, useCallback} from 'react'; -import {ActionListContext} from '../../pages/home/ReportScreenContext'; +import {ActionListContext, ActionListContextType} from '../../pages/home/ReportScreenContext'; -function useReportScrollManager() { +function useReportScrollManager(): { + ref: ActionListContextType; + scrollToIndex: (index: number) => void; + scrollToBottom: () => void; +} { const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. * - * @param {Object} index */ - const scrollToIndex = (index) => { - if (!flatListRef.current) { + const scrollToIndex = (index: number) => { + if (!flatListRef?.current) { return; } - flatListRef.current.scrollToIndex(index); + flatListRef.current.scrollToIndex({index}); }; /** * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - if (!flatListRef.current) { + if (!flatListRef?.current) { return; } diff --git a/src/hooks/useReportScrollManager/index.js b/src/hooks/useReportScrollManager/index.ts similarity index 58% rename from src/hooks/useReportScrollManager/index.js rename to src/hooks/useReportScrollManager/index.ts index 9a3303504b92..c466b4050266 100644 --- a/src/hooks/useReportScrollManager/index.js +++ b/src/hooks/useReportScrollManager/index.ts @@ -1,29 +1,31 @@ import {useContext, useCallback} from 'react'; -import {ActionListContext} from '../../pages/home/ReportScreenContext'; +import {ActionListContext, ActionListContextType} from '../../pages/home/ReportScreenContext'; -function useReportScrollManager() { +function useReportScrollManager(): { + ref: ActionListContextType; + scrollToIndex: (index: number, isEditing: boolean) => void; + scrollToBottom: () => void; +} { const flatListRef = useContext(ActionListContext); /** * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because * we are editing a comment. * - * @param {Object} index - * @param {Boolean} isEditing */ - const scrollToIndex = (index, isEditing) => { - if (!flatListRef.current || isEditing) { + const scrollToIndex = (index: number, isEditing: boolean) => { + if (!flatListRef?.current || isEditing) { return; } - flatListRef.current.scrollToIndex(index); + flatListRef.current.scrollToIndex({index}); }; /** * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - if (!flatListRef.current) { + if (!flatListRef?.current) { return; } diff --git a/src/pages/home/ReportScreenContext.js b/src/pages/home/ReportScreenContext.js deleted file mode 100644 index 1e8d30cf7585..000000000000 --- a/src/pages/home/ReportScreenContext.js +++ /dev/null @@ -1,6 +0,0 @@ -import {createContext} from 'react'; - -const ActionListContext = createContext(); -const ReactionListContext = createContext(); - -export {ActionListContext, ReactionListContext}; diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts new file mode 100644 index 000000000000..a74c6d9797ff --- /dev/null +++ b/src/pages/home/ReportScreenContext.ts @@ -0,0 +1,17 @@ +import {RefObject, createContext} from 'react'; +import {FlatList, GestureResponderEvent} from 'react-native'; + +type ReactionListRefType = { + showReactionList: (event: GestureResponderEvent | undefined, reactionListAnchor: Element, emojiName: string, reportActionID: string) => void; + hideReactionList: () => void; + isActiveReportAction: (actionID: number | string) => boolean; +}; + +type ActionListContextType = RefObject> | null; +type ReactionListContextType = RefObject | null; + +const ActionListContext = createContext(null); +const ReactionListContext = createContext(null); + +export {ActionListContext, ReactionListContext}; +export type {ReactionListRefType, ActionListContextType, ReactionListContextType}; From f7ba68a4757a73341437184996652df78490cf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 11 Oct 2023 16:22:19 +0200 Subject: [PATCH 041/337] refactoring --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Card.ts | 23 ++++++++ .../settings/Wallet/ExpensifyCardPage.js | 36 +++++------ .../settings/Wallet/revealCardDetailsUtils.ts | 59 ------------------- src/types/onyx/Card.ts | 15 +++++ 6 files changed, 56 insertions(+), 79 deletions(-) create mode 100644 src/libs/actions/Card.ts delete mode 100644 src/pages/settings/Wallet/revealCardDetailsUtils.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 9d9d4e0a4d70..2090ee4b76f2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -838,6 +838,7 @@ export default { revealDetails: 'Reveal details', copyCardNumber: 'Copy card number', }, + cardDetailsLoadingFailure: 'An error occurred loading card details, please try again.', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2ad75685a1a2..ed44aeedd6d9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -834,6 +834,7 @@ export default { revealDetails: 'Revelar detalles', copyCardNumber: 'Copiar número de la tarjeta', }, + cardDetailsLoadingFailure: 'Ocurrió un error al cargar los detalles de la tarjeta. Por favor, inténtalo de nuevo.', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts new file mode 100644 index 000000000000..f75c77c928d9 --- /dev/null +++ b/src/libs/actions/Card.ts @@ -0,0 +1,23 @@ +import * as API from '../API'; +import CONST from '../../CONST'; +import {TCardDetails} from '../../types/onyx/Card'; +import * as Localize from '../Localize'; + +function revealVirtualCardDetails(cardID: string): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID}) + .then((response) => { + if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + reject(response.message || Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + return; + } + resolve(response); + }) + .catch((err) => { + reject(err.message); + }); + }); +} + +export default {revealVirtualCardDetails}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 892aea0495da..4e94a1ecfe95 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useReducer} from 'react'; +import React, {useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -18,10 +18,7 @@ import styles from '../../../styles/styles'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; -// eslint-disable-next-line rulesdir/no-api-in-views -import * as API from '../../../libs/API'; -import CONST from '../../../CONST'; -import * as revealCardDetailsUtils from './revealCardDetailsUtils'; +import Card from '../../../libs/actions/Card'; const propTypes = { /* Onyx Props */ @@ -51,7 +48,10 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const [{isLoading, details, error}, dispatch] = useReducer(revealCardDetailsUtils.reducer, revealCardDetailsUtils.initialState); + // card details state + const [isLoading, setIsLoading] = useState(false); + const [details, setDetails] = useState({}); + const [errorMessage, setErrorMessage] = useState(''); if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; @@ -60,19 +60,15 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); const handleRevealDetails = () => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.start}); - // eslint-disable-next-line rulesdir/no-api-in-views,rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealVirtualCardDetails', {cardID: virtualCard.cardID}) - .then((response) => { - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: response.message}); - return; - } - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.SUCCESS, payload: response}); - }) - .catch((err) => { - dispatch({type: revealCardDetailsUtils.ACTION_TYPES.FAIL, payload: err.message}); - }); + setIsLoading(true); + // We can't store the response in Onyx for security reasons. + // That is this action is handled manually and the response is stored in a local state + // Hence the eslint disable here. + // eslint-disable-next-line rulesdir/no-thenable-actions-in-views + Card.revealVirtualCardDetails(virtualCard.cardID) + .then(setDetails) + .catch(setErrorMessage) + .finally(() => setIsLoading(false)); }; return ( @@ -113,7 +109,7 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.walletCardNumber} shouldShowRightComponent - error={error} + error={errorMessage} rightComponent={