From a551927b2bf0b97860a5a4efa8a73f9659c6ea24 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 9 Jan 2024 17:27:41 -0800 Subject: [PATCH 01/82] Add optimistic violations to money request edits Update everywhere that accesses an arg on policy to use {} as default arg for safety, since we can't use optional chaining --- src/libs/actions/IOU.js | 88 +++++++++++++++++++++++++++++------- src/pages/EditRequestPage.js | 30 +++++++----- 2 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f2584cb8accd..31fee6d2b9d7 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -341,7 +341,7 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, - policy, + policy = {}, policyTags, policyCategories, hasOutstandingChildRequest = false, @@ -918,13 +918,16 @@ function createDistanceRequest(report, participant, comment, created, category, * @param {String} transactionThreadReportID * @param {Object} transactionChanges * @param {String} [transactionChanges.created] Present when updated the date field + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories * @param {Boolean} onlyIncludeChangedFields * When 'true', then the returned params will only include the transaction details for the fields that were changed. * When `false`, then the returned params will include all the transaction details, regardless of which fields were changed. * This setting is necessary while the UpdateDistanceRequest API is refactored to be fully 1:1:1 in https://github.com/Expensify/App/issues/28358 * @returns {object} */ -function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, onlyIncludeChangedFields) { +function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy = {}, policyTags, policyCategories, onlyIncludeChangedFields) { const optimisticData = []; const successData = []; const failureData = []; @@ -1050,6 +1053,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } } + // Add optimistic transaction violations + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + }); + // Clear out the error fields and loading states on success successData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1088,6 +1098,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: iouReport, }); + // Reset transaction violations to their original state + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + }); + return { params, onyxData: {optimisticData, successData, failureData}, @@ -1100,12 +1117,15 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t * @param {String} transactionID * @param {String} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestDate(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { created: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestDate', params, onyxData); } @@ -1115,12 +1135,15 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { merchant: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestMerchant', params, onyxData); } @@ -1130,12 +1153,15 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} tag + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { +function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag, policy, policyTags, policyCategories) { const transactionChanges = { tag, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestTag', params, onyxData); } @@ -1149,10 +1175,12 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { * @param {Number} [transactionChanges.amount] * @param {Object} [transactionChanges.comment] * @param {Object} [transactionChanges.waypoints] - * + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateDistanceRequest(transactionID, transactionThreadReportID, transactionChanges) { - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, false); +function updateDistanceRequest(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories) { + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, false); API.write('UpdateDistanceRequest', params, onyxData); } @@ -2170,8 +2198,11 @@ function setDraftSplitTransaction(transactionID, transactionChanges = {}) { * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {Object} transactionChanges + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function editRegularMoneyRequest(transactionID, transactionThreadReportID, transactionChanges) { +function editRegularMoneyRequest(transactionID, transactionThreadReportID, transactionChanges, policy = {}, policyTags, policyCategories) { // STEP 1: Get all collections we're updating const transactionThread = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]; const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -2225,6 +2256,13 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); + // TODO + const previousViolationsOnyxData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + }; const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2256,6 +2294,11 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: updatedViolationsOnyxData, + }, ...(!isScanning ? [ { @@ -2379,6 +2422,11 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: transactionThread.lastVisibleActionCreated, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: previousViolationsOnyxData, + }, ]; // STEP 6: Call the API endpoint @@ -2405,12 +2453,15 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans * @param {object} transaction * @param {String} transactionThreadReportID * @param {Object} transactionChanges + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function editMoneyRequest(transaction, transactionThreadReportID, transactionChanges) { +function editMoneyRequest(transaction, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories) { if (TransactionUtils.isDistanceRequest(transaction)) { - updateDistanceRequest(transaction.transactionID, transactionThreadReportID, transactionChanges); + updateDistanceRequest(transaction.transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); } else { - editRegularMoneyRequest(transaction.transactionID, transactionThreadReportID, transactionChanges); + editRegularMoneyRequest(transaction.transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); } } @@ -2421,13 +2472,16 @@ function editMoneyRequest(transaction, transactionThreadReportID, transactionCha * @param {String} transactionThreadReportID * @param {String} currency * @param {Number} amount + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadReportID, currency, amount) { +function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadReportID, currency, amount, policy, policyTags, policyCategories) { const transactionChanges = { amount, currency, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestAmountAndCurrency', params, onyxData); } diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index b322f4eb106c..57fe1e7957a7 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -29,6 +29,7 @@ import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import reportPropTypes from './reportPropTypes'; +import {policyPropTypes} from './workspace/withPolicy'; const propTypes = { /** Route from navigation */ @@ -47,6 +48,9 @@ const propTypes = { /** The report object for the thread report */ report: reportPropTypes, + /** The policy of the report */ + policy: policyPropTypes.policy, + /** Collection of categories attached to a policy */ policyCategories: PropTypes.objectOf(categoryPropTypes), @@ -62,13 +66,14 @@ const propTypes = { const defaultProps = { report: {}, + policy: {}, policyCategories: {}, policyTags: {}, parentReportActions: {}, transaction: {}, }; -function EditRequestPage({report, route, policyCategories, policyTags, parentReportActions, transaction}) { +function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { @@ -112,7 +117,7 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { - IOU.editMoneyRequest(transaction, report.reportID, transactionChanges); + IOU.editMoneyRequest(transaction, report.reportID, transactionChanges, policy, policyTags, policyCategories); Navigation.dismissModal(report.reportID); } @@ -126,10 +131,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep return; } - IOU.updateMoneyRequestAmountAndCurrency(transaction.transactionID, report.reportID, newCurrency, newAmount); + IOU.updateMoneyRequestAmountAndCurrency(transaction.transactionID, report.reportID, newCurrency, newAmount, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transaction, report], + [transaction, report, policy, policyTags, policyCategories], ); const saveCreated = useCallback( @@ -139,10 +144,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep Navigation.dismissModal(); return; } - IOU.updateMoneyRequestDate(transaction.transactionID, report.reportID, newCreated); + IOU.updateMoneyRequestDate(transaction.transactionID, report.reportID, newCreated, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transaction, report], + [transaction, report, policy, policyTags, policyCategories], ); const saveMerchant = useCallback( @@ -158,14 +163,14 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // This is possible only in case of IOU requests. if (newTrimmedMerchant === '') { - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, policy, policyTags, policyCategories); return; } - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant); + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionMerchant, transaction, report], + [transactionMerchant, transaction, report, policy, policyTags, policyCategories], ); const saveTag = useCallback( @@ -175,10 +180,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // In case the same tag has been selected, reset the tag. updatedTag = ''; } - IOU.updateMoneyRequestTag(transaction.transactionID, report.reportID, updatedTag); + IOU.updateMoneyRequestTag(transaction.transactionID, report.reportID, updatedTag, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionTag, transaction.transactionID, report.reportID], + [transactionTag, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { @@ -300,6 +305,9 @@ export default compose( }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, policyCategories: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, }, From 56dd74d8898a29fc50a922d16f601ca6733d7662 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 9 Jan 2024 22:51:29 -0800 Subject: [PATCH 02/82] Lint fix --- src/pages/EditRequestPage.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 8e867c150b90..7e0577fa86b9 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -162,7 +162,14 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p } // An empty newTrimmedMerchant is only possible for the P2P IOU case - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newTrimmedMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, policy, policyTags, policyCategories); + IOU.updateMoneyRequestMerchant( + transaction.transactionID, + report.reportID, + newTrimmedMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + policy, + policyTags, + policyCategories, + ); Navigation.dismissModal(); }, [transactionMerchant, transaction, report, policy, policyTags, policyCategories], From 079b15d7c89055cbbe548553a5416ee699a1f6ea Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 27 Dec 2023 12:38:40 +0100 Subject: [PATCH 03/82] [TS migration] Migrate 'SettingsWalletPhysicalCard' page to TypeScript --- src/ONYXKEYS.ts | 6 +- src/libs/CardUtils.ts | 7 +- src/libs/GetPhysicalCardUtils.ts | 74 +++------ src/libs/Navigation/types.ts | 24 ++- src/libs/UserUtils.ts | 4 +- ...hysicalCard.js => BaseGetPhysicalCard.tsx} | 145 +++++++----------- ...dAddress.js => GetPhysicalCardAddress.tsx} | 57 ++----- ...dConfirm.js => GetPhysicalCardConfirm.tsx} | 74 +++------ ...calCardName.js => GetPhysicalCardName.tsx} | 66 ++++---- ...lCardPhone.js => GetPhysicalCardPhone.tsx} | 55 +++---- src/types/onyx/Card.ts | 4 +- src/types/onyx/Form.ts | 23 ++- src/types/onyx/PrivatePersonalDetails.ts | 1 + src/types/onyx/index.ts | 22 +-- 14 files changed, 234 insertions(+), 328 deletions(-) rename src/pages/settings/Wallet/Card/{BaseGetPhysicalCard.js => BaseGetPhysicalCard.tsx} (59%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardAddress.js => GetPhysicalCardAddress.tsx} (59%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardConfirm.js => GetPhysicalCardConfirm.tsx} (61%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardName.js => GetPhysicalCardName.tsx} (61%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardPhone.js => GetPhysicalCardPhone.tsx} (60%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 98e3856f4544..bb0fdc16188b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -405,7 +405,7 @@ type OnyxValues = { [ONYXKEYS.WALLET_TERMS]: OnyxTypes.WalletTerms; [ONYXKEYS.BANK_ACCOUNT_LIST]: OnyxTypes.BankAccountList; [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; - [ONYXKEYS.CARD_LIST]: Record; + [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; @@ -525,8 +525,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm | undefined; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index d71ad9c2629a..f66ddbab2c7f 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,5 +1,6 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx/lib/types'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -72,11 +73,11 @@ function getYearFromExpirationDateString(expirationDateString: string) { * @param cardList - collection of assigned cards * @returns collection of assigned cards grouped by domain */ -function getDomainCards(cardList: Record) { +function getDomainCards(cardList: OnyxCollection) { // Check for domainName to filter out personal credit cards. // eslint-disable-next-line you-dont-need-lodash-underscore/filter - const activeCards = lodash.filter(cardList, (card) => !!card.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName); + const activeCards = lodash.filter(cardList, (card) => !!card?.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); + return lodash.groupBy(activeCards, (card) => card?.domainName); } /** diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index eebefd7c1d52..4e6775fa10b3 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,30 +1,12 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; -import type {Login} from '@src/types/onyx'; +import type {Route} from '@src/ROUTES'; +import type {GetPhysicalCardForm, LoginList, PrivatePersonalDetails} from '@src/types/onyx'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as UserUtils from './UserUtils'; -type DraftValues = { - addressLine1: string; - addressLine2: string; - city: string; - country: string; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; - state: string; - zipPostCode: string; -}; - -type PrivatePersonalDetails = { - address: {street: string; city: string; state: string; country: string; zip: string}; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; -}; - -type LoginList = Record; - /** * * @param domain @@ -32,13 +14,8 @@ type LoginList = Record; * @param loginList * @returns */ -function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { - const { - address: {street, city, state, country, zip}, - legalFirstName, - legalLastName, - phoneNumber, - } = privatePersonalDetails; +function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route { + const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); @@ -46,7 +23,7 @@ function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonal if (!phoneNumber && !UserUtils.getSecondaryPhoneLogin(loginList)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } - if (!(street && city && state && country && zip)) { + if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain); } @@ -60,7 +37,7 @@ function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonal * @param loginList * @returns */ -function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { +function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList)); } @@ -72,7 +49,7 @@ function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: Priva * @param loginList * @returns */ -function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { +function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { const expectedRoute = getCurrentRoute(domain, privatePersonalDetails, loginList); // If the user is on the current route or the current route is confirmation, then he's allowed to stay on the current step @@ -90,24 +67,19 @@ function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDe * @param privatePersonalDetails * @returns */ -function getUpdatedDraftValues(draftValues: DraftValues, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { - const { - address: {city, country, state, street = '', zip}, - legalFirstName, - legalLastName, - phoneNumber, - } = privatePersonalDetails; +function getUpdatedDraftValues(draftValues: OnyxEntry, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): GetPhysicalCardForm { + const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; return { - legalFirstName: draftValues.legalFirstName || legalFirstName, - legalLastName: draftValues.legalLastName || legalLastName, - addressLine1: draftValues.addressLine1 || street.split('\n')[0], - addressLine2: draftValues.addressLine2 || street.split('\n')[1] || '', - city: draftValues.city || city, - country: draftValues.country || country, - phoneNumber: draftValues.phoneNumber || (phoneNumber ?? UserUtils.getSecondaryPhoneLogin(loginList) ?? ''), - state: draftValues.state || state, - zipPostCode: draftValues.zipPostCode || zip, + legalFirstName: draftValues?.legalFirstName || legalFirstName, + legalLastName: draftValues?.legalLastName || legalLastName, + addressLine1: draftValues?.addressLine1 || address?.street.split('\n')[0], + addressLine2: draftValues?.addressLine2 || address?.street.split('\n')[1] || '', + city: draftValues?.city || address?.city, + country: draftValues?.country || address?.country, + phoneNumber: draftValues?.phoneNumber || phoneNumber || UserUtils.getSecondaryPhoneLogin(loginList) || '', + state: draftValues?.state || address?.state, + zipPostCode: draftValues?.zipPostCode || address?.zip || '', }; } @@ -116,13 +88,13 @@ function getUpdatedDraftValues(draftValues: DraftValues, privatePersonalDetails: * @param draftValues * @returns */ -function getUpdatedPrivatePersonalDetails(draftValues: DraftValues) { - const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues; +function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { + const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues ?? {}; return { legalFirstName, legalLastName, phoneNumber, - address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city, country, state, zip: zipPostCode}, + address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city: city || '', country: country || '', state: state || '', zip: zipPostCode || ''}, }; } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 90f5361f11f4..a384fed9fff9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -72,10 +72,26 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: undefined; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: undefined; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: undefined; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: { + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: { + /** Currently selected country */ + country: string; + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: { + /** Currently selected country */ + country: string; + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 653acfa36216..fc0c0c655324 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -218,8 +218,8 @@ function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): Ava /** * Gets the secondary phone login number */ -function getSecondaryPhoneLogin(loginList: Record): string | undefined { - const parsedLoginList = Object.keys(loginList).map((login) => Str.removeSMSDomain(login)); +function getSecondaryPhoneLogin(loginList: OnyxEntry): string | undefined { + const parsedLoginList = Object.keys(loginList ?? {}).map((login) => Str.removeSMSDomain(login)); return parsedLoginList.find((login) => Str.isValidPhone(login)); } diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx similarity index 59% rename from src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js rename to src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index cd1f4591a61a..ec5296434505 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,123 +9,83 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as FormActions from '@libs/actions/FormActions'; import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; -import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import assignedCardPropTypes from '@pages/settings/Wallet/assignedCardPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {CardList, GetPhysicalCardForm, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; -const propTypes = { - /* Onyx Props */ +type OnValidate = (values: OnyxEntry) => void; + +type RenderContentProps = ChildrenProps & { + onSubmit: () => void; + submitButtonText: string; + onValidate: OnValidate; +}; + +type BaseGetPhysicalCardOnyxProps = { /** List of available assigned cards */ - cardList: PropTypes.objectOf(assignedCardPropTypes), + cardList: OnyxEntry; /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - phoneNumber: PropTypes.string, - /** User's home address */ - address: PropTypes.shape({ - street: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - zip: PropTypes.string, - country: PropTypes.string, - }), - }), + privatePersonalDetails: OnyxEntry; /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - country: PropTypes.string, - phoneNumber: PropTypes.string, - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - state: PropTypes.string, - zipPostCode: PropTypes.string, - }), + draftValues: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), + session: OnyxEntry; /** List of available login methods */ - loginList: PropTypes.shape({ - /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */ - partnerName: PropTypes.string, - - /** Phone/Email associated with user */ - partnerUserID: PropTypes.string, - - /** The date when the login was validated, used to show the brickroad status */ - validatedDate: PropTypes.string, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - - /** Field-specific pending states for offline UI status */ - pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), + loginList: OnyxEntry; +}; - /* Base Props */ +type BaseGetPhysicalCardProps = ChildrenProps & BaseGetPhysicalCardOnyxProps & { /** Text displayed below page title */ - headline: PropTypes.string.isRequired, - - /** Children components that will be rendered by renderContent */ - children: PropTypes.node, + headline: string; /** Current route from ROUTES */ - currentRoute: PropTypes.string.isRequired, + currentRoute: string; /** Expensify card domain */ - domain: PropTypes.string, + domain: string; /** Whether or not the current step of the get physical card flow is the confirmation page */ - isConfirmation: PropTypes.bool, + isConfirmation?: boolean; /** Render prop, used to render form content */ - renderContent: PropTypes.func, + renderContent?: (args: RenderContentProps) => React.ReactNode; /** Text displayed on bottom submit button */ - submitButtonText: PropTypes.string.isRequired, + submitButtonText: string; /** Title displayed on top of the page */ - title: PropTypes.string.isRequired, + title: string; /** Callback executed when validating get physical card form data */ - onValidate: PropTypes.func, + onValidate?: OnValidate; }; -const defaultProps = { - cardList: {}, - children: null, - domain: '', - draftValues: null, - privatePersonalDetails: null, - session: {}, - loginList: {}, - isConfirmation: false, - renderContent: (onSubmit, submitButtonText, styles, children = () => {}, onValidate = () => ({})) => ( + +function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate}: RenderContentProps) { + const styles = useThemeStyles(); + + return ( + // @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript. {children} - ), - onValidate: () => ({}), -}; + ) +} function BaseGetPhysicalCard({ cardList, @@ -136,14 +95,14 @@ function BaseGetPhysicalCard({ draftValues, privatePersonalDetails, headline, - isConfirmation, + isConfirmation = false, loginList, - renderContent, - session: {authToken}, + renderContent = DefaultRenderContent, + session, submitButtonText, title, - onValidate, -}) { + onValidate = () => ({}), +}: BaseGetPhysicalCardProps) { const styles = useThemeStyles(); const isRouteSet = useRef(false); @@ -153,7 +112,7 @@ function BaseGetPhysicalCard({ } const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; - const physicalCard = _.find(domainCards, (card) => !card.isVirtual); + const physicalCard = domainCards.find((card) => !card?.isVirtual); // When there are no cards for the specified domain, user is redirected to the wallet page if (domainCards.length === 0) { @@ -169,7 +128,7 @@ function BaseGetPhysicalCard({ } if (!draftValues) { - const updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues({}, privatePersonalDetails, loginList); + const updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues(null, privatePersonalDetails, loginList); // Form draft data needs to be initialized with the private personal details // If no draft data exists FormActions.setDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM, updatedDraftValues); @@ -187,9 +146,9 @@ function BaseGetPhysicalCard({ // If the current step of the get physical card flow is the confirmation page if (isConfirmation) { const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; - const cardID = virtualCard.cardID; - Wallet.requestPhysicalExpensifyCard(cardID, authToken, updatedPrivatePersonalDetails); + const virtualCard = domainCards.find((card) => card?.isVirtual); + const cardID = virtualCard?.cardID ?? ''; + Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken, updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); @@ -197,7 +156,7 @@ function BaseGetPhysicalCard({ return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails, loginList); - }, [authToken, cardList, domain, draftValues, isConfirmation, loginList]); + }, [cardList, domain, draftValues, isConfirmation, loginList, session?.authToken]); return ( Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} /> {headline} - {renderContent(onSubmit, submitButtonText, styles, children, onValidate)} + {renderContent({onSubmit, submitButtonText, children, onValidate})} ); } -BaseGetPhysicalCard.defaultProps = defaultProps; BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard'; -BaseGetPhysicalCard.propTypes = propTypes; -export default withOnyx({ +export default withOnyx({ cardList: { key: ONYXKEYS.CARD_LIST, }, @@ -232,6 +189,8 @@ export default withOnyx({ key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(BaseGetPhysicalCard); + +export type {RenderContentProps}; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx similarity index 59% rename from src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 21ba85b6c5dd..19e7cbc23d05 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -1,58 +1,35 @@ -import PropTypes from 'prop-types'; +import {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import AddressForm from '@components/AddressForm'; import useLocalize from '@hooks/useLocalize'; import * as FormActions from '@libs/actions/FormActions'; -import FormUtils from '@libs/FormUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; +import type {RenderContentProps} from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ +type GetPhysicalCardAddressOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - // User home address - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - country: PropTypes.string, - state: PropTypes.string, - zipPostCode: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** Currently selected country */ - country: PropTypes.string, - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, + draftValues: OnyxEntry; }; -const defaultProps = { - draftValues: { - addressLine1: '', - addressLine2: '', - city: '', - country: '', - state: '', - zipPostCode: '', - }, -}; +type GetPhysicalCardAddressProps = GetPhysicalCardAddressOnyxProps & StackScreenProps; function GetPhysicalCardAddress({ - draftValues: {addressLine1, addressLine2, city, state, zipPostCode, country}, + draftValues, route: { params: {country: countryFromUrl, domain}, }, -}) { +}: GetPhysicalCardAddressProps) { const {translate} = useLocalize(); + const {addressLine1, addressLine2, city, state, zipPostCode, country} = draftValues ?? {}; + useEffect(() => { if (!countryFromUrl) { return; @@ -61,7 +38,7 @@ function GetPhysicalCardAddress({ }, [countryFromUrl]); const renderContent = useCallback( - (onSubmit, submitButtonText) => ( + ({onSubmit, submitButtonText}: RenderContentProps) => ( ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardAddress); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx similarity index 61% rename from src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 9f364c32c075..67a77ce6630b 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -1,79 +1,53 @@ -import PropTypes from 'prop-types'; +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const goToGetPhysicalCardName = (domain) => { +const goToGetPhysicalCardName = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const goToGetPhysicalCardPhone = (domain) => { +const goToGetPhysicalCardPhone = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const goToGetPhysicalCardAddress = (domain) => { +const goToGetPhysicalCardAddress = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const propTypes = { - /* Onyx Props */ +type GetPhysicalCardConfirmOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - country: PropTypes.string, - zipPostCode: PropTypes.string, - phoneNumber: PropTypes.string, - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), - - /* Navigation Props */ - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, + draftValues: OnyxEntry; }; -const defaultProps = { - draftValues: { - addressLine1: '', - addressLine2: '', - city: '', - state: '', - country: '', - zipPostCode: '', - phoneNumber: '', - legalFirstName: '', - legalLastName: '', - }, -}; +type GetPhysicalCardConfirmProps = GetPhysicalCardConfirmOnyxProps & StackScreenProps; function GetPhysicalCardConfirm({ - draftValues: {addressLine1, addressLine2, city, state, country, zipPostCode, legalFirstName, legalLastName, phoneNumber}, + draftValues, route: { params: {domain}, }, -}) { +}: GetPhysicalCardConfirmProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {addressLine1, addressLine2, city, state, zipPostCode, country, phoneNumber, legalFirstName, legalLastName} = draftValues ?? {}; + return ( - {translate('getPhysicalCard.estimatedDeliveryMessage')} + {translate('getPhysicalCard.estimatedDeliveryMessage')} @@ -117,12 +91,10 @@ function GetPhysicalCardConfirm({ ); } -GetPhysicalCardConfirm.defaultProps = defaultProps; GetPhysicalCardConfirm.displayName = 'GetPhysicalCardConfirm'; -GetPhysicalCardConfirm.propTypes = propTypes; -export default withOnyx({ +export default withOnyx({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardConfirm); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx similarity index 61% rename from src/pages/settings/Wallet/Card/GetPhysicalCardName.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 5b954d432cce..2264845e710d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -1,63 +1,55 @@ -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ - /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, +type OnValidateResult = { + legalFirstName?: string; + legalLastName?: string; }; -const defaultProps = { - draftValues: { - legalFirstName: '', - legalLastName: '', - }, +type GetPhysicalCardNameOnyxProps = { + /** Draft values used by the get physical card form */ + draftValues: OnyxEntry; }; +type GetPhysicalCardNameProps = GetPhysicalCardNameOnyxProps & StackScreenProps; + function GetPhysicalCardName({ - draftValues: {legalFirstName, legalLastName}, + draftValues, route: { params: {domain}, }, -}) { +}: GetPhysicalCardNameProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const onValidate = (values) => { - const errors = {}; - if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { + const {legalFirstName, legalLastName} = draftValues ?? {}; + + const onValidate = (values: OnyxEntry): OnValidateResult => { + const errors: OnValidateResult = {}; + + if (values?.legalFirstName && !ValidationUtils.isValidLegalName(values.legalFirstName)) { errors.legalFirstName = 'privatePersonalDetails.error.hasInvalidCharacter'; - } else if (_.isEmpty(values.legalFirstName)) { + } else if (!values?.legalFirstName) { errors.legalFirstName = 'common.error.fieldRequired'; } - if (!ValidationUtils.isValidLegalName(values.legalLastName)) { + if (values?.legalLastName && !ValidationUtils.isValidLegalName(values.legalLastName)) { errors.legalLastName = 'privatePersonalDetails.error.hasInvalidCharacter'; - } else if (_.isEmpty(values.legalLastName)) { + } else if (!values?.legalLastName) { errors.legalLastName = 'common.error.fieldRequired'; } @@ -74,6 +66,7 @@ function GetPhysicalCardName({ onValidate={onValidate} > - ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardName); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx similarity index 60% rename from src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index 5e4feac83d96..ea53fb7ba81e 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -1,57 +1,49 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ - /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - phoneNumber: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, +type OnValidateResult = { + phoneNumber?: string; }; -const defaultProps = { - draftValues: { - phoneNumber: '', - }, +type GetPhysicalCardPhoneOnyxProps = { + /** Draft values used by the get physical card form */ + draftValues: OnyxEntry; }; +type GetPhysicalCardPhoneProps = GetPhysicalCardPhoneOnyxProps & StackScreenProps; + function GetPhysicalCardPhone({ - draftValues: {phoneNumber}, route: { params: {domain}, }, -}) { + draftValues, +}: GetPhysicalCardPhoneProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const onValidate = (values) => { - const errors = {}; + const {phoneNumber} = draftValues ?? {}; + + const onValidate = (values: OnyxEntry): OnValidateResult => { + const errors: OnValidateResult = {}; - if (!(parsePhoneNumber(values.phoneNumber).possible && Str.isValidPhone(values.phoneNumber))) { + if (!(parsePhoneNumber(values?.phoneNumber ?? '').possible && Str.isValidPhone(values?.phoneNumber ?? ''))) { errors.phoneNumber = 'common.error.phoneNumber'; - } else if (_.isEmpty(values.phoneNumber)) { + } else if (!values?.phoneNumber) { errors.phoneNumber = 'common.error.fieldRequired'; } @@ -68,6 +60,7 @@ function GetPhysicalCardPhone({ onValidate={onValidate} > ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardPhone); diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index e3b025ff5a2f..68acd88aa120 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -33,5 +33,7 @@ type TCardDetails = { }; }; +type CardList = Record; + export default Card; -export type {TCardDetails}; +export type {TCardDetails, CardList}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index ca8d6574adf5..fdde34898f84 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -21,6 +21,27 @@ type DateOfBirthForm = Form & { dob?: string; }; +type GetPhysicalCardForm = Form & { + /** Address line 1 for delivery */ + addressLine1?: string; + /** Address line 2 for delivery */ + addressLine2?: string; + /** City for delivery */ + city?: string; + /** Country for delivery */ + country?: string; + /** First name for delivery */ + legalFirstName?: string; + /** Last name for delivery */ + legalLastName?: string; + /** Phone number for delivery */ + phoneNumber?: string; + /** State for delivery */ + state?: string; + /** Zip code for delivery */ + zipPostCode?: string; +}; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm}; +export type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm}; diff --git a/src/types/onyx/PrivatePersonalDetails.ts b/src/types/onyx/PrivatePersonalDetails.ts index 6ef5b75c4a0f..4d0dedf16ea7 100644 --- a/src/types/onyx/PrivatePersonalDetails.ts +++ b/src/types/onyx/PrivatePersonalDetails.ts @@ -10,6 +10,7 @@ type PrivatePersonalDetails = { legalFirstName?: string; legalLastName?: string; dob?: string; + phoneNumber?: string; /** User's home address */ address?: Address; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..3fb7f47f1137 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,37 +1,38 @@ import type Account from './Account'; import type AccountData from './AccountData'; -import type {BankAccountList} from './BankAccount'; +import type { BankAccountList } from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; +import type { CardList } from './Card'; import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; -import type {FundList} from './Fund'; +import type { FundList } from './Fund'; import type Fund from './Fund'; import type IOU from './IOU'; import type Locale from './Locale'; -import type {LoginList} from './Login'; +import type { LoginList } from './Login'; import type Login from './Login'; import type MapboxAccessToken from './MapboxAccessToken'; import type Modal from './Modal'; import type Network from './Network'; -import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; +import type { OnyxUpdateEvent, OnyxUpdatesFromServer } from './OnyxUpdatesFromServer'; import type PersonalBankAccount from './PersonalBankAccount'; -import type {PersonalDetailsList} from './PersonalDetails'; +import type { PersonalDetailsList } from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type PolicyReportField from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type PolicyReportField from './PolicyReportField'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -40,7 +41,7 @@ import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type Report from './Report'; -import type {ReportActions} from './ReportAction'; +import type { ReportActions } from './ReportAction'; import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; @@ -55,7 +56,7 @@ import type SecurityGroup from './SecurityGroup'; import type Session from './Session'; import type Task from './Task'; import type Transaction from './Transaction'; -import type {TransactionViolation, ViolationName} from './TransactionViolation'; +import type { TransactionViolation, ViolationName } from './TransactionViolation'; import type User from './User'; import type UserLocation from './UserLocation'; import type UserWallet from './UserWallet'; @@ -65,6 +66,7 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; + export type { Account, AccountData, @@ -74,6 +76,7 @@ export type { Beta, BlockedFromConcierge, Card, + CardList, Credentials, Currency, CustomStatusDraft, @@ -83,6 +86,7 @@ export type { FrequentlyUsedEmoji, Fund, FundList, + GetPhysicalCardForm, IOU, Locale, Login, From c97ce23271d67c9562f14153e9e32771d5060711 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 2 Jan 2024 16:53:29 +0100 Subject: [PATCH 04/82] fix comments --- src/libs/CardUtils.ts | 4 ++-- src/libs/GetPhysicalCardUtils.ts | 22 +++++-------------- .../Wallet/Card/BaseGetPhysicalCard.tsx | 6 ++++- .../Wallet/Card/GetPhysicalCardAddress.tsx | 8 +++---- .../Wallet/Card/GetPhysicalCardConfirm.tsx | 17 +++++++------- .../Wallet/Card/GetPhysicalCardName.tsx | 6 ++--- .../Wallet/Card/GetPhysicalCardPhone.tsx | 10 +++++---- src/types/onyx/Form.ts | 8 +++++++ src/types/onyx/index.ts | 17 +++++++------- 9 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f66ddbab2c7f..11b68244fbe1 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -75,8 +75,8 @@ function getYearFromExpirationDateString(expirationDateString: string) { */ function getDomainCards(cardList: OnyxCollection) { // Check for domainName to filter out personal credit cards. - // eslint-disable-next-line you-dont-need-lodash-underscore/filter - const activeCards = lodash.filter(cardList, (card) => !!card?.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); + const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); + return lodash.groupBy(activeCards, (card) => card?.domainName); } diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 4e6775fa10b3..6b0e940a053d 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -7,13 +6,6 @@ import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as UserUtils from './UserUtils'; -/** - * - * @param domain - * @param privatePersonalDetails - * @param loginList - * @returns - */ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route { const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; @@ -30,13 +22,6 @@ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList)); } @@ -71,6 +56,8 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; return { + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + // we do not need to use nullish coalescing here because we want to allow empty strings legalFirstName: draftValues?.legalFirstName || legalFirstName, legalLastName: draftValues?.legalLastName || legalLastName, addressLine1: draftValues?.addressLine1 || address?.street.split('\n')[0], @@ -80,6 +67,7 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv phoneNumber: draftValues?.phoneNumber || phoneNumber || UserUtils.getSecondaryPhoneLogin(loginList) || '', state: draftValues?.state || address?.state, zipPostCode: draftValues?.zipPostCode || address?.zip || '', + /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ }; } @@ -89,12 +77,12 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv * @returns */ function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { - const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues ?? {}; + const {addressLine1, addressLine2, city = '', country = '', legalFirstName, legalLastName, phoneNumber, state = '', zipPostCode = ''} = draftValues ?? {}; return { legalFirstName, legalLastName, phoneNumber, - address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city: city || '', country: country || '', state: state || '', zip: zipPostCode || ''}, + address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city, country, state, zip: zipPostCode}, }; } diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ec5296434505..36bf749125b5 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useEffect, useRef} from 'react'; +import type {ReactNode} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; @@ -42,10 +43,13 @@ type BaseGetPhysicalCardOnyxProps = { loginList: OnyxEntry; }; -type BaseGetPhysicalCardProps = ChildrenProps & BaseGetPhysicalCardOnyxProps & { +type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { /** Text displayed below page title */ headline: string; + /** Children components that will be rendered by renderContent */ + children?: ReactNode; + /** Current route from ROUTES */ currentRoute: string; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 19e7cbc23d05..578a36afc31f 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -1,14 +1,14 @@ -import {StackScreenProps} from '@react-navigation/stack'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import AddressForm from '@components/AddressForm'; import useLocalize from '@hooks/useLocalize'; import * as FormActions from '@libs/actions/FormActions'; import type {SettingsNavigatorParamList} from '@navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; import type {RenderContentProps} from './BaseGetPhysicalCard'; @@ -28,7 +28,7 @@ function GetPhysicalCardAddress({ }: GetPhysicalCardAddressProps) { const {translate} = useLocalize(); - const {addressLine1, addressLine2, city, state, zipPostCode, country} = draftValues ?? {}; + const {addressLine1 = '', addressLine2 = '', city = '', state = '', zipPostCode = '', country = ''} = draftValues ?? {}; useEffect(() => { if (!countryFromUrl) { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 67a77ce6630b..967d919b239c 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ -import {StackScreenProps} from '@react-navigation/stack'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Text from '@components/Text'; @@ -14,7 +13,7 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; @@ -46,7 +45,7 @@ function GetPhysicalCardConfirm({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {addressLine1, addressLine2, city, state, zipPostCode, country, phoneNumber, legalFirstName, legalLastName} = draftValues ?? {}; + const {addressLine1, addressLine2, city = '', state = '', zipPostCode = '', country = '', phoneNumber = '', legalFirstName = '', legalLastName = ''} = draftValues ?? {}; return ( diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 2264845e710d..67eaa0193f0d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -36,7 +36,7 @@ function GetPhysicalCardName({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {legalFirstName, legalLastName} = draftValues ?? {}; + const {legalFirstName = '', legalLastName = ''} = draftValues ?? {}; const onValidate = (values: OnyxEntry): OnValidateResult => { const errors: OnValidateResult = {}; @@ -72,7 +72,7 @@ function GetPhysicalCardName({ name="legalFirstName" label={translate('getPhysicalCard.legalFirstName')} aria-label={translate('getPhysicalCard.legalFirstName')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} autoCapitalize="words" defaultValue={legalFirstName} containerStyles={[styles.mh5]} @@ -85,7 +85,7 @@ function GetPhysicalCardName({ name="legalLastName" label={translate('getPhysicalCard.legalLastName')} aria-label={translate('getPhysicalCard.legalLastName')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} autoCapitalize="words" defaultValue={legalLastName} containerStyles={[styles.mt5, styles.mh5]} diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index ea53fb7ba81e..9c3426cac991 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -36,14 +36,16 @@ function GetPhysicalCardPhone({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {phoneNumber} = draftValues ?? {}; + const {phoneNumber = ''} = draftValues ?? {}; const onValidate = (values: OnyxEntry): OnValidateResult => { + const {phoneNumber: phoneNumberToValidate = ''} = values ?? {}; + const errors: OnValidateResult = {}; - if (!(parsePhoneNumber(values?.phoneNumber ?? '').possible && Str.isValidPhone(values?.phoneNumber ?? ''))) { + if (!(parsePhoneNumber(phoneNumberToValidate).possible && Str.isValidPhone(phoneNumberToValidate))) { errors.phoneNumber = 'common.error.phoneNumber'; - } else if (!values?.phoneNumber) { + } else if (!phoneNumberToValidate) { errors.phoneNumber = 'common.error.fieldRequired'; } @@ -66,7 +68,7 @@ function GetPhysicalCardPhone({ name="phoneNumber" label={translate('getPhysicalCard.phoneNumber')} aria-label={translate('getPhysicalCard.phoneNumber')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} defaultValue={phoneNumber} containerStyles={[styles.mh5]} shouldSaveDraft diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index fdde34898f84..f299f5e161fb 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -24,20 +24,28 @@ type DateOfBirthForm = Form & { type GetPhysicalCardForm = Form & { /** Address line 1 for delivery */ addressLine1?: string; + /** Address line 2 for delivery */ addressLine2?: string; + /** City for delivery */ city?: string; + /** Country for delivery */ country?: string; + /** First name for delivery */ legalFirstName?: string; + /** Last name for delivery */ legalLastName?: string; + /** Phone number for delivery */ phoneNumber?: string; + /** State for delivery */ state?: string; + /** Zip code for delivery */ zipPostCode?: string; }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 3fb7f47f1137..fda23d340e16 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,11 +1,11 @@ import type Account from './Account'; import type AccountData from './AccountData'; -import type { BankAccountList } from './BankAccount'; +import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; -import type { CardList } from './Card'; +import type {CardList} from './Card'; import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; @@ -13,18 +13,18 @@ import type Download from './Download'; import type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; -import type { FundList } from './Fund'; +import type {FundList} from './Fund'; import type Fund from './Fund'; import type IOU from './IOU'; import type Locale from './Locale'; -import type { LoginList } from './Login'; +import type {LoginList} from './Login'; import type Login from './Login'; import type MapboxAccessToken from './MapboxAccessToken'; import type Modal from './Modal'; import type Network from './Network'; -import type { OnyxUpdateEvent, OnyxUpdatesFromServer } from './OnyxUpdatesFromServer'; +import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import type PersonalBankAccount from './PersonalBankAccount'; -import type { PersonalDetailsList } from './PersonalDetails'; +import type {PersonalDetailsList} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; @@ -41,7 +41,7 @@ import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type Report from './Report'; -import type { ReportActions } from './ReportAction'; +import type {ReportActions} from './ReportAction'; import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; @@ -56,7 +56,7 @@ import type SecurityGroup from './SecurityGroup'; import type Session from './Session'; import type Task from './Task'; import type Transaction from './Transaction'; -import type { TransactionViolation, ViolationName } from './TransactionViolation'; +import type {TransactionViolation, ViolationName} from './TransactionViolation'; import type User from './User'; import type UserLocation from './UserLocation'; import type UserWallet from './UserWallet'; @@ -66,7 +66,6 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; - export type { Account, AccountData, From fe4b1788b7c5adc4ed32246b32fbc3693e89e997 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 10 Jan 2024 10:17:43 -0800 Subject: [PATCH 05/82] Fix bugs --- src/libs/actions/IOU.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 27326f896f14..b1f0b00d7f7e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1054,11 +1054,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } // Add optimistic transaction violations - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), - }); + optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -2272,7 +2268,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); - // TODO const previousViolationsOnyxData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, @@ -2309,11 +2304,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: updatedViolationsOnyxData, - }, + updatedViolationsOnyxData, ...(!isScanning ? [ { From a1899701ed39638bfedd73ea840f8d193b15e4b5 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 10 Jan 2024 10:50:46 -0800 Subject: [PATCH 06/82] Fix data passing --- src/libs/actions/IOU.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b1f0b00d7f7e..474028af01f0 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1054,7 +1054,8 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } // Add optimistic transaction violations - optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -1098,7 +1099,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + value: currentTransactionViolations, }); return { @@ -2267,12 +2268,15 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); - const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); - const previousViolationsOnyxData = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], - }; + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + currentTransactionViolations, + policy.requiresTag, + policyTags, + policy.requiresCategory, + policyCategories, + ); const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2431,7 +2435,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: previousViolationsOnyxData, + value: currentTransactionViolations, }, ]; From 69ad6950c34bf5d2b6f6da84e5b4dc097a178e2e Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 11:47:23 -0800 Subject: [PATCH 07/82] Temp fix for tag issue --- src/libs/ViolationsUtils.ts | 10 ++++++++-- src/types/onyx/PolicyTag.ts | 12 +++++++++++- src/types/onyx/index.ts | 3 ++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 2637686e726b..f7b5482c560a 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,8 +2,9 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; +import * as PolicyUtils from './PolicyUtils'; const ViolationsUtils = { /** @@ -14,7 +15,7 @@ const ViolationsUtils = { transaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTags: PolicyTags, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -50,7 +51,12 @@ const ViolationsUtils = { } } + if (policyRequiresTags) { + // TODO, this fixes it but TS rightly complains about + // @ts-ignore + const tagListName: string = PolicyUtils.getTagListName(policyTagList); + const policyTags = policyTagList[tagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 58a21dcf4df5..149da7eb3341 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,4 +12,14 @@ type PolicyTag = { type PolicyTags = Record; -export type {PolicyTag, PolicyTags}; +type PolicyTagList = Record; + +export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..d307c61b0baa 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type {PolicyTag, PolicyTags, PolicyTagList} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -103,6 +103,7 @@ export type { PolicyMembers, PolicyTag, PolicyTags, + PolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 41e87caeb581dd95d5b8ef8b8be8886281089626 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 16:18:24 -0800 Subject: [PATCH 08/82] Use full policyTags data as ViolationsUtils input --- src/libs/ViolationsUtils.ts | 11 ++++------- src/types/onyx/PolicyTag.ts | 8 ++++++-- tests/unit/ViolationUtilsTest.js | 8 +++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index f7b5482c560a..6db4eb58cfa9 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,9 +2,8 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTagList, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; -import * as PolicyUtils from './PolicyUtils'; const ViolationsUtils = { /** @@ -15,7 +14,7 @@ const ViolationsUtils = { transaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: PolicyTagList, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -53,10 +52,8 @@ const ViolationsUtils = { if (policyRequiresTags) { - // TODO, this fixes it but TS rightly complains about - // @ts-ignore - const tagListName: string = PolicyUtils.getTagListName(policyTagList); - const policyTags = policyTagList[tagListName].tags; + const policyTagListName = Object.keys(policyTagList)[0]; + const policyTags = policyTagList[policyTagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 149da7eb3341..a2872971a7f5 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,7 +12,9 @@ type PolicyTag = { type PolicyTags = Record; -type PolicyTagList = Record = Record = Record; + }>; + + type PolicyTagList = PolicyTagListGeneric; export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index cc84c547da2e..1f16d41f598e 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -128,7 +128,13 @@ describe('getViolationsOnyxData', () => { describe('policyRequiresTags', () => { beforeEach(() => { policyRequiresTags = true; - policyTags = {Lunch: {enabled: true}, Dinner: {enabled: true}}; + policyTags = { + Tag: { + name: 'Tag', + required: true, + tags: {Lunch: {enabled: true}, Dinner: {enabled: true}}, + }, + }; transaction.tag = 'Lunch'; }); From a09e4ac881643945daefd623724a2be3273aa017 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 16:54:15 -0800 Subject: [PATCH 09/82] getViolationsOnyxData requires new transaction data but previous transaction violations. Fix and make that more clear with argument names --- src/libs/ViolationsUtils.ts | 21 ++++++++++----------- src/libs/actions/IOU.js | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 6db4eb58cfa9..97891235b5d3 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -11,7 +11,7 @@ const ViolationsUtils = { * violations. */ getViolationsOnyxData( - transaction: Transaction, + updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, policyTagList: PolicyTagList, @@ -27,15 +27,15 @@ const ViolationsUtils = { if (policyRequiresCategories) { const hasCategoryOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy'); const hasMissingCategoryViolation = transactionViolations.some((violation) => violation.name === 'missingCategory'); - const isCategoryInPolicy = Boolean(policyCategories[transaction.category]?.enabled); + const isCategoryInPolicy = Boolean(policyCategories[updatedTransaction.category]?.enabled); // Add 'categoryOutOfPolicy' violation if category is not in policy - if (!hasCategoryOutOfPolicyViolation && transaction.category && !isCategoryInPolicy) { + if (!hasCategoryOutOfPolicyViolation && updatedTransaction.category && !isCategoryInPolicy) { newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation', userMessage: ''}); } // Remove 'categoryOutOfPolicy' violation if category is in policy - if (hasCategoryOutOfPolicyViolation && transaction.category && isCategoryInPolicy) { + if (hasCategoryOutOfPolicyViolation && updatedTransaction.category && isCategoryInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: 'categoryOutOfPolicy'}); } @@ -45,26 +45,25 @@ const ViolationsUtils = { } // Add 'missingCategory' violation if category is required and not set - if (!hasMissingCategoryViolation && policyRequiresCategories && !transaction.category) { + if (!hasMissingCategoryViolation && policyRequiresCategories && !updatedTransaction.category) { newTransactionViolations.push({name: 'missingCategory', type: 'violation', userMessage: ''}); } } - if (policyRequiresTags) { const policyTagListName = Object.keys(policyTagList)[0]; const policyTags = policyTagList[policyTagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); - const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); + const isTagInPolicy = Boolean(policyTags[updatedTransaction.tag]?.enabled); // Add 'tagOutOfPolicy' violation if tag is not in policy - if (!hasTagOutOfPolicyViolation && transaction.tag && !isTagInPolicy) { + if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { newTransactionViolations.push({name: 'tagOutOfPolicy', type: 'violation', userMessage: ''}); } // Remove 'tagOutOfPolicy' violation if tag is in policy - if (hasTagOutOfPolicyViolation && transaction.tag && isTagInPolicy) { + if (hasTagOutOfPolicyViolation && updatedTransaction.tag && isTagInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: 'tagOutOfPolicy'}); } @@ -74,14 +73,14 @@ const ViolationsUtils = { } // Add 'missingTag violation' if tag is required and not set - if (!hasMissingTagViolation && !transaction.tag && policyRequiresTags) { + if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) { newTransactionViolations.push({name: 'missingTag', type: 'violation', userMessage: ''}); } } return { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${updatedTransaction.transactionID}`, value: newTransactionViolations, }; }, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 474028af01f0..550b7176a103 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1055,7 +1055,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // Add optimistic transaction violations const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + optimisticData.push(ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -2270,7 +2270,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans const currentTime = DateUtils.getDBTime(); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( - transaction, + updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, From 72cfa451cb7dc4fe5a9943139cb587c462026cec Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 14:37:08 -0800 Subject: [PATCH 10/82] Improve types --- src/types/onyx/PolicyTag.ts | 25 ++++++++++++------------- src/types/onyx/index.ts | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index a2872971a7f5..1171ade17006 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,18 +12,17 @@ type PolicyTag = { type PolicyTags = Record; -// Using a generic to indicate that the top-level key and name should be the -// same value. Not meant for direct use, just used by the alias below. -type PolicyTagListGeneric = Record; - - type PolicyTagList = PolicyTagListGeneric; +type PolicyTagList = Record< + T, + { + /** Name of the tag list */ + name: T; + + /** Flag that determines if tags are required */ + required: boolean; + + tags: PolicyTags; + } +>; export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d307c61b0baa..e4a9123af56f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTags, PolicyTagList} from './PolicyTag'; +import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; From 8ff7c692224605c4d616b671799a7b0ed85da822 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 14:52:44 -0800 Subject: [PATCH 11/82] Fix case of passing empty object to policyTags --- src/libs/ViolationsUtils.ts | 8 ++++---- src/types/onyx/PolicyTag.ts | 6 +++++- src/types/onyx/index.ts | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 97891235b5d3..09b3b0632723 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {MaybePolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { @@ -14,7 +14,7 @@ const ViolationsUtils = { updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: PolicyTagList, + policyTagList: MaybePolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -52,10 +52,10 @@ const ViolationsUtils = { if (policyRequiresTags) { const policyTagListName = Object.keys(policyTagList)[0]; - const policyTags = policyTagList[policyTagListName].tags; + const policyTags = policyTagList[policyTagListName]?.tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); - const isTagInPolicy = Boolean(policyTags[updatedTransaction.tag]?.enabled); + const isTagInPolicy = Boolean(policyTags?.[updatedTransaction.tag]?.enabled); // Add 'tagOutOfPolicy' violation if tag is not in policy if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 1171ade17006..ca8545775a5c 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -25,4 +25,8 @@ type PolicyTagList = Record< } >; -export type {PolicyTag, PolicyTags, PolicyTagList}; +// When queried from Onyx, if there is no matching policy tag list, the data +// returned will be an empty object. +type MaybePolicyTagList = PolicyTagList | Record; + +export type {PolicyTag, PolicyTags, PolicyTagList, MaybePolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e4a9123af56f..20cf1ae69897 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type {MaybePolicyTagList, PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -104,6 +104,7 @@ export type { PolicyTag, PolicyTags, PolicyTagList, + MaybePolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From f2942ea411fd776f58639334416ebfa417c69ecd Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 15:29:16 -0800 Subject: [PATCH 12/82] We should only run getViolationsOnyxData if there's a policy --- src/libs/actions/IOU.js | 52 ++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 550b7176a103..ab3bf0882e54 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1053,9 +1053,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } } - // Add optimistic transaction violations + // Add optimistic transaction violations if there is a policy const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - optimisticData.push(ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + if (policy && policy.id) { + optimisticData.push( + ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + ); + } // Clear out the error fields and loading states on success successData.push({ @@ -1095,12 +1099,14 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: iouReport, }); - // Reset transaction violations to their original state - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: currentTransactionViolations, - }); + // If there is a policy, restore transaction violations to their original state + if (policy && policy.id) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: currentTransactionViolations, + }); + } return { params, @@ -2268,15 +2274,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( - updatedTransaction, - currentTransactionViolations, - policy.requiresTag, - policyTags, - policy.requiresCategory, - policyCategories, - ); const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2308,7 +2305,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, - updatedViolationsOnyxData, ...(!isScanning ? [ { @@ -2432,12 +2428,26 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: transactionThread.lastVisibleActionCreated, }, }, - { + ]; + + // Add transaction violations if there is a policy + if (policy && policy.id) { + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( + updatedTransaction, + currentTransactionViolations, + policy.requiresTag, + policyTags, + policy.requiresCategory, + policyCategories, + ); + optimisticData.push(updatedViolationsOnyxData); + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, value: currentTransactionViolations, - }, - ]; + }); + } // STEP 6: Call the API endpoint const {created, amount, currency, comment, merchant, category, billable, tag} = ReportUtils.getTransactionDetails(updatedTransaction); From 37bbacc60158ac87db4c357bbffc868244197264 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Mon, 15 Jan 2024 13:14:36 -0800 Subject: [PATCH 13/82] Feedback: Don't use Maybe type --- src/libs/ViolationsUtils.ts | 4 ++-- src/types/onyx/PolicyTag.ts | 10 ++++------ src/types/onyx/index.ts | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 09b3b0632723..dd128a68c703 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {MaybePolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { @@ -14,7 +14,7 @@ const ViolationsUtils = { updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: MaybePolicyTagList, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index ca8545775a5c..7c3636551746 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,6 +12,8 @@ type PolicyTag = { type PolicyTags = Record; +// When queried from Onyx, if there is no matching policy tag list, the data +// returned will be an empty object, represented by Record. type PolicyTagList = Record< T, { @@ -23,10 +25,6 @@ type PolicyTagList = Record< tags: PolicyTags; } ->; - -// When queried from Onyx, if there is no matching policy tag list, the data -// returned will be an empty object. -type MaybePolicyTagList = PolicyTagList | Record; +> | Record; -export type {PolicyTag, PolicyTags, PolicyTagList, MaybePolicyTagList}; +export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 20cf1ae69897..e4a9123af56f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {MaybePolicyTagList, PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -104,7 +104,6 @@ export type { PolicyTag, PolicyTags, PolicyTagList, - MaybePolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 0a3e81856b1a2550ca714d22b13fbb5a7779a508 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Mon, 15 Jan 2024 13:21:48 -0800 Subject: [PATCH 14/82] Ensure an array is always passed to getViolationsOnyxData --- src/libs/actions/IOU.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ab3bf0882e54..ff16336b97dd 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1057,7 +1057,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; if (policy && policy.id) { optimisticData.push( - ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations || [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), ); } @@ -2435,7 +2435,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, - currentTransactionViolations, + currentTransactionViolations || [], policy.requiresTag, policyTags, policy.requiresCategory, From 8b77219fd3d973c32f96ba46dba2184540e64603 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 16 Jan 2024 18:22:50 +0100 Subject: [PATCH 15/82] fix comments --- src/ONYXKEYS.ts | 2 +- src/libs/CardUtils.ts | 8 ++++---- src/libs/GetPhysicalCardUtils.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- src/libs/actions/Wallet.ts | 7 +------ src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 10 +++++----- .../settings/Wallet/Card/GetPhysicalCardAddress.tsx | 2 +- .../settings/Wallet/Card/GetPhysicalCardConfirm.tsx | 2 +- src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx | 6 +++--- .../settings/Wallet/Card/GetPhysicalCardPhone.tsx | 4 ++-- src/types/onyx/index.ts | 2 +- 11 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bb0fdc16188b..3353651fbfc3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -526,7 +526,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 11b68244fbe1..ba7809bba907 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,10 +1,10 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card} from '@src/types/onyx'; +import type {Card, CardList} from '@src/types/onyx'; import * as Localize from './Localize'; let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; @@ -73,11 +73,11 @@ function getYearFromExpirationDateString(expirationDateString: string) { * @param cardList - collection of assigned cards * @returns collection of assigned cards grouped by domain */ -function getDomainCards(cardList: OnyxCollection) { +function getDomainCards(cardList: OnyxEntry): Record { // Check for domainName to filter out personal credit cards. const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); - return lodash.groupBy(activeCards, (card) => card?.domainName); + return lodash.groupBy(activeCards, (card) => card.domainName as string); } /** diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 77a0b8d26708..82d991efb3aa 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -76,7 +76,7 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv * @param draftValues * @returns */ -function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { +function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { const {addressLine1, addressLine2, city = '', country = '', legalFirstName, legalLastName, phoneNumber, state = '', zipPostCode = ''} = draftValues ?? {}; return { legalFirstName, diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 6b73636e6d82..e0275d717472 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), undefined); + Onyx.set(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index bc2fb518d8e6..8486402d2799 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -286,12 +286,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str } function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails) { - const { - legalFirstName, - legalLastName, - phoneNumber, - address: {city, country, state, street, zip}, - } = privatePersonalDetails; + const {legalFirstName = '', legalLastName = '', phoneNumber = '', address: {city = '', country = '', state = '', street = '', zip = ''} = {}} = privatePersonalDetails; type RequestPhysicalExpensifyCardParams = { authToken: string; diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 3877ec4c9009..96af02d64dd7 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -34,7 +34,7 @@ type BaseGetPhysicalCardOnyxProps = { privatePersonalDetails: OnyxEntry; /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Session info for the currently logged in user. */ session: OnyxEntry; @@ -72,7 +72,6 @@ type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { onValidate?: OnValidate; }; - function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate}: RenderContentProps) { const styles = useThemeStyles(); @@ -87,7 +86,7 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} > {children} - ) + ); } function BaseGetPhysicalCard({ @@ -150,8 +149,9 @@ function BaseGetPhysicalCard({ if (isConfirmation) { const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = domainCards.find((card) => card?.isVirtual); - const cardID = virtualCard?.cardID ?? ''; - Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken, updatedPrivatePersonalDetails); + const cardID = virtualCard?.cardID ?? 0; + + Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken ?? '', updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 578a36afc31f..849e37835528 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -15,7 +15,7 @@ import type {RenderContentProps} from './BaseGetPhysicalCard'; type GetPhysicalCardAddressOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardAddressProps = GetPhysicalCardAddressOnyxProps & StackScreenProps; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 967d919b239c..af4343d67af0 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -31,7 +31,7 @@ const goToGetPhysicalCardAddress = (domain: string) => { type GetPhysicalCardConfirmOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardConfirmProps = GetPhysicalCardConfirmOnyxProps & StackScreenProps; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 4f9996a2feb5..6604f08ed015 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -1,8 +1,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,7 +22,7 @@ type OnValidateResult = { type GetPhysicalCardNameOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardNameProps = GetPhysicalCardNameOnyxProps & StackScreenProps; @@ -77,7 +77,7 @@ function GetPhysicalCardName({ defaultValue={legalFirstName} shouldSaveDraft /> - ; + draftValues: OnyxEntry; }; type GetPhysicalCardPhoneProps = GetPhysicalCardPhoneOnyxProps & StackScreenProps; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e477567ee3b8..901476159bbd 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,8 +31,8 @@ import type Policy from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type {PolicyTag, PolicyTags} from './PolicyTag'; import type PolicyReportField from './PolicyReportField'; +import type {PolicyTag, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; From ede3bbe35c4564871666fea79d55b946d0a8afa4 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 16 Jan 2024 16:06:24 -0800 Subject: [PATCH 16/82] More prettier changes --- src/libs/ViolationsUtils.ts | 2 +- src/types/onyx/PolicyTag.ts | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index dd128a68c703..e24ac5277283 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 7c3636551746..cea555a2a0f9 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -14,17 +14,19 @@ type PolicyTags = Record; // When queried from Onyx, if there is no matching policy tag list, the data // returned will be an empty object, represented by Record. -type PolicyTagList = Record< - T, - { - /** Name of the tag list */ - name: T; +type PolicyTagList = + | Record< + T, + { + /** Name of the tag list */ + name: T; - /** Flag that determines if tags are required */ - required: boolean; + /** Flag that determines if tags are required */ + required: boolean; - tags: PolicyTags; - } -> | Record; + tags: PolicyTags; + } + > + | Record; export type {PolicyTag, PolicyTags, PolicyTagList}; From ba7057d11c79c7dabfa9bc007fb2c898be1ffd8e Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 17 Jan 2024 09:48:56 +0100 Subject: [PATCH 17/82] fix clean function --- src/libs/CardUtils.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index ba7809bba907..1e2b5ab0be3c 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -77,7 +77,7 @@ function getDomainCards(cardList: OnyxEntry): Record { // Check for domainName to filter out personal credit cards. const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName as string); + return lodash.groupBy(activeCards, (card) => card.domainName); } /** diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index e0275d717472..c6f511c7caf1 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.set(FormUtils.getDraftKey(formID), {}); + Onyx.set(FormUtils.getDraftKey(formID), null); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; From 0de53952178e9f73c075cdcc095b46902f76c422 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 17:30:32 +0700 Subject: [PATCH 18/82] fix: duplicated waypoints after dragging --- src/components/DistanceRequest/index.js | 22 ++++++++++-- src/libs/actions/Transaction.ts | 35 ++++++++++--------- .../request/step/IOURequestStepDistance.js | 20 +++++++++-- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index b63ce337a1d9..4a360cffecfb 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -194,17 +194,33 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe } const newWaypoints = {}; + let emptyWaypointIndex = -1; _.each(data, (waypoint, index) => { newWaypoints[`waypoint${index}`] = lodashGet(waypoints, waypoint, {}); + // Find waypoint that BECOMES empty after dragging + if (_.isEmpty(newWaypoints[`waypoint${index}`]) && !_.isEmpty(lodashGet(waypoints, `waypoint${index}`, {}))) { + emptyWaypointIndex = index; + } }); setOptimisticWaypoints(newWaypoints); // eslint-disable-next-line rulesdir/no-thenable-actions-in-views - Transaction.updateWaypoints(transactionID, newWaypoints).then(() => { - setOptimisticWaypoints(null); + Transaction.updateWaypoints(transactionID, newWaypoints, true).then(() => { + if (emptyWaypointIndex === -1) { + setOptimisticWaypoints(null); + return; + } + // This is a workaround because at this point, transaction data has not been updated yet + const updatedTransaction = { + ...transaction, + ...Transaction.getUpdatedWaypointsTransaction(newWaypoints), + }; + Transaction.removeWaypoint(updatedTransaction, emptyWaypointIndex, true).then(() => { + setOptimisticWaypoints(null); + }); }); }, - [transactionID, waypoints, waypointsList], + [transactionID, transaction, waypoints, waypointsList], ); const submitWaypoints = useCallback(() => { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..3e2ffba3949d 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -100,7 +100,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp } } -function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) { +function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean): Promise { // Index comes from the route params and is a string const index = Number(currentIndex); const existingWaypoints = transaction?.comment?.waypoints ?? {}; @@ -109,7 +109,7 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: const waypointValues = Object.values(existingWaypoints); const removed = waypointValues.splice(index, 1); if (removed.length === 0) { - return; + return Promise.resolve(); } const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {}); @@ -155,10 +155,9 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: }; } if (isDraft) { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); - return; + return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); } - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction); + return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction); } function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): OnyxData { @@ -234,15 +233,8 @@ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) ); } -/** - * Updates all waypoints stored in the transaction specified by the provided transactionID. - * - * @param transactionID - The ID of the transaction to be updated - * @param waypoints - An object containing all the waypoints - * which will replace the existing ones. - */ -function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { - return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { +function getUpdatedWaypointsTransaction(waypoints: WaypointCollection) { + return { comment: { waypoints, }, @@ -261,7 +253,18 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i }, }, }, - }); + }; +} + +/** + * Updates all waypoints stored in the transaction specified by the provided transactionID. + * + * @param transactionID - The ID of the transaction to be updated + * @param waypoints - An object containing all the waypoints + * which will replace the existing ones. + */ +function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { + return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, getUpdatedWaypointsTransaction(waypoints)); } -export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, getRouteForDraft, updateWaypoints}; +export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, getRouteForDraft, getUpdatedWaypointsTransaction, updateWaypoints}; diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 9549a93c8124..c199bef4a01e 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -150,17 +150,33 @@ function IOURequestStepDistance({ } const newWaypoints = {}; + let emptyWaypointIndex = -1; _.each(data, (waypoint, index) => { newWaypoints[`waypoint${index}`] = lodashGet(waypoints, waypoint, {}); + // Find waypoint that BECOMES empty after dragging + if (_.isEmpty(newWaypoints[`waypoint${index}`]) && !_.isEmpty(lodashGet(waypoints, `waypoint${index}`, {}))) { + emptyWaypointIndex = index; + } }); setOptimisticWaypoints(newWaypoints); // eslint-disable-next-line rulesdir/no-thenable-actions-in-views Transaction.updateWaypoints(transactionID, newWaypoints, true).then(() => { - setOptimisticWaypoints(null); + if (emptyWaypointIndex === -1) { + setOptimisticWaypoints(null); + return; + } + // This is a workaround because at this point, transaction data has not been updated yet + const updatedTransaction = { + ...transaction, + ...Transaction.getUpdatedWaypointsTransaction(newWaypoints), + }; + Transaction.removeWaypoint(updatedTransaction, emptyWaypointIndex, true).then(() => { + setOptimisticWaypoints(null); + }); }); }, - [transactionID, waypoints, waypointsList], + [transactionID, transaction, waypoints, waypointsList], ); const submitWaypoints = useCallback(() => { From cb77b60e2ca2689bc7011be5154fb2fa70d717c4 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 24 Jan 2024 13:48:14 +0530 Subject: [PATCH 19/82] Update tagOutOfPolicy violation copy We update the copy to use 'Tag' when tagName is not available --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a754a883d07..e8cdadd3c27a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2114,7 +2114,7 @@ export default { }, smartscanFailed: 'Receipt scanning failed. Enter details manually.', someTagLevelsRequired: 'Missing tag', - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? ''} no longer valid`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? ''} no longer valid`, taxRateChanged: 'Tax rate was modified', From e36610905b11f2f1df86a48a81cd1a70c8a20f44 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 24 Jan 2024 13:49:32 +0530 Subject: [PATCH 20/82] Update taxOutOfPolicy violation copy We update the copy to use 'Tax' when taxName is not available --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e8cdadd3c27a..6b8f56489903 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2116,7 +2116,7 @@ export default { someTagLevelsRequired: 'Missing tag', tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', - taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? ''} no longer valid`, + taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`, taxRateChanged: 'Tax rate was modified', taxRequired: 'Missing tax rate', }, From 10f9139aa38b5242f86c6e7aa1e4a8b71cf4378f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 18:04:27 +0100 Subject: [PATCH 21/82] Migrate WorkspaceInviteMessagePage to TypeScript --- src/ONYXKEYS.ts | 1 + src/libs/OptionsListUtils.js | 2 +- ...Page.js => WorkspaceInviteMessagePage.tsx} | 138 ++++++++---------- .../workspace/WorkspacePageWithSections.tsx | 8 +- .../invoices/WorkspaceInvoicesPage.tsx | 9 +- src/pages/workspace/withPolicy.tsx | 7 +- 6 files changed, 76 insertions(+), 89 deletions(-) rename src/pages/workspace/{WorkspaceInviteMessagePage.js => WorkspaceInviteMessagePage.tsx} (60%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9693c907a5fe..a9e59a55e172 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -460,6 +460,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2973228af51f..2bd15fc96983 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -200,7 +200,7 @@ function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { /** * Return true if personal details data is ready, i.e. report list options can be created. - * @param {Object} personalDetails + * @param {Object | null} personalDetails * @returns {Boolean} */ function isPersonalDetailsReady(personalDetails) { diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.tsx similarity index 60% rename from src/pages/workspace/WorkspaceInviteMessagePage.js rename to src/pages/workspace/WorkspaceInviteMessagePage.tsx index 00bdce30891a..22bdd9c8db94 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,9 +1,10 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashDebounce from 'lodash/debounce'; import React, {useEffect, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -13,118 +14,96 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withNavigationFocus from '@components/withNavigationFocus'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Link from '@userActions/Link'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList} from '@src/types/onyx'; +import type {Errors, Icon} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The accountID of the person */ - accountID: PropTypes.number.isRequired, - - /** The login of the person (either email or phone number) */ - login: PropTypes.string, - - /** The URL of the person's avatar (there should already be a default avatar if - the person doesn't have their own avatar uploaded yet, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); - -const propTypes = { +type WorkspaceInviteMessagePageOnyxProps = { /** All of the personal details for everyone */ - allPersonalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), + allPersonalDetails: OnyxEntry; - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** policyID passed via route: /workspace/:policyID/invite-message */ - policyID: PropTypes.string, - }), - }).isRequired, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry>; - ...policyPropTypes, + /** Updated workspace invite message */ + workspaceInviteMessageDraft: OnyxEntry; }; -const defaultProps = { - ...policyDefaultProps, - allPersonalDetails: {}, - invitedEmailsToAccountIDsDraft: {}, -}; +type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & + WorkspaceInviteMessagePageOnyxProps & + StackScreenProps; -function WorkspaceInviteMessagePage(props) { +function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, policy, route, allPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [welcomeNote, setWelcomeNote] = useState(); + const [welcomeNote, setWelcomeNote] = useState(); const {inputCallbackRef} = useAutoFocusInput(); const getDefaultWelcomeNote = () => - props.workspaceInviteMessageDraft || + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft || translate('workspace.inviteMessage.welcomeNote', { - workspaceName: props.policy.name, + workspaceName: policy?.name ?? '', }); useEffect(() => { - if (!_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { setWelcomeNote(getDefaultWelcomeNote()); return; } - Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID), true); + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const debouncedSaveDraft = _.debounce((newDraft) => { - Policy.setWorkspaceInviteMessageDraft(props.route.params.policyID, newDraft); + const debouncedSaveDraft = lodashDebounce((newDraft: string) => { + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); }); const sendInvitation = () => { Keyboard.dismiss(); - Policy.addMembersToWorkspace(props.invitedEmailsToAccountIDsDraft, welcomeNote, props.route.params.policyID); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); SearchInputManager.searchInput = ''; // Pop the invite message page before navigating to the members page. Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); }; - /** - * Opens privacy url as an external link - * @param {Object} event - */ - const openPrivacyURL = (event) => { - event.preventDefault(); + /** Opens privacy url as an external link */ + const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { + event?.preventDefault(); Link.openExternalLink(CONST.PRIVACY_URL); }; - const validate = () => { - const errorFields = {}; - if (_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + const validate = (): Errors => { + const errorFields: Errors = {}; + if (isEmptyObject(invitedEmailsToAccountIDsDraft)) { errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError'; } return errorFields; }; - const policyName = lodashGet(props.policy, 'name'); + const policyName = policy?.name; return ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > Navigation.dismissModal()} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))} /> + {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + onChangeText={(text: string) => { setWelcomeNote(text); debouncedSaveDraft(text); }} - ref={(el) => { - if (!el) { + ref={(element) => { + if (!element) { return; } - inputCallbackRef(el); - updateMultilineInputRange(el); + inputCallbackRef(element); + updateMultilineInputRange(element); }} /> @@ -210,13 +198,10 @@ function WorkspaceInviteMessagePage(props) { ); } -WorkspaceInviteMessagePage.propTypes = propTypes; -WorkspaceInviteMessagePage.defaultProps = defaultProps; WorkspaceInviteMessagePage.displayName = 'WorkspaceInviteMessagePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ allPersonalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, @@ -226,6 +211,5 @@ export default compose( workspaceInviteMessageDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`, }, - }), - withNavigationFocus, -)(WorkspaceInviteMessagePage); + })(WorkspaceInviteMessagePage), +); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8817f813a990..48e8d60a5a51 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -1,4 +1,3 @@ -import type {RouteProp} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; @@ -20,6 +19,7 @@ import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {PolicyRoute} from './withPolicy'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -40,7 +40,7 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & headerText: string; /** The route object passed to this page from the navigator */ - route: RouteProp<{params: {policyID: string}}>; + route: PolicyRoute; /** Main content of the page */ children: (hasVBA?: boolean, policyID?: string, isUsingECard?: boolean) => ReactNode; @@ -92,7 +92,7 @@ function WorkspacePageWithSections({ const isLoading = reimbursementAccount?.isLoading ?? true; const achState = reimbursementAccount?.achData?.state ?? ''; const isUsingECard = user?.isUsingExpensifyCard ?? false; - const policyID = route.params.policyID; + const policyID = route.params?.policyID; const policyName = policy?.name; const hasVBA = achState === BankAccount.STATE.OPEN; const content = children(hasVBA, policyID, isUsingECard); @@ -132,7 +132,7 @@ function WorkspacePageWithSections({ subtitle={policyName} shouldShowGetAssistanceButton guidesCallTaskID={guidesCallTaskID} - onBackButtonPress={() => Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? ''))} /> {(isLoading || firstRender.current) && shouldShowLoading ? ( diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 79ff76204c69..ffd9a700ae7e 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -1,15 +1,14 @@ -import type {RouteProp} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import useLocalize from '@hooks/useLocalize'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView'; import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView'; -/** Defined route object that contains the policyID param, WorkspacePageWithSections is a common component for Workspaces and expect the route prop that includes the policyID */ -type WorkspaceInvoicesPageProps = { - route: RouteProp<{params: {policyID: string}}>; -}; +type WorkspaceInvoicesPageProps = StackScreenProps; function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index ec38b61fb0dc..aee03f1f74e9 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -5,13 +5,16 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import policyMemberPropType from '@pages/policyMemberPropType'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -type PolicyRoute = RouteProp<{params: {policyID: string}}>; +type PolicyRoute = RouteProp>; function getPolicyIDFromRoute(route: PolicyRoute): string { return route?.params?.policyID ?? ''; @@ -131,4 +134,4 @@ export default function (WrappedComponent: } export {policyPropTypes, policyDefaultProps}; -export type {WithPolicyOnyxProps, WithPolicyProps}; +export type {WithPolicyOnyxProps, WithPolicyProps, PolicyRoute}; From 371e1665a81aa89ad8a02593b70c2ac0fa184fe5 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 24 Jan 2024 16:34:32 -0800 Subject: [PATCH 22/82] Support client-side violations in new commands --- src/libs/actions/IOU.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 574a1a027440..06084fc0704f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1167,12 +1167,15 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val, p * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { billable: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestBillable', params, onyxData); } @@ -1218,12 +1221,15 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag, po * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} category + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestCategory(transactionID, transactionThreadReportID, category) { +function updateMoneyRequestCategory(transactionID, transactionThreadReportID, category, policy, policyTags, policyCategories) { const transactionChanges = { category, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestCategory', params, onyxData); } @@ -1233,12 +1239,15 @@ function updateMoneyRequestCategory(transactionID, transactionThreadReportID, ca * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} comment + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestDescription(transactionID, transactionThreadReportID, comment) { +function updateMoneyRequestDescription(transactionID, transactionThreadReportID, comment, policy, policyTags, policyCategories) { const transactionChanges = { comment, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestDescription', params, onyxData); } From 8810e0abd4df346e558f0a002c44d2d317fe7483 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 26 Jan 2024 16:08:59 +0100 Subject: [PATCH 23/82] update form type, remove unused ts-expect --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 4 ++-- src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx | 2 -- src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx | 1 - src/types/onyx/Form.ts | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 487e7057517f..627cfddcaf62 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -16,9 +16,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CardList, GetPhysicalCardForm, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type OnValidate = (values: OnyxEntry) => void; +type OnValidate = (values: OnyxEntry) => Errors; type RenderContentProps = ChildrenProps & { onSubmit: () => void; @@ -76,7 +77,6 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} const styles = useThemeStyles(); return ( - // @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript. ; -type GetPhysicalCardForm = Form & { +type GetPhysicalCardForm = Form<{ /** Address line 1 for delivery */ addressLine1?: string; @@ -81,7 +81,7 @@ type GetPhysicalCardForm = Form & { /** Zip code for delivery */ zipPostCode?: string; -}; +}>; export default Form; From 38ffff6f91b8895a20c9c53fc2fd0aac730f5cb7 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 26 Jan 2024 11:47:43 -0800 Subject: [PATCH 24/82] Pass policy args to all commands --- src/pages/EditRequestPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 18f10e09bdbc..3b71e7046158 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -186,21 +186,21 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p ({category: newCategory}) => { // In case the same category has been selected, reset the category. const updatedCategory = newCategory === transactionCategory ? '' : newCategory; - IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory); + IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionCategory, transaction.transactionID, report.reportID], + [transactionCategory, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); const saveComment = useCallback( ({comment: newComment}) => { // Only update comment if it has changed if (newComment.trim() !== transactionDescription) { - IOU.updateMoneyRequestDescription(transaction.transactionID, report.reportID, newComment.trim()); + IOU.updateMoneyRequestDescription(transaction.transactionID, report.reportID, newComment.trim(), policy, policyTags, policyCategories); } Navigation.dismissModal(); }, - [transactionDescription, transaction.transactionID, report.reportID], + [transactionDescription, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { From 3d4a3e24fc5ca7baf97b9debbd288c8e9c4f0575 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 29 Jan 2024 16:25:12 +0100 Subject: [PATCH 25/82] Migrate WorkspaceInvitePage to TypeScript --- src/ONYXKEYS.ts | 4 +- src/libs/OptionsListUtils.ts | 7 +- .../workspace/WorkspaceInviteMessagePage.tsx | 26 +- ...eInvitePage.js => WorkspaceInvitePage.tsx} | 224 +++++++++--------- .../onyx/InvitedEmailsToAccountIDsDraft.ts | 3 + src/types/onyx/index.ts | 2 + 6 files changed, 129 insertions(+), 137 deletions(-) rename src/pages/workspace/{WorkspaceInvitePage.js => WorkspaceInvitePage.tsx} (58%) create mode 100644 src/types/onyx/InvitedEmailsToAccountIDsDraft.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8f8627a2927d..7aa89de1caa4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -453,8 +453,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDsDraft | undefined; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6332a57deec0..93664f1dbc21 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -310,9 +310,9 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, perso /** * Return true if personal details data is ready, i.e. report list options can be created. */ -function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { - const personalDetailsKeys = Object.keys(personalDetails ?? {}); - return personalDetailsKeys.some((key) => personalDetails?.[key]?.accountID); +function isPersonalDetailsReady(personalDetails: OnyxEntry | ReportUtils.OptionData[]): boolean { + const personalDetailsValues = Array.isArray(personalDetails) ? personalDetails : Object.values(personalDetails ?? {}); + return personalDetailsValues.some((personalDetail) => personalDetail?.accountID); } /** @@ -1993,3 +1993,4 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, }; +export type {MemberForList}; diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 22bdd9c8db94..bd206811ff5a 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -11,6 +11,7 @@ import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -28,8 +29,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList} from '@src/types/onyx'; -import type {Errors, Icon} from '@src/types/onyx/OnyxCommon'; +import type {InvitedEmailsToAccountIDsDraft, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -40,10 +41,10 @@ type WorkspaceInviteMessagePageOnyxProps = { allPersonalDetails: OnyxEntry; /** An object containing the accountID for every invited user email */ - invitedEmailsToAccountIDsDraft: OnyxEntry>; + invitedEmailsToAccountIDsDraft: OnyxEntry; /** Updated workspace invite message */ - workspaceInviteMessageDraft: OnyxEntry; + workspaceInviteMessageDraft: OnyxEntry; }; type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & @@ -124,7 +125,6 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT onCloseButtonPress={() => Navigation.dismissModal()} onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + ref={(element: AnimatedTextInputRef) => { if (!element) { return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.tsx similarity index 58% rename from src/pages/workspace/WorkspaceInvitePage.js rename to src/pages/workspace/WorkspaceInvitePage.tsx index 72f3747c127c..172dd12314fa 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,11 +1,10 @@ import {useNavigation} from '@react-navigation/native'; +import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,85 +13,73 @@ import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {MemberForList} from '@libs/OptionsListUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Beta, InvitedEmailsToAccountIDsDraft, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The login of the person (either email or phone number) */ - login: PropTypes.string, +type SelectedOption = Partial; - /** The URL of the person's avatar (there should already be a default avatar if - the person doesn't have their own avatar uploaded yet, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); +type WorkspaceInvitePageOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; -const propTypes = { /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** policyID passed via route: /workspace/:policyID/invite */ - policyID: PropTypes.string, - }), - }).isRequired, - - isLoadingReportData: PropTypes.bool, - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), - ...policyPropTypes, -}; + betas: OnyxEntry; -const defaultProps = { - personalDetails: {}, - betas: [], - isLoadingReportData: true, - invitedEmailsToAccountIDsDraft: {}, - ...policyDefaultProps, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry; }; -function WorkspaceInvitePage(props) { +type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps; + +function WorkspaceInvitePage({ + route, + policyMembers, + personalDetails: personalDetailsProp, + betas, + invitedEmailsToAccountIDsDraft, + policy, + isLoadingReportData = true, +}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [personalDetails, setPersonalDetails] = useState([]); - const [usersToInvite, setUsersToInvite] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [personalDetails, setPersonalDetails] = useState([]); + const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation = useNavigation(); + const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { - const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); - Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); + Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; useEffect(() => { setSearchTerm(SearchInputManager.searchInput); return () => { - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); }; - }, [props.route.params.policyID]); + }, [route.params.policyID]); useEffect(() => { - Policy.clearErrors(props.route.params.policyID); + Policy.clearErrors(route.params.policyID); openWorkspaceInvitePage(); // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); @@ -111,54 +98,66 @@ function WorkspaceInvitePage(props) { useNetwork({onReconnect: openWorkspaceInvitePage}); - const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); + const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]); useEffect(() => { - const newUsersToInviteDict = {}; - const newPersonalDetailsDict = {}; - const newSelectedOptionsDict = {}; + const newUsersToInviteDict: Record = {}; + const newPersonalDetailsDict: Record = {}; + const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap = {}; - _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); + const detailsMap: Record = {}; + inviteOptions.personalDetails.forEach((detail) => { + if (!detail.login) { + return; + } - const newSelectedOptions = []; - _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => { - if (!_.has(detailsMap, login)) { + detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail); + }); + + const newSelectedOptions: SelectedOption[] = []; + Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { + if (!(login in detailsMap)) { return; } newSelectedOptions.push({...detailsMap[login], isSelected: true}); }); - _.each(selectedOptions, (option) => { - newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); + selectedOptions.forEach((option) => { + newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); }); const userToInvite = inviteOptions.userToInvite; // Only add the user to the invites list if it is valid - if (userToInvite) { + if (typeof userToInvite?.accountID === 'number') { newUsersToInviteDict[userToInvite.accountID] = userToInvite; } // Add all personal details to the new dict - _.each(inviteOptions.personalDetails, (details) => { + inviteOptions.personalDetails.forEach((details) => { + if (typeof details.accountID !== 'number') { + return; + } newPersonalDetailsDict[details.accountID] = details; }); // Add all selected options to the new dict - _.each(newSelectedOptions, (option) => { + newSelectedOptions.forEach((option) => { + if (typeof option.accountID !== 'number') { + return; + } newSelectedOptionsDict[option.accountID] = option; }); // Strip out dictionary keys and update arrays - setUsersToInvite(_.values(newUsersToInviteDict)); - setPersonalDetails(_.values(newPersonalDetailsDict)); - setSelectedOptions(_.values(newSelectedOptionsDict)); + setUsersToInvite(Object.values(newUsersToInviteDict)); + setPersonalDetails(Object.values(newPersonalDetailsDict)); + setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]); + }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); const sections = useMemo(() => { const sectionsArr = []; @@ -171,13 +170,13 @@ function WorkspaceInvitePage(props) { // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { - filterSelectedOptions = _.filter(selectedOptions, (option) => { - const accountID = lodashGet(option, 'accountID', null); - const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); + filterSelectedOptions = selectedOptions.filter((option) => { + const accountID = option.accountID; + const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchTerm.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); - const isPartOfSearchTerm = option.text.toLowerCase().includes(searchValue) || option.login.toLowerCase().includes(searchValue); + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); } @@ -191,20 +190,20 @@ function WorkspaceInvitePage(props) { indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results - const selectedLogins = _.map(selectedOptions, ({login}) => login); - const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); + const selectedLogins = selectedOptions.map(({login}) => login); + const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login)); + const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => OptionsListUtils.formatMemberForList(item)); sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, - shouldShow: !_.isEmpty(personalDetailsFormatted), + shouldShow: !isEmptyObject(personalDetailsFormatted), indexOffset, }); indexOffset += personalDetailsFormatted.length; - _.each(usersToInvite, (userToInvite) => { - const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); + Object.values(usersToInvite).forEach((userToInvite) => { + const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); if (hasUnselectedUserToInvite) { sectionsArr.push({ @@ -219,14 +218,14 @@ function WorkspaceInvitePage(props) { return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option) => { - Policy.clearErrors(props.route.params.policyID); + const toggleOption = (option: OptionData) => { + Policy.clearErrors(route.params.policyID); - const isOptionInList = _.some(selectedOptions, (selectedOption) => selectedOption.login === option.login); + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - let newSelectedOptions; + let newSelectedOptions: SelectedOption[]; if (isOptionInList) { - newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; } @@ -234,14 +233,14 @@ function WorkspaceInvitePage(props) { setSelectedOptions(newSelectedOptions); }; - const validate = () => { - const errors = {}; + const validate = (): boolean => { + const errors: Errors = {}; if (selectedOptions.length <= 0) { - errors.noUserSelected = true; + errors.noUserSelected = 'true'; } - Policy.setWorkspaceErrors(props.route.params.policyID, errors); - return _.size(errors) <= 0; + Policy.setWorkspaceErrors(route.params.policyID, errors); + return isEmptyObject(errors); }; const inviteUser = () => { @@ -249,27 +248,24 @@ function WorkspaceInvitePage(props) { return; } - const invitedEmailsToAccountIDs = {}; - _.each(selectedOptions, (option) => { - const login = option.login || ''; - const accountID = lodashGet(option, 'accountID', ''); + const invitedEmailsToAccountIDs: Record = {}; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } invitedEmailsToAccountIDs[login] = Number(accountID); }); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, invitedEmailsToAccountIDs); - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(props.route.params.policyID)); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs); + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID)); }; - const [policyName, shouldShowAlertPrompt] = useMemo( - () => [lodashGet(props.policy, 'name'), _.size(lodashGet(props.policy, 'errors', {})) > 0 || lodashGet(props.policy, 'alertMessage', '').length > 0], - [props.policy], - ); + const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); - if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { + if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.some((email) => email === searchValue)) { return translate('messages.errorMessageInvalidEmail'); } if ( @@ -289,8 +285,8 @@ function WorkspaceInvitePage(props) { testID={WorkspaceInvitePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > { - Policy.clearErrors(props.route.params.policyID); - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + Policy.clearErrors(route.params.policyID); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); }} /> @@ -325,7 +321,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={props.policy.alertMessage} + message={policy?.alertMessage} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter @@ -336,24 +332,18 @@ function WorkspaceInvitePage(props) { ); } -WorkspaceInvitePage.propTypes = propTypes; -WorkspaceInvitePage.defaultProps = defaultProps; WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, betas: { key: ONYXKEYS.BETAS, }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, invitedEmailsToAccountIDsDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, - }), -)(WorkspaceInvitePage); + })(WorkspaceInvitePage), +); diff --git a/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts b/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts new file mode 100644 index 000000000000..d29282b0aee9 --- /dev/null +++ b/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts @@ -0,0 +1,3 @@ +type InvitedEmailsToAccountIDsDraft = Record; + +export default InvitedEmailsToAccountIDsDraft; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 5b04cae58671..a222efbbeed3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -15,6 +15,7 @@ import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; import type IntroSelected from './IntroSelected'; +import type InvitedEmailsToAccountIDsDraft from './InvitedEmailsToAccountIDsDraft'; import type IOU from './IOU'; import type Locale from './Locale'; import type {LoginList} from './Login'; @@ -150,5 +151,6 @@ export type { NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, + InvitedEmailsToAccountIDsDraft, PrivateNotesForm, }; From 4f70618c0b5010fbf887118981cf2c10b7dca2ef Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 29 Jan 2024 17:20:21 +0100 Subject: [PATCH 26/82] TypeScript fixes --- src/components/SelectionList/BaseListItem.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 6 ++--- src/components/SelectionList/types.ts | 25 +++++++++++-------- src/libs/OptionsListUtils.ts | 7 ++++-- src/pages/workspace/WorkspaceInvitePage.tsx | 10 +++++--- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 71845931ba52..37e5ff0bff1c 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -55,7 +55,7 @@ function BaseListItem({ onSelectRow(item)} disabled={isDisabled} - accessibilityLabel={item.text} + accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1d73873d836b..07125a0fb005 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -300,14 +300,14 @@ function BaseSelectionList( selectRow(item, true)} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} - keyForList={item.keyForList} + keyForList={item.keyForList ?? undefined} /> ); }; @@ -471,7 +471,7 @@ function BaseSelectionList( getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item) => item.keyForList} + keyExtractor={(item) => item.keyForList ?? ''} extraData={focusedIndex} indicatorStyle="white" keyboardShouldPersistTaps="always" diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index a82ddef6febb..3e1d74a2e05d 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -34,28 +34,28 @@ type CommonListItemProps = { type User = { /** Text to display */ - text: string; + text?: string; /** Alternate text to display */ - alternateText?: string; + alternateText?: string | null; /** Key used internally by React */ - keyForList: string; + keyForList?: string | null; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean; + isDisabled?: boolean | null; /** User accountID */ - accountID?: number; + accountID?: number | null; /** User login */ - login?: string; + login?: string | null; /** Element to show on the right side of the item */ - rightElement?: ReactElement; + rightElement?: ReactElement | null; /** Icons for the user (can be multiple if it's a Workspace) */ icons?: Icon[]; @@ -85,19 +85,19 @@ type UserListItemProps = CommonListItemProps & { type RadioItem = { /** Text to display */ - text: string; + text?: string; /** Alternate text to display */ - alternateText?: string; + alternateText?: string | null; /** Key used internally by React */ - keyForList: string; + keyForList?: string | null; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean; + isDisabled?: boolean | null; /** Represents the index of the section it came from */ sectionIndex?: number; @@ -129,6 +129,9 @@ type Section = { /** Whether this section items disabled for selection */ isDisabled?: boolean; + + /** Whether this section should be shown or not */ + shouldShow?: boolean; }; type BaseSelectionListProps = Partial & { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 93664f1dbc21..67437957999d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -5,6 +5,7 @@ import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; +import type {ReactElement} from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -109,7 +110,7 @@ type MemberForList = { isDisabled: boolean | null; accountID?: number | null; login: string | null; - rightElement: React.ReactNode | null; + rightElement: ReactElement | null; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; }; @@ -1810,7 +1811,9 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config?: ReportUtils.OptionData | EmptyObject): MemberForList; +function formatMemberForList(member: null | undefined, config?: ReportUtils.OptionData | EmptyObject): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { if (!member) { return undefined; } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 172dd12314fa..3a8023b6256f 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -2,6 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useEffect, useMemo, useState} from 'react'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -10,6 +11,7 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import type {Section} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,7 +19,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {MemberForList} from '@libs/OptionsListUtils'; +import type {MemberForList} from '@libs/OptionsListUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -159,8 +161,8 @@ function WorkspaceInvitePage({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); - const sections = useMemo(() => { - const sectionsArr = []; + const sections: Array>> = useMemo(() => { + const sectionsArr: Array>> = []; let indexOffset = 0; if (!didScreenTransitionEnd) { @@ -218,7 +220,7 @@ function WorkspaceInvitePage({ return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option: OptionData) => { + const toggleOption = (option: SelectedOption) => { Policy.clearErrors(route.params.policyID); const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); From 2fe100c6b2c483332d96b854243d09f87ec391d1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 30 Jan 2024 10:41:47 +0100 Subject: [PATCH 27/82] Type improvements --- src/components/SelectionList/BaseListItem.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 6 +++--- src/components/SelectionList/types.ts | 20 +++++++++---------- src/libs/OptionsListUtils.ts | 20 +++++++++---------- src/pages/RoomInvitePage.js | 6 +++--- src/pages/workspace/WorkspaceInvitePage.tsx | 18 ++++++++--------- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 37e5ff0bff1c..71845931ba52 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -55,7 +55,7 @@ function BaseListItem({ onSelectRow(item)} disabled={isDisabled} - accessibilityLabel={item.text ?? ''} + accessibilityLabel={item.text} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 07125a0fb005..1d73873d836b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -300,14 +300,14 @@ function BaseSelectionList( selectRow(item, true)} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} - keyForList={item.keyForList ?? undefined} + keyForList={item.keyForList} /> ); }; @@ -471,7 +471,7 @@ function BaseSelectionList( getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item) => item.keyForList ?? ''} + keyExtractor={(item) => item.keyForList} extraData={focusedIndex} indicatorStyle="white" keyboardShouldPersistTaps="always" diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 3e1d74a2e05d..e34f0f28be42 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -34,25 +34,25 @@ type CommonListItemProps = { type User = { /** Text to display */ - text?: string; + text: string; /** Alternate text to display */ - alternateText?: string | null; + alternateText?: string; /** Key used internally by React */ - keyForList?: string | null; + keyForList: string; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean | null; + isDisabled?: boolean; /** User accountID */ - accountID?: number | null; + accountID?: number; /** User login */ - login?: string | null; + login?: string; /** Element to show on the right side of the item */ rightElement?: ReactElement | null; @@ -85,19 +85,19 @@ type UserListItemProps = CommonListItemProps & { type RadioItem = { /** Text to display */ - text?: string; + text: string; /** Alternate text to display */ - alternateText?: string | null; + alternateText?: string; /** Key used internally by React */ - keyForList?: string | null; + keyForList: string; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean | null; + isDisabled?: boolean; /** Represents the index of the section it came from */ sectionIndex?: number; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 67437957999d..3f262f360c18 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -16,7 +16,6 @@ import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -104,12 +103,12 @@ type GetOptionsConfig = { type MemberForList = { text: string; - alternateText: string | null; - keyForList: string | null; + alternateText: string; + keyForList: string; isSelected: boolean; - isDisabled: boolean | null; - accountID?: number | null; - login: string | null; + isDisabled: boolean; + accountID?: number; + login: string; rightElement: ReactElement | null; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; @@ -1811,14 +1810,14 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config?: ReportUtils.OptionData | EmptyObject): MemberForList; -function formatMemberForList(member: null | undefined, config?: ReportUtils.OptionData | EmptyObject): undefined; -function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData): MemberForList; +function formatMemberForList(member: null | undefined): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined): MemberForList | undefined { if (!member) { return undefined; } - const accountID = member.accountID; + const accountID = member.accountID ?? undefined; return { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -1834,7 +1833,6 @@ function formatMemberForList(member: ReportUtils.OptionData | null | undefined, rightElement: null, icons: member.icons, pendingAction: member.pendingAction, - ...config, }; } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 588a90e98649..40c9559c9619 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -89,7 +89,7 @@ function RoomInvitePage(props) { // Update selectedOptions with the latest personalDetails information const detailsMap = {}; - _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail, false))); + _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); const newSelectedOptions = []; _.forEach(selectedOptions, (option) => { newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); @@ -145,7 +145,7 @@ function RoomInvitePage(props) { // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false)); + const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); sectionsArr.push({ @@ -159,7 +159,7 @@ function RoomInvitePage(props) { if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, - data: [OptionsListUtils.formatMemberForList(userToInvite, false)], + data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, indexOffset, }); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3a8023b6256f..541b8413b0ec 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -36,7 +36,7 @@ import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -type SelectedOption = Partial; +type MembersSection = SectionListData>; type WorkspaceInvitePageOnyxProps = { /** All of the personal details for everyone */ @@ -63,7 +63,7 @@ function WorkspaceInvitePage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); - const [selectedOptions, setSelectedOptions] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); @@ -105,12 +105,12 @@ function WorkspaceInvitePage({ useEffect(() => { const newUsersToInviteDict: Record = {}; const newPersonalDetailsDict: Record = {}; - const newSelectedOptionsDict: Record = {}; + const newSelectedOptionsDict: Record = {}; const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap: Record = {}; + const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { if (!detail.login) { return; @@ -119,7 +119,7 @@ function WorkspaceInvitePage({ detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail); }); - const newSelectedOptions: SelectedOption[] = []; + const newSelectedOptions: MemberForList[] = []; Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { if (!(login in detailsMap)) { return; @@ -161,8 +161,8 @@ function WorkspaceInvitePage({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); - const sections: Array>> = useMemo(() => { - const sectionsArr: Array>> = []; + const sections: MembersSection[] = useMemo(() => { + const sectionsArr: MembersSection[] = []; let indexOffset = 0; if (!didScreenTransitionEnd) { @@ -220,12 +220,12 @@ function WorkspaceInvitePage({ return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option: SelectedOption) => { + const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - let newSelectedOptions: SelectedOption[]; + let newSelectedOptions: MemberForList[]; if (isOptionInList) { newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { From f8896692f83bf0393714d5e5400b9395b2a0f684 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 30 Jan 2024 10:57:16 +0100 Subject: [PATCH 28/82] Update formatMemberForList typing --- src/libs/OptionsListUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3f262f360c18..73c867381609 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1810,9 +1810,9 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData): MemberForList; -function formatMemberForList(member: null | undefined): undefined; -function formatMemberForList(member: ReportUtils.OptionData | null | undefined): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config?: Partial): MemberForList; +function formatMemberForList(member: null | undefined, config?: Partial): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: Partial = {}): MemberForList | undefined { if (!member) { return undefined; } @@ -1833,6 +1833,7 @@ function formatMemberForList(member: ReportUtils.OptionData | null | undefined): rightElement: null, icons: member.icons, pendingAction: member.pendingAction, + ...config, }; } From ef0b1bdd6787d196f57cc84f99ae9c6115b0cfbb Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 30 Jan 2024 16:12:45 +0100 Subject: [PATCH 29/82] migrate ConnectBankAccountButton to TypeScript --- src/components/ConnectBankAccountButton.js | 57 --------------------- src/components/ConnectBankAccountButton.tsx | 44 ++++++++++++++++ 2 files changed, 44 insertions(+), 57 deletions(-) delete mode 100644 src/components/ConnectBankAccountButton.js create mode 100644 src/components/ConnectBankAccountButton.tsx diff --git a/src/components/ConnectBankAccountButton.js b/src/components/ConnectBankAccountButton.js deleted file mode 100644 index f036918d9429..000000000000 --- a/src/components/ConnectBankAccountButton.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; -import Button from './Button'; -import * as Expensicons from './Icon/Expensicons'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; -import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - ...withLocalizePropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** PolicyID for navigating to bank account route of that policy */ - policyID: PropTypes.string.isRequired, - - /** Button styles, also applied for offline message wrapper */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - style: [], -}; - -function ConnectBankAccountButton(props) { - const styles = useThemeStyles(); - const activeRoute = Navigation.getActiveRouteWithoutParams(); - return props.network.isOffline ? ( - - {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`} - - ) : ( -