From 2327a44642d0117ea01a2ad8b7991dcb6906d82c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 19 Sep 2023 14:54:50 +0200 Subject: [PATCH 01/13] [TS migration] Migrate 'UserUtils.js' lib to TypeScript --- src/libs/{UserUtils.js => UserUtils.ts} | 115 ++++++++++-------------- 1 file changed, 46 insertions(+), 69 deletions(-) rename src/libs/{UserUtils.js => UserUtils.ts} (63%) diff --git a/src/libs/UserUtils.js b/src/libs/UserUtils.ts similarity index 63% rename from src/libs/UserUtils.js rename to src/libs/UserUtils.ts index 2d5930b0dfd8..f91d687411f4 100644 --- a/src/libs/UserUtils.js +++ b/src/libs/UserUtils.ts @@ -1,9 +1,11 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; +import {SvgProps} from 'react-native-svg'; import CONST from '../CONST'; import hashCode from './hashCode'; -import * as Expensicons from '../components/Icon/Expensicons'; +import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; +import Login from '../types/onyx/Login'; + +type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; /** * Searches through given loginList for any contact method / login with an error. @@ -25,36 +27,31 @@ import * as defaultAvatars from '../components/Icon/DefaultAvatars'; * } * } * }} - * - * @param {Object} loginList - * @param {Object} loginList.errorFields - * @returns {Boolean} */ -function hasLoginListError(loginList) { - return _.some(loginList, (login) => _.some(lodashGet(login, 'errorFields', {}), (field) => !_.isEmpty(field))); +function hasLoginListError(loginList: Login[]) { + return loginList?.some((login) => { + const errorFields = login?.errorFields ?? {}; + return Object.values(errorFields).some((field) => Object.keys(field).length > 0); + }); } /** * Searches through given loginList for any contact method / login that requires * an Info brick road status indicator. Currently this only applies if the user * has an unvalidated contact method. - * - * @param {Object} loginList - * @param {String} loginList.validatedDate - * @returns {Boolean} */ -function hasLoginListInfo(loginList) { - return _.some(loginList, (login) => _.isEmpty(login.validatedDate)); +function hasLoginListInfo(loginList: Login[]) { + return loginList?.some((login) => !login.validatedDate); } /** * Gets the appropriate brick road indicator status for a given loginList. * Error status is higher priority, so we check for that first. * - * @param {Object} loginList - * @returns {String} + * @param loginList + * @returns */ -function getLoginListBrickRoadIndicator(loginList) { +function getLoginListBrickRoadIndicator(loginList: Login[]): '' | 'error' | 'info' { if (hasLoginListError(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } @@ -66,42 +63,35 @@ function getLoginListBrickRoadIndicator(loginList) { /** * Hashes provided string and returns a value between [0, range) - * @param {String} text - * @param {Number} range - * @returns {Number} */ -function hashText(text, range) { +function hashText(text: string, range: number): number { return Math.abs(hashCode(text.toLowerCase())) % range; } /** * Helper method to return the default avatar associated with the given accountID - * @param {Number} [accountID] - * @returns {String} + * @param [accountID] + * @returns */ -function getDefaultAvatar(accountID = -1) { +function getDefaultAvatar(accountID = -1): React.FC { if (accountID <= 0) { - return Expensicons.FallbackAvatar; + return FallbackAvatar; } if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { - return Expensicons.ConciergeAvatar; + return ConciergeAvatar; } // There are 24 possible default avatars, so we choose which one this user has based // on a simple modulo operation of their login number. Note that Avatar count starts at 1. - const accountIDHashBucket = (accountID % CONST.DEFAULT_AVATAR_COUNT) + 1; + const accountIDHashBucket = ((accountID % CONST.DEFAULT_AVATAR_COUNT) + 1) as AvatarRange; return defaultAvatars[`Avatar${accountIDHashBucket}`]; } /** * Helper method to return default avatar URL associated with login - * - * @param {Number} [accountID] - * @param {Boolean} [isNewDot] - * @returns {String} */ -function getDefaultAvatarURL(accountID = '', isNewDot = false) { +function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): string { if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { return CONST.CONCIERGE_ICON_URL; } @@ -115,26 +105,25 @@ function getDefaultAvatarURL(accountID = '', isNewDot = false) { /** * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar - * @param {String} [avatarURL] - the avatar source from user's personalDetails - * @returns {Boolean} + * @param [avatarURL] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL) { - if ( - _.isString(avatarURL) && - (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) - ) { - return true; - } - - // We use a hardcoded "default" Concierge avatar - if (_.isString(avatarURL) && (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL)) { - return true; +function isDefaultAvatar(avatarURL: string): boolean { + if (typeof avatarURL === 'string') { + if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { + return true; + } + + // We use a hardcoded "default" Concierge avatar + if (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL) { + return true; + } } if (!avatarURL) { // If null URL, we should also use a default avatar return true; } + return false; } @@ -142,11 +131,10 @@ function isDefaultAvatar(avatarURL) { * Provided a source URL, if source is a default avatar, return the associated SVG. * Otherwise, return the URL pointing to a user-uploaded avatar. * - * @param {String} avatarURL - the avatar source from user's personalDetails - * @param {Number} accountID - the accountID of the user - * @returns {String|Function} + * @param avatarURL - the avatar source from user's personalDetails + * @param accountID - the accountID of the user */ -function getAvatar(avatarURL, accountID) { +function getAvatar(avatarURL: string, accountID: number): React.FC | string { return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; } @@ -154,25 +142,20 @@ function getAvatar(avatarURL, accountID) { * Provided an avatar URL, if avatar is a default avatar, return NewDot default avatar URL. * Otherwise, return the URL pointing to a user-uploaded avatar. * - * @param {String} avatarURL - the avatar source from user's personalDetails - * @param {Number} accountID - the accountID of the user - * @returns {String} + * @param avatarURL - the avatar source from user's personalDetails + * @param accountID - the accountID of the user */ -function getAvatarUrl(avatarURL, accountID) { +function getAvatarUrl(avatarURL: string, accountID: number): string { return isDefaultAvatar(avatarURL) ? getDefaultAvatarURL(accountID, true) : avatarURL; } /** * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. - * - * @param {String} [avatarURL] - * @param {Number} [accountID] - * @returns {String|Function} */ -function getFullSizeAvatar(avatarURL, accountID) { +function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC | string { const source = getAvatar(avatarURL, accountID); - if (!_.isString(source)) { + if (typeof source !== 'string') { return source; } return source.replace('_128', ''); @@ -181,14 +164,10 @@ function getFullSizeAvatar(avatarURL, accountID) { /** * Small sized avatars end with _128.. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. - * - * @param {String} avatarURL - * @param {Number} accountID - * @returns {String|Function} */ -function getSmallSizeAvatar(avatarURL, accountID) { +function getSmallSizeAvatar(avatarURL: string, accountID: number): React.FC | string { const source = getAvatar(avatarURL, accountID); - if (!_.isString(source)) { + if (typeof source !== 'string') { return source; } @@ -207,10 +186,8 @@ function getSmallSizeAvatar(avatarURL, accountID) { /** * Generate a random accountID base on searchValue. - * @param {String} searchValue - * @returns {Number} */ -function generateAccountID(searchValue) { +function generateAccountID(searchValue: string): number { return hashText(searchValue, 2 ** 32); } From adaca7db6abdc0efaa78a751de7d4d23526dcc3f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 19 Sep 2023 21:19:12 +0200 Subject: [PATCH 02/13] Fix tests --- src/ONYXKEYS.ts | 2 +- src/libs/ErrorUtils.ts | 113 ++++++++++++++++++++++ src/libs/UserUtils.ts | 16 ++- src/types/onyx/{Login.ts => LoginList.ts} | 4 +- src/types/onyx/index.ts | 4 +- 5 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 src/libs/ErrorUtils.ts rename src/types/onyx/{Login.ts => LoginList.ts} (91%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index af060ea58901..554c8f40ead3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -318,7 +318,7 @@ type OnyxValues = { [ONYXKEYS.COUNTRY_CODE]: number; [ONYXKEYS.COUNTRY]: string; [ONYXKEYS.USER]: OnyxTypes.User; - [ONYXKEYS.LOGIN_LIST]: OnyxTypes.Login; + [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts new file mode 100644 index 000000000000..66dbd923a82d --- /dev/null +++ b/src/libs/ErrorUtils.ts @@ -0,0 +1,113 @@ +import CONST from '../CONST'; +import DateUtils from './DateUtils'; +import * as Localize from './Localize'; +import Response from '../types/onyx/Response'; +import {ErrorFields} from '../types/onyx/OnyxCommon'; + +function getAuthenticateErrorMessage(response: Response): string { + switch (response.jsonCode) { + case CONST.JSON_CODE.UNABLE_TO_RETRY: + return 'session.offlineMessageRetry'; + case 401: + return 'passwordForm.error.incorrectLoginOrPassword'; + case 402: + // If too few characters are passed as the password, the WAF will pass it to the API as an empty + // string, which results in a 402 error from Auth. + if (response.message === '402 Missing partnerUserSecret') { + return 'passwordForm.error.incorrectLoginOrPassword'; + } + return 'passwordForm.error.twoFactorAuthenticationEnabled'; + case 403: + if (response.message === 'Invalid code') { + return 'passwordForm.error.incorrect2fa'; + } + return 'passwordForm.error.invalidLoginOrPassword'; + case 404: + return 'passwordForm.error.unableToResetPassword'; + case 405: + return 'passwordForm.error.noAccess'; + case 413: + return 'passwordForm.error.accountLocked'; + default: + return 'passwordForm.error.fallback'; + } +} + +/** + * Method used to get an error object with microsecond as the key. + * @param error - error key or message to be saved + */ +function getMicroSecondOnyxError(error: string): Record { + return {[DateUtils.getMicroseconds()]: error}; +} + +type OnyxDataWithErrors = { + errors: Record; +}; + +function getLatestErrorMessage(onyxData: TOnyxData): string { + const errors = onyxData.errors ?? {}; + + if (Object.keys(errors).length === 0) { + return ''; + } + + const key = Object.keys(errors).sort().reverse()[0]; + + return errors[key]; +} + +type OnyxDataWithErrorFields = { + errorFields: ErrorFields; +}; + +function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { + const errorsForField = onyxData.errorFields[fieldName] ?? {}; + + if (Object.keys(errorsForField).length === 0) { + return {}; + } + + const key = Object.keys(errorsForField).sort().reverse()[0]; + + return {[key]: errorsForField[key]}; +} + +function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { + const errorsForField = onyxData.errorFields[fieldName] ?? {}; + + if (Object.keys(errorsForField).length === 0) { + return {}; + } + + const key = Object.keys(errorsForField).sort()[0]; + + return {[key]: errorsForField[key]}; +} + +type ErrorsList = Record; + +/** + * Method used to generate error message for given inputID + * @param errorList - An object containing current errors in the form + * @param message - Message to assign to the inputID errors + */ +function addErrorMessage(errors: ErrorsList, inputID?: string, message?: string) { + if (!message || !inputID) { + return; + } + + const errorList = errors; + const error = errorList[inputID]; + const translatedMessage = Localize.translateIfPhraseKey(message); + + if (!error) { + errorList[inputID] = [translatedMessage, {isTranslated: true}]; + } else if (typeof error === 'string') { + errorList[inputID] = [`${error}\n${translatedMessage}`, {isTranslated: true}]; + } else if (Array.isArray(error)) { + error[0] = `${error[0]}\n${translatedMessage}`; + } +} + +export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, addErrorMessage}; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index f91d687411f4..ba7b7a24ec7b 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -3,7 +3,7 @@ import CONST from '../CONST'; import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; -import Login from '../types/onyx/Login'; +import LoginList from '../types/onyx/LoginList'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; @@ -28,11 +28,9 @@ type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | * } * }} */ -function hasLoginListError(loginList: Login[]) { - return loginList?.some((login) => { - const errorFields = login?.errorFields ?? {}; - return Object.values(errorFields).some((field) => Object.keys(field).length > 0); - }); +function hasLoginListError(loginList: LoginList): boolean { + const errorFields = loginList?.errorFields ?? {}; + return Object.values(errorFields).some((field) => Object.keys(field).length > 0); } /** @@ -40,8 +38,8 @@ function hasLoginListError(loginList: Login[]) { * an Info brick road status indicator. Currently this only applies if the user * has an unvalidated contact method. */ -function hasLoginListInfo(loginList: Login[]) { - return loginList?.some((login) => !login.validatedDate); +function hasLoginListInfo(loginList: LoginList): boolean { + return !loginList.validatedDate; } /** @@ -51,7 +49,7 @@ function hasLoginListInfo(loginList: Login[]) { * @param loginList * @returns */ -function getLoginListBrickRoadIndicator(loginList: Login[]): '' | 'error' | 'info' { +function getLoginListBrickRoadIndicator(loginList: LoginList): '' | 'error' | 'info' { if (hasLoginListError(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } diff --git a/src/types/onyx/Login.ts b/src/types/onyx/LoginList.ts similarity index 91% rename from src/types/onyx/Login.ts rename to src/types/onyx/LoginList.ts index 60ea5985315e..f1be0ffa50e6 100644 --- a/src/types/onyx/Login.ts +++ b/src/types/onyx/LoginList.ts @@ -1,6 +1,6 @@ import * as OnyxCommon from './OnyxCommon'; -type Login = { +type LoginList = { /** Phone/Email associated with user */ partnerUserID?: string; @@ -17,4 +17,4 @@ type Login = { pendingFields?: OnyxCommon.ErrorFields; }; -export default Login; +export default LoginList; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 8711a0d208ef..3af99c45480e 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,7 +11,7 @@ import Task from './Task'; import Currency from './Currency'; import ScreenShareRequest from './ScreenShareRequest'; import User from './User'; -import Login from './Login'; +import LoginList from './LoginList'; import Session from './Session'; import Beta from './Beta'; import BlockedFromConcierge from './BlockedFromConcierge'; @@ -61,7 +61,7 @@ export type { Currency, ScreenShareRequest, User, - Login, + LoginList, Session, Beta, BlockedFromConcierge, From 04cc1cbed48bc37f6543f13250db9c5056f7e904 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 20 Sep 2023 11:23:12 +0200 Subject: [PATCH 03/13] Remove file from another migration --- src/libs/ErrorUtils.ts | 113 ------------------------------------- src/types/onyx/Response.ts | 3 +- 2 files changed, 2 insertions(+), 114 deletions(-) delete mode 100644 src/libs/ErrorUtils.ts diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts deleted file mode 100644 index 66dbd923a82d..000000000000 --- a/src/libs/ErrorUtils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import CONST from '../CONST'; -import DateUtils from './DateUtils'; -import * as Localize from './Localize'; -import Response from '../types/onyx/Response'; -import {ErrorFields} from '../types/onyx/OnyxCommon'; - -function getAuthenticateErrorMessage(response: Response): string { - switch (response.jsonCode) { - case CONST.JSON_CODE.UNABLE_TO_RETRY: - return 'session.offlineMessageRetry'; - case 401: - return 'passwordForm.error.incorrectLoginOrPassword'; - case 402: - // If too few characters are passed as the password, the WAF will pass it to the API as an empty - // string, which results in a 402 error from Auth. - if (response.message === '402 Missing partnerUserSecret') { - return 'passwordForm.error.incorrectLoginOrPassword'; - } - return 'passwordForm.error.twoFactorAuthenticationEnabled'; - case 403: - if (response.message === 'Invalid code') { - return 'passwordForm.error.incorrect2fa'; - } - return 'passwordForm.error.invalidLoginOrPassword'; - case 404: - return 'passwordForm.error.unableToResetPassword'; - case 405: - return 'passwordForm.error.noAccess'; - case 413: - return 'passwordForm.error.accountLocked'; - default: - return 'passwordForm.error.fallback'; - } -} - -/** - * Method used to get an error object with microsecond as the key. - * @param error - error key or message to be saved - */ -function getMicroSecondOnyxError(error: string): Record { - return {[DateUtils.getMicroseconds()]: error}; -} - -type OnyxDataWithErrors = { - errors: Record; -}; - -function getLatestErrorMessage(onyxData: TOnyxData): string { - const errors = onyxData.errors ?? {}; - - if (Object.keys(errors).length === 0) { - return ''; - } - - const key = Object.keys(errors).sort().reverse()[0]; - - return errors[key]; -} - -type OnyxDataWithErrorFields = { - errorFields: ErrorFields; -}; - -function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { - const errorsForField = onyxData.errorFields[fieldName] ?? {}; - - if (Object.keys(errorsForField).length === 0) { - return {}; - } - - const key = Object.keys(errorsForField).sort().reverse()[0]; - - return {[key]: errorsForField[key]}; -} - -function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { - const errorsForField = onyxData.errorFields[fieldName] ?? {}; - - if (Object.keys(errorsForField).length === 0) { - return {}; - } - - const key = Object.keys(errorsForField).sort()[0]; - - return {[key]: errorsForField[key]}; -} - -type ErrorsList = Record; - -/** - * Method used to generate error message for given inputID - * @param errorList - An object containing current errors in the form - * @param message - Message to assign to the inputID errors - */ -function addErrorMessage(errors: ErrorsList, inputID?: string, message?: string) { - if (!message || !inputID) { - return; - } - - const errorList = errors; - const error = errorList[inputID]; - const translatedMessage = Localize.translateIfPhraseKey(message); - - if (!error) { - errorList[inputID] = [translatedMessage, {isTranslated: true}]; - } else if (typeof error === 'string') { - errorList[inputID] = [`${error}\n${translatedMessage}`, {isTranslated: true}]; - } else if (Array.isArray(error)) { - error[0] = `${error[0]}\n${translatedMessage}`; - } -} - -export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, addErrorMessage}; diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index c501034e971c..255ac6d9bae4 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -3,9 +3,10 @@ import {OnyxUpdate} from 'react-native-onyx'; type Response = { previousUpdateID?: number | string; lastUpdateID?: number | string; - jsonCode?: number; + jsonCode?: number | string; onyxData?: OnyxUpdate[]; requestID?: string; + message?: string; }; export default Response; From c03f7e880cda49fe21a204cf0a594c6b3c35fd3b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 21 Sep 2023 15:41:42 +0200 Subject: [PATCH 04/13] Make param optional --- src/libs/UserUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index ba7b7a24ec7b..6b7861138f0c 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -105,7 +105,7 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param [avatarURL] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL: string): boolean { +function isDefaultAvatar(avatarURL?: string): boolean { if (typeof avatarURL === 'string') { if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { return true; From f9ccb69801d7a2f1581d90b1520eb89dc1a13bc7 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 09:44:13 +0200 Subject: [PATCH 05/13] Small improvements after review --- src/ONYXKEYS.ts | 2 +- src/libs/UserUtils.ts | 14 +++++++------- src/types/onyx/{LoginList.ts => Login.ts} | 4 ++-- src/types/onyx/index.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/types/onyx/{LoginList.ts => Login.ts} (91%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 985fb2cc0dd1..05256f2b806c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -321,7 +321,7 @@ type OnyxValues = { [ONYXKEYS.COUNTRY_CODE]: number; [ONYXKEYS.COUNTRY]: string; [ONYXKEYS.USER]: OnyxTypes.User; - [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; + [ONYXKEYS.LOGIN_LIST]: OnyxTypes.Login; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 6b7861138f0c..b129887cf86e 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -1,12 +1,15 @@ import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; import CONST from '../CONST'; import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; -import LoginList from '../types/onyx/LoginList'; +import Login from '../types/onyx/Login'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; +type LoginListIndicator = ValueOf | ''; + /** * Searches through given loginList for any contact method / login with an error. * @@ -28,7 +31,7 @@ type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | * } * }} */ -function hasLoginListError(loginList: LoginList): boolean { +function hasLoginListError(loginList: Login): boolean { const errorFields = loginList?.errorFields ?? {}; return Object.values(errorFields).some((field) => Object.keys(field).length > 0); } @@ -38,18 +41,15 @@ function hasLoginListError(loginList: LoginList): boolean { * an Info brick road status indicator. Currently this only applies if the user * has an unvalidated contact method. */ -function hasLoginListInfo(loginList: LoginList): boolean { +function hasLoginListInfo(loginList: Login): boolean { return !loginList.validatedDate; } /** * Gets the appropriate brick road indicator status for a given loginList. * Error status is higher priority, so we check for that first. - * - * @param loginList - * @returns */ -function getLoginListBrickRoadIndicator(loginList: LoginList): '' | 'error' | 'info' { +function getLoginListBrickRoadIndicator(loginList: Login): LoginListIndicator { if (hasLoginListError(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } diff --git a/src/types/onyx/LoginList.ts b/src/types/onyx/Login.ts similarity index 91% rename from src/types/onyx/LoginList.ts rename to src/types/onyx/Login.ts index f1be0ffa50e6..60ea5985315e 100644 --- a/src/types/onyx/LoginList.ts +++ b/src/types/onyx/Login.ts @@ -1,6 +1,6 @@ import * as OnyxCommon from './OnyxCommon'; -type LoginList = { +type Login = { /** Phone/Email associated with user */ partnerUserID?: string; @@ -17,4 +17,4 @@ type LoginList = { pendingFields?: OnyxCommon.ErrorFields; }; -export default LoginList; +export default Login; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6c1b2375fd49..069909153096 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,7 +11,7 @@ import Task from './Task'; import Currency from './Currency'; import ScreenShareRequest from './ScreenShareRequest'; import User from './User'; -import LoginList from './LoginList'; +import Login from './Login'; import Session from './Session'; import Beta from './Beta'; import BlockedFromConcierge from './BlockedFromConcierge'; @@ -60,7 +60,7 @@ export type { Currency, ScreenShareRequest, User, - LoginList, + Login, Session, Beta, BlockedFromConcierge, From 88bad8736dcb8e55ea1aeac35a59914dcc4b48a2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 13:15:31 +0200 Subject: [PATCH 06/13] Remove ^ (Compatible with version) for typescript (TS doesn't follow semver) --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 64abe30d6187..f87f183fbfaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -221,7 +221,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "type-fest": "^3.12.0", - "typescript": "^5.1.6", + "typescript": "5.1.6", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -46224,9 +46224,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -80606,9 +80606,9 @@ "dev": true }, "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, "typewise": { diff --git a/package.json b/package.json index 6f0d4d70f768..339bcce589aa 100644 --- a/package.json +++ b/package.json @@ -263,7 +263,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "type-fest": "^3.12.0", - "typescript": "^5.1.6", + "typescript": "5.1.6", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", From a6f5d24fe0340b28eb8ad2c12151ebf511bacf7f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 14:20:28 +0200 Subject: [PATCH 07/13] Revert "Remove ^ (Compatible with version) for typescript (TS doesn't follow semver)" This reverts commit 88bad8736dcb8e55ea1aeac35a59914dcc4b48a2. --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f87f183fbfaf..64abe30d6187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -221,7 +221,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "type-fest": "^3.12.0", - "typescript": "5.1.6", + "typescript": "^5.1.6", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -46224,9 +46224,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -80606,9 +80606,9 @@ "dev": true }, "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, "typewise": { diff --git a/package.json b/package.json index 339bcce589aa..6f0d4d70f768 100644 --- a/package.json +++ b/package.json @@ -263,7 +263,7 @@ "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", "type-fest": "^3.12.0", - "typescript": "5.1.6", + "typescript": "^5.1.6", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", From faf29b74a64a0007bc43841ab231354b049f49d4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 14:20:42 +0200 Subject: [PATCH 08/13] Fix type error --- src/libs/PersonalDetailsUtils.js | 155 ------------------------------- src/libs/PersonalDetailsUtils.ts | 142 ++++++++++++++++++++++++++++ src/libs/UserUtils.ts | 3 +- 3 files changed, 144 insertions(+), 156 deletions(-) delete mode 100644 src/libs/PersonalDetailsUtils.js create mode 100644 src/libs/PersonalDetailsUtils.ts diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js deleted file mode 100644 index a401dea4b911..000000000000 --- a/src/libs/PersonalDetailsUtils.js +++ /dev/null @@ -1,155 +0,0 @@ -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import ONYXKEYS from '../ONYXKEYS'; -import * as Localize from './Localize'; -import * as UserUtils from './UserUtils'; -import * as LocalePhoneNumber from './LocalePhoneNumber'; - -let personalDetails = []; -let allPersonalDetails = {}; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - personalDetails = _.values(val); - allPersonalDetails = val; - }, -}); - -/** - * @param {Object} passedPersonalDetails - * @param {Array} pathToDisplayName - * @param {String} [defaultValue] optional default display name value - * @returns {String} - */ -function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue) { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); - - return displayName || defaultValue || Localize.translateLocal('common.hidden'); -} - -/** - * Given a list of account IDs (as number) it will return an array of personal details objects. - * @param {Array} accountIDs - Array of accountIDs - * @param {Number} currentUserAccountID - * @param {Boolean} shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. - * @returns {Array} - Array of personal detail objects - */ -function getPersonalDetailsByIDs(accountIDs, currentUserAccountID, shouldChangeUserDisplayName = false) { - const result = []; - _.each( - _.filter(personalDetails, (detail) => accountIDs.includes(detail.accountID)), - (detail) => { - if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { - result.push({ - ...detail, - displayName: Localize.translateLocal('common.you'), - }); - } else { - result.push(detail); - } - }, - ); - return result; -} - -/** - * Given a list of logins, find the associated personal detail and return related accountIDs. - * - * @param {Array} logins Array of user logins - * @returns {Array} - Array of accountIDs according to passed logins - */ -function getAccountIDsByLogins(logins) { - return _.reduce( - logins, - (foundAccountIDs, login) => { - const currentDetail = _.find(personalDetails, (detail) => detail.login === login); - if (!currentDetail) { - // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet - foundAccountIDs.push(UserUtils.generateAccountID(login)); - } else { - foundAccountIDs.push(Number(currentDetail.accountID)); - } - return foundAccountIDs; - }, - [], - ); -} - -/** - * Given a list of accountIDs, find the associated personal detail and return related logins. - * - * @param {Array} accountIDs Array of user accountIDs - * @returns {Array} - Array of logins according to passed accountIDs - */ -function getLoginsByAccountIDs(accountIDs) { - return _.reduce( - accountIDs, - (foundLogins, accountID) => { - const currentDetail = _.find(personalDetails, (detail) => Number(detail.accountID) === Number(accountID)) || {}; - if (currentDetail.login) { - foundLogins.push(currentDetail.login); - } - return foundLogins; - }, - [], - ); -} - -/** - * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored - * - * @param {Array} logins Array of user logins - * @param {Array} accountIDs Array of user accountIDs - * @returns {Object} - Object with optimisticData, successData and failureData (object of personal details objects) - */ -function getNewPersonalDetailsOnyxData(logins, accountIDs) { - const optimisticData = {}; - const successData = {}; - const failureData = {}; - - _.each(logins, (login, index) => { - const accountID = accountIDs[index]; - - if (_.isEmpty(allPersonalDetails[accountID])) { - optimisticData[accountID] = { - login, - accountID, - avatar: UserUtils.getDefaultAvatarURL(accountID), - displayName: LocalePhoneNumber.formatPhoneNumber(login), - }; - - /** - * Cleanup the optimistic user to ensure it does not permanently persist. - * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. - */ - successData[accountID] = null; - } - }); - - return { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: optimisticData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: successData, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: failureData, - }, - ], - }; -} - -export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts new file mode 100644 index 000000000000..45afaf0f139d --- /dev/null +++ b/src/libs/PersonalDetailsUtils.ts @@ -0,0 +1,142 @@ +import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import {NullishDeep} from 'react-native-onyx/lib/types'; +import ONYXKEYS from '../ONYXKEYS'; +import * as Localize from './Localize'; +import * as UserUtils from './UserUtils'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; +import * as OnyxTypes from '../types/onyx'; + +type PersonalDetailsList = Record; + +let personalDetails: OnyxTypes.PersonalDetails[] = []; +let allPersonalDetails: OnyxEntry> = {}; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => { + personalDetails = Object.values(val ?? {}); + allPersonalDetails = val; + }, +}); + +/** + * @param [defaultValue] optional default display name value + */ +function getDisplayNameOrDefault(displayName: string, defaultValue?: string): string { + return displayName ?? defaultValue ?? Localize.translateLocal('common.hidden'); +} + +/** + * Given a list of account IDs (as number) it will return an array of personal details objects. + * @param accountIDs - Array of accountIDs + * @param shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. + * @returns Array of personal detail objects + */ +function getPersonalDetailsByIDs(accountIDs: number[], currentUserAccountID: number, shouldChangeUserDisplayName = false): OnyxTypes.PersonalDetails[] { + const result: OnyxTypes.PersonalDetails[] = []; + personalDetails + .filter((detail) => accountIDs.includes(detail.accountID)) + .forEach((detail) => { + if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { + result.push({ + ...detail, + displayName: Localize.translateLocal('common.you'), + }); + } else { + result.push(detail); + } + }); + return result; +} + +/** + * Given a list of logins, find the associated personal detail and return related accountIDs. + * + * @param logins Array of user logins + * @returns Array of accountIDs according to passed logins + */ +function getAccountIDsByLogins(logins: string[]): number[] { + return logins.reduce((foundAccountIDs: number[], login) => { + const currentDetail = personalDetails.find((detail) => detail.login === login); + if (!currentDetail) { + // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet + foundAccountIDs.push(UserUtils.generateAccountID(login)); + } else { + foundAccountIDs.push(Number(currentDetail.accountID)); + } + return foundAccountIDs; + }, []); +} + +/** + * Given a list of accountIDs, find the associated personal detail and return related logins. + * + * @param accountIDs Array of user accountIDs + * @returns Array of logins according to passed accountIDs + */ +function getLoginsByAccountIDs(accountIDs: number[]): string[] { + return accountIDs.reduce((foundLogins: string[], accountID) => { + const currentDetail: Partial = personalDetails.find((detail) => Number(detail.accountID) === Number(accountID)) ?? {}; + if (currentDetail.login) { + foundLogins.push(currentDetail.login); + } + return foundLogins; + }, []); +} + +/** + * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored + * + * @param logins Array of user logins + * @param accountIDs Array of user accountIDs + * @returns Object with optimisticData, successData and failureData (object of personal details objects) + */ +function getNewPersonalDetailsOnyxData(logins: string[], accountIDs: number[]): Record { + const optimisticData: NullishDeep = {}; + const successData: NullishDeep = {}; + const failureData: NullishDeep = {}; + + logins.forEach((login, index) => { + const accountID = accountIDs[index]; + + if (allPersonalDetails && Object.keys(allPersonalDetails[accountID]).length === 0) { + optimisticData[accountID] = { + login, + accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), + displayName: LocalePhoneNumber.formatPhoneNumber(login), + }; + + /** + * Cleanup the optimistic user to ensure it does not permanently persist. + * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. + */ + successData[accountID] = null; + } + }); + + return { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticData, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successData, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: failureData, + }, + ], + }; +} + +export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData}; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index b129887cf86e..a35c32ef36f0 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -5,6 +5,7 @@ import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import Login from '../types/onyx/Login'; +import {ErrorFields} from '../types/onyx/OnyxCommon'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; @@ -32,7 +33,7 @@ type LoginListIndicator = ValueOf | '' * }} */ function hasLoginListError(loginList: Login): boolean { - const errorFields = loginList?.errorFields ?? {}; + const errorFields: ErrorFields = loginList?.errorFields ?? {}; return Object.values(errorFields).some((field) => Object.keys(field).length > 0); } From ff6b62277724804cc82cf4e9ff5b8e59c8deb12c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 14:31:45 +0200 Subject: [PATCH 09/13] Revert "Fix type error" This reverts commit faf29b74a64a0007bc43841ab231354b049f49d4. --- src/libs/PersonalDetailsUtils.js | 155 +++++++++++++++++++++++++++++++ src/libs/PersonalDetailsUtils.ts | 142 ---------------------------- src/libs/UserUtils.ts | 3 +- 3 files changed, 156 insertions(+), 144 deletions(-) create mode 100644 src/libs/PersonalDetailsUtils.js delete mode 100644 src/libs/PersonalDetailsUtils.ts diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js new file mode 100644 index 000000000000..a401dea4b911 --- /dev/null +++ b/src/libs/PersonalDetailsUtils.js @@ -0,0 +1,155 @@ +import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import _ from 'underscore'; +import ONYXKEYS from '../ONYXKEYS'; +import * as Localize from './Localize'; +import * as UserUtils from './UserUtils'; +import * as LocalePhoneNumber from './LocalePhoneNumber'; + +let personalDetails = []; +let allPersonalDetails = {}; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => { + personalDetails = _.values(val); + allPersonalDetails = val; + }, +}); + +/** + * @param {Object} passedPersonalDetails + * @param {Array} pathToDisplayName + * @param {String} [defaultValue] optional default display name value + * @returns {String} + */ +function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue) { + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); + + return displayName || defaultValue || Localize.translateLocal('common.hidden'); +} + +/** + * Given a list of account IDs (as number) it will return an array of personal details objects. + * @param {Array} accountIDs - Array of accountIDs + * @param {Number} currentUserAccountID + * @param {Boolean} shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. + * @returns {Array} - Array of personal detail objects + */ +function getPersonalDetailsByIDs(accountIDs, currentUserAccountID, shouldChangeUserDisplayName = false) { + const result = []; + _.each( + _.filter(personalDetails, (detail) => accountIDs.includes(detail.accountID)), + (detail) => { + if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { + result.push({ + ...detail, + displayName: Localize.translateLocal('common.you'), + }); + } else { + result.push(detail); + } + }, + ); + return result; +} + +/** + * Given a list of logins, find the associated personal detail and return related accountIDs. + * + * @param {Array} logins Array of user logins + * @returns {Array} - Array of accountIDs according to passed logins + */ +function getAccountIDsByLogins(logins) { + return _.reduce( + logins, + (foundAccountIDs, login) => { + const currentDetail = _.find(personalDetails, (detail) => detail.login === login); + if (!currentDetail) { + // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet + foundAccountIDs.push(UserUtils.generateAccountID(login)); + } else { + foundAccountIDs.push(Number(currentDetail.accountID)); + } + return foundAccountIDs; + }, + [], + ); +} + +/** + * Given a list of accountIDs, find the associated personal detail and return related logins. + * + * @param {Array} accountIDs Array of user accountIDs + * @returns {Array} - Array of logins according to passed accountIDs + */ +function getLoginsByAccountIDs(accountIDs) { + return _.reduce( + accountIDs, + (foundLogins, accountID) => { + const currentDetail = _.find(personalDetails, (detail) => Number(detail.accountID) === Number(accountID)) || {}; + if (currentDetail.login) { + foundLogins.push(currentDetail.login); + } + return foundLogins; + }, + [], + ); +} + +/** + * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored + * + * @param {Array} logins Array of user logins + * @param {Array} accountIDs Array of user accountIDs + * @returns {Object} - Object with optimisticData, successData and failureData (object of personal details objects) + */ +function getNewPersonalDetailsOnyxData(logins, accountIDs) { + const optimisticData = {}; + const successData = {}; + const failureData = {}; + + _.each(logins, (login, index) => { + const accountID = accountIDs[index]; + + if (_.isEmpty(allPersonalDetails[accountID])) { + optimisticData[accountID] = { + login, + accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), + displayName: LocalePhoneNumber.formatPhoneNumber(login), + }; + + /** + * Cleanup the optimistic user to ensure it does not permanently persist. + * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. + */ + successData[accountID] = null; + } + }); + + return { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticData, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: successData, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: failureData, + }, + ], + }; +} + +export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts deleted file mode 100644 index 45afaf0f139d..000000000000 --- a/src/libs/PersonalDetailsUtils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; -import {NullishDeep} from 'react-native-onyx/lib/types'; -import ONYXKEYS from '../ONYXKEYS'; -import * as Localize from './Localize'; -import * as UserUtils from './UserUtils'; -import * as LocalePhoneNumber from './LocalePhoneNumber'; -import * as OnyxTypes from '../types/onyx'; - -type PersonalDetailsList = Record; - -let personalDetails: OnyxTypes.PersonalDetails[] = []; -let allPersonalDetails: OnyxEntry> = {}; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - personalDetails = Object.values(val ?? {}); - allPersonalDetails = val; - }, -}); - -/** - * @param [defaultValue] optional default display name value - */ -function getDisplayNameOrDefault(displayName: string, defaultValue?: string): string { - return displayName ?? defaultValue ?? Localize.translateLocal('common.hidden'); -} - -/** - * Given a list of account IDs (as number) it will return an array of personal details objects. - * @param accountIDs - Array of accountIDs - * @param shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. - * @returns Array of personal detail objects - */ -function getPersonalDetailsByIDs(accountIDs: number[], currentUserAccountID: number, shouldChangeUserDisplayName = false): OnyxTypes.PersonalDetails[] { - const result: OnyxTypes.PersonalDetails[] = []; - personalDetails - .filter((detail) => accountIDs.includes(detail.accountID)) - .forEach((detail) => { - if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { - result.push({ - ...detail, - displayName: Localize.translateLocal('common.you'), - }); - } else { - result.push(detail); - } - }); - return result; -} - -/** - * Given a list of logins, find the associated personal detail and return related accountIDs. - * - * @param logins Array of user logins - * @returns Array of accountIDs according to passed logins - */ -function getAccountIDsByLogins(logins: string[]): number[] { - return logins.reduce((foundAccountIDs: number[], login) => { - const currentDetail = personalDetails.find((detail) => detail.login === login); - if (!currentDetail) { - // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet - foundAccountIDs.push(UserUtils.generateAccountID(login)); - } else { - foundAccountIDs.push(Number(currentDetail.accountID)); - } - return foundAccountIDs; - }, []); -} - -/** - * Given a list of accountIDs, find the associated personal detail and return related logins. - * - * @param accountIDs Array of user accountIDs - * @returns Array of logins according to passed accountIDs - */ -function getLoginsByAccountIDs(accountIDs: number[]): string[] { - return accountIDs.reduce((foundLogins: string[], accountID) => { - const currentDetail: Partial = personalDetails.find((detail) => Number(detail.accountID) === Number(accountID)) ?? {}; - if (currentDetail.login) { - foundLogins.push(currentDetail.login); - } - return foundLogins; - }, []); -} - -/** - * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored - * - * @param logins Array of user logins - * @param accountIDs Array of user accountIDs - * @returns Object with optimisticData, successData and failureData (object of personal details objects) - */ -function getNewPersonalDetailsOnyxData(logins: string[], accountIDs: number[]): Record { - const optimisticData: NullishDeep = {}; - const successData: NullishDeep = {}; - const failureData: NullishDeep = {}; - - logins.forEach((login, index) => { - const accountID = accountIDs[index]; - - if (allPersonalDetails && Object.keys(allPersonalDetails[accountID]).length === 0) { - optimisticData[accountID] = { - login, - accountID, - avatar: UserUtils.getDefaultAvatarURL(accountID), - displayName: LocalePhoneNumber.formatPhoneNumber(login), - }; - - /** - * Cleanup the optimistic user to ensure it does not permanently persist. - * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. - */ - successData[accountID] = null; - } - }); - - return { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: optimisticData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: successData, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: failureData, - }, - ], - }; -} - -export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData}; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index a35c32ef36f0..b129887cf86e 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -5,7 +5,6 @@ import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import Login from '../types/onyx/Login'; -import {ErrorFields} from '../types/onyx/OnyxCommon'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; @@ -33,7 +32,7 @@ type LoginListIndicator = ValueOf | '' * }} */ function hasLoginListError(loginList: Login): boolean { - const errorFields: ErrorFields = loginList?.errorFields ?? {}; + const errorFields = loginList?.errorFields ?? {}; return Object.values(errorFields).some((field) => Object.keys(field).length > 0); } From f5f1d1d8ae7ba998d9cb82029003b258829b6a2b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 14:33:12 +0200 Subject: [PATCH 10/13] Another try --- src/libs/UserUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index b129887cf86e..a35c32ef36f0 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -5,6 +5,7 @@ import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import Login from '../types/onyx/Login'; +import {ErrorFields} from '../types/onyx/OnyxCommon'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; @@ -32,7 +33,7 @@ type LoginListIndicator = ValueOf | '' * }} */ function hasLoginListError(loginList: Login): boolean { - const errorFields = loginList?.errorFields ?? {}; + const errorFields: ErrorFields = loginList?.errorFields ?? {}; return Object.values(errorFields).some((field) => Object.keys(field).length > 0); } From 42ae0f548ec844bbef74c080ae2c0045dc47615d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Sep 2023 14:53:55 +0200 Subject: [PATCH 11/13] Fix error --- src/libs/UserUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index a35c32ef36f0..503d447c1f47 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -33,8 +33,8 @@ type LoginListIndicator = ValueOf | '' * }} */ function hasLoginListError(loginList: Login): boolean { - const errorFields: ErrorFields = loginList?.errorFields ?? {}; - return Object.values(errorFields).some((field) => Object.keys(field).length > 0); + const errorFields = loginList?.errorFields ?? {}; + return Object.values(errorFields).some((field) => Object.keys(field ?? {}).length > 0); } /** From 84175dce26272430245e55ad7e44dd7334c0704c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Sep 2023 11:12:17 +0200 Subject: [PATCH 12/13] Remove unused import --- src/libs/UserUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 503d447c1f47..751ca5b69609 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -5,7 +5,6 @@ import hashCode from './hashCode'; import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons'; import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import Login from '../types/onyx/Login'; -import {ErrorFields} from '../types/onyx/OnyxCommon'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; From 50e1e79153a414d5780f8944a94dd21b859f239d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Sep 2023 11:40:28 +0200 Subject: [PATCH 13/13] Rerun tests