From 63c06cfdadb8278d25b6c1efebc93f2b28e544f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 26 Sep 2023 11:46:07 +0100 Subject: [PATCH 01/10] Rename file to TS --- src/libs/actions/{PersonalDetails.js => PersonalDetails.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/libs/actions/{PersonalDetails.js => PersonalDetails.ts} (100%) diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.ts similarity index 100% rename from src/libs/actions/PersonalDetails.js rename to src/libs/actions/PersonalDetails.ts From 4ac2485d29d90e4a4b15d29b3a6dd1a27e352db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 26 Sep 2023 11:46:35 +0100 Subject: [PATCH 02/10] Organize imports --- src/libs/actions/PersonalDetails.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 69cf05b89b34..83313e784f09 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -1,14 +1,14 @@ +import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; -import Str from 'expensify-common/lib/str'; import _ from 'underscore'; -import ONYXKEYS from '../../ONYXKEYS'; import CONST from '../../CONST'; +import ONYXKEYS from '../../ONYXKEYS'; +import ROUTES from '../../ROUTES'; import * as API from '../API'; -import * as UserUtils from '../UserUtils'; import * as LocalePhoneNumber from '../LocalePhoneNumber'; -import ROUTES from '../../ROUTES'; import Navigation from '../Navigation/Navigation'; +import * as UserUtils from '../UserUtils'; let currentUserEmail = ''; let currentUserAccountID; @@ -526,21 +526,21 @@ function getPrivatePersonalDetails() { } export { + clearAvatarErrors, + deleteAvatar, + extractFirstAndLastNameFromAvailableDetails, + getCountryISO, getDisplayName, getDisplayNameForTypingIndicator, - updateAvatar, - deleteAvatar, + getPrivatePersonalDetails, openPersonalDetailsPage, openPublicProfilePage, - extractFirstAndLastNameFromAvailableDetails, + updateAddress, + updateAutomaticTimezone, + updateAvatar, + updateDateOfBirth, updateDisplayName, updateLegalName, - updateDateOfBirth, - updateAddress, updatePronouns, - clearAvatarErrors, - updateAutomaticTimezone, updateSelectedTimezone, - getCountryISO, - getPrivatePersonalDetails, }; From 0a1bae4717ce9e1bc8176463d2ab793d8d77fad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 26 Sep 2023 15:52:15 +0100 Subject: [PATCH 03/10] Migrate PersonalDetails to TS --- src/ONYXKEYS.ts | 2 +- src/libs/actions/PersonalDetails.ts | 360 +++++++++++++---------- src/libs/cropOrRotateImage/types.ts | 2 +- src/types/onyx/Form.ts | 7 +- src/types/onyx/PersonalDetails.ts | 32 +- src/types/onyx/PrivatePersonalDetails.ts | 3 + src/types/onyx/index.ts | 3 +- 7 files changed, 239 insertions(+), 170 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6649a33fe15e..6ee7b6982744 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -404,7 +404,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: OnyxTypes.Form; diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 83313e784f09..73ca7149d1c6 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -1,17 +1,24 @@ import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; +import * as OnyxTypes from '../../types/onyx'; +import {PersonalDetailsTimezone} from '../../types/onyx/PersonalDetails'; import * as API from '../API'; import * as LocalePhoneNumber from '../LocalePhoneNumber'; import Navigation from '../Navigation/Navigation'; import * as UserUtils from '../UserUtils'; +import {CustomRNImageManipulatorResult, FileWithUri} from '../cropOrRotateImage/types'; -let currentUserEmail = ''; -let currentUserAccountID; +type FirstAndLastName = { + firstName: string; + lastName: string; +}; + +let currentUserEmail: string | undefined = ''; +let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { @@ -20,13 +27,13 @@ Onyx.connect({ }, }); -let allPersonalDetails; +let allPersonalDetails: OnyxCollection | undefined; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), }); -let privatePersonalDetails; +let privatePersonalDetails: OnyxEntry | undefined; Onyx.connect({ key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, callback: (val) => (privatePersonalDetails = val), @@ -34,64 +41,51 @@ Onyx.connect({ /** * Returns the displayName for a user - * - * @param {String} login - * @param {Object} [personalDetail] - * @returns {String} */ -function getDisplayName(login, personalDetail) { +function getDisplayName(login: string, personalDetail?: Pick): string { // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. const userLogin = LocalePhoneNumber.formatPhoneNumber(login); - const userDetails = personalDetail || lodashGet(allPersonalDetails, login); + const userDetails = personalDetail ?? allPersonalDetails?.[login]; if (!userDetails) { return userLogin; } - const firstName = userDetails.firstName || ''; - const lastName = userDetails.lastName || ''; + const firstName = userDetails.firstName ?? ''; + const lastName = userDetails.lastName ?? ''; const fullName = `${firstName} ${lastName}`.trim(); return fullName || userLogin; } /** - * @param {String} userAccountIDOrLogin - * @param {String} [defaultDisplayName] display name to use if user details don't exist in Onyx or if + * @param [defaultDisplayName] display name to use if user details don't exist in Onyx or if * found details don't include the user's displayName or login - * @returns {String} */ -function getDisplayNameForTypingIndicator(userAccountIDOrLogin, defaultDisplayName = '') { +function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultDisplayName = ''): string { // Try to convert to a number, which means we have an accountID const accountID = Number(userAccountIDOrLogin); // If the user is typing on OldDot, userAccountIDOrLogin will be a string (the user's login), // so Number(string) is NaN. Search for personalDetails by login to get the display name. - if (_.isNaN(accountID)) { - const detailsByLogin = _.findWhere(allPersonalDetails, {login: userAccountIDOrLogin}) || {}; - return detailsByLogin.displayName || userAccountIDOrLogin; + if (Number.isNaN(accountID)) { + const detailsByLogin = _.findWhere(allPersonalDetails ?? {}, {login: userAccountIDOrLogin}); + return detailsByLogin?.displayName ?? userAccountIDOrLogin; } - const detailsByAccountID = lodashGet(allPersonalDetails, accountID, {}); - return detailsByAccountID.displayName || detailsByAccountID.login || defaultDisplayName; + const detailsByAccountID = allPersonalDetails?.[accountID]; + return detailsByAccountID?.displayName ?? detailsByAccountID?.login ?? defaultDisplayName; } /** * Gets the first and last name from the user's personal details. * If the login is the same as the displayName, then they don't exist, * so we return empty strings instead. - * @param {Object} personalDetail - * @param {String} personalDetail.login - * @param {String} personalDetail.displayName - * @param {String} personalDetail.firstName - * @param {String} personalDetail.lastName - * - * @returns {Object} */ -function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}) { - if (firstName || lastName) { - return {firstName: firstName || '', lastName: lastName || ''}; +function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}: OnyxTypes.PersonalDetails): FirstAndLastName { + if (firstName ?? lastName) { + return {firstName: firstName ?? '', lastName: lastName ?? ''}; } if (login && Str.removeSMSDomain(login) === displayName) { return {firstName: '', lastName: ''}; @@ -112,24 +106,23 @@ function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstN /** * Convert country names obtained from the backend to their respective ISO codes * This is for backward compatibility of stored data before E/App#15507 - * @param {String} countryName - * @returns {String} */ -function getCountryISO(countryName) { +function getCountryISO(countryName: string): string { if (_.isEmpty(countryName) || countryName.length === 2) { return countryName; } - return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) || ''; + return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) ?? ''; } -/** - * @param {String} pronouns - */ -function updatePronouns(pronouns) { - API.write( - 'UpdatePronouns', - {pronouns}, - { +function updatePronouns(pronouns: string) { + if (currentUserAccountID) { + type UpdatePronounsParams = { + pronouns: string; + }; + + const parameters: UpdatePronounsParams = {pronouns}; + + API.write('UpdatePronouns', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -141,20 +134,22 @@ function updatePronouns(pronouns) { }, }, ], - }, - ); + }); + } + Navigation.goBack(ROUTES.SETTINGS_PROFILE); } -/** - * @param {String} firstName - * @param {String} lastName - */ -function updateDisplayName(firstName, lastName) { - API.write( - 'UpdateDisplayName', - {firstName, lastName}, - { +function updateDisplayName(firstName: string, lastName: string) { + if (currentUserAccountID) { + type UpdateDisplayNameParams = { + firstName: string; + lastName: string; + }; + + const parameters: UpdateDisplayNameParams = {firstName, lastName}; + + API.write('UpdateDisplayName', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -163,7 +158,7 @@ function updateDisplayName(firstName, lastName) { [currentUserAccountID]: { firstName, lastName, - displayName: getDisplayName(currentUserEmail, { + displayName: getDisplayName(currentUserEmail ?? '', { firstName, lastName, }), @@ -171,67 +166,73 @@ function updateDisplayName(firstName, lastName) { }, }, ], - }, - ); + }); + } + Navigation.goBack(ROUTES.SETTINGS_PROFILE); } -/** - * @param {String} legalFirstName - * @param {String} legalLastName - */ -function updateLegalName(legalFirstName, legalLastName) { - API.write( - 'UpdateLegalName', - {legalFirstName, legalLastName}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - value: { - legalFirstName, - legalLastName, - }, +function updateLegalName(legalFirstName: string, legalLastName: string) { + type UpdateLegalNameParams = { + legalFirstName: string; + legalLastName: string; + }; + + const parameters: UpdateLegalNameParams = {legalFirstName, legalLastName}; + + API.write('UpdateLegalName', parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + legalFirstName, + legalLastName, }, - ], - }, - ); + }, + ], + }); + Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS); } /** - * @param {String} dob - date of birth + * @param dob - date of birth */ -function updateDateOfBirth({dob}) { - API.write( - 'UpdateDateOfBirth', - {dob}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - value: { - dob, - }, +function updateDateOfBirth({dob}: OnyxTypes.DateOfBirthForm) { + type UpdateDateOfBirthParams = { + dob?: string; + }; + + const parameters: UpdateDateOfBirthParams = {dob}; + + API.write('UpdateDateOfBirth', parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + value: { + dob, }, - ], - }, - ); + }, + ], + }); + Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS); } -/** - * @param {String} street - * @param {String} street2 - * @param {String} city - * @param {String} state - * @param {String} zip - * @param {String} country - */ -function updateAddress(street, street2, city, state, zip, country) { - const parameters = { +function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: string) { + type UpdateHomeAddressParams = { + homeAddressStreet: string; + addressStreet2: string; + homeAddressCity: string; + addressState: string; + addressZipCode: string; + addressCountry: string; + addressStateLong?: string; + }; + + const parameters: UpdateHomeAddressParams = { homeAddressStreet: street, addressStreet2: street2, homeAddressCity: city, @@ -245,6 +246,7 @@ function updateAddress(street, street2, city, state, zip, country) { if (country !== CONST.COUNTRY.US) { parameters.addressStateLong = state; } + API.write('UpdateHomeAddress', parameters, { optimisticData: [ { @@ -262,55 +264,61 @@ function updateAddress(street, street2, city, state, zip, country) { }, ], }); + Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS); } /** * Updates timezone's 'automatic' setting, and updates * selected timezone if set to automatically update. - * - * @param {Object} timezone - * @param {Boolean} timezone.automatic - * @param {String} timezone.selected */ -function updateAutomaticTimezone(timezone) { - API.write( - 'UpdateAutomaticTimezone', - { - timezone: JSON.stringify(timezone), - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - timezone, - }, +function updateAutomaticTimezone(timezone: PersonalDetailsTimezone) { + if (!currentUserAccountID) { + return; + } + + type UpdateAutomaticTimezoneParams = { + timezone: string; + }; + + const parameters: UpdateAutomaticTimezoneParams = { + timezone: JSON.stringify(timezone), + }; + + API.write('UpdateAutomaticTimezone', parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [currentUserAccountID]: { + timezone, }, }, - ], - }, - ); + }, + ], + }); } /** * Updates user's 'selected' timezone, then navigates to the * initial Timezone page. - * - * @param {String} selectedTimezone */ -function updateSelectedTimezone(selectedTimezone) { - const timezone = { +function updateSelectedTimezone(selectedTimezone: string) { + const timezone: PersonalDetailsTimezone = { selected: selectedTimezone, }; - API.write( - 'UpdateSelectedTimezone', - { - timezone: JSON.stringify(timezone), - }, - { + + type UpdateSelectedTimezoneParams = { + timezone: string; + }; + + const parameters: UpdateSelectedTimezoneParams = { + timezone: JSON.stringify(timezone), + }; + + if (currentUserAccountID) { + API.write('UpdateSelectedTimezone', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -322,8 +330,9 @@ function updateSelectedTimezone(selectedTimezone) { }, }, ], - }, - ); + }); + } + Navigation.goBack(ROUTES.SETTINGS_TIMEZONE); } @@ -331,7 +340,7 @@ function updateSelectedTimezone(selectedTimezone) { * Fetches additional personal data like legal name, date of birth, address */ function openPersonalDetailsPage() { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, @@ -341,7 +350,7 @@ function openPersonalDetailsPage() { }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, @@ -351,7 +360,7 @@ function openPersonalDetailsPage() { }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, @@ -361,17 +370,20 @@ function openPersonalDetailsPage() { }, ]; - API.read('OpenPersonalDetailsPage', {}, {optimisticData, successData, failureData}); + type OpenPersonalDetailsPageParams = Record; + + const parameters: OpenPersonalDetailsPageParams = {}; + + API.read('OpenPersonalDetailsPage', parameters, {optimisticData, successData, failureData}); } /** * Fetches public profile info about a given user. * The API will only return the accountID, displayName, and avatar for the user * but the profile page will use other info (e.g. contact methods and pronouns) if they are already available in Onyx - * @param {Number} accountID */ -function openPublicProfilePage(accountID) { - const optimisticData = [ +function openPublicProfilePage(accountID: number) { + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -382,7 +394,8 @@ function openPublicProfilePage(accountID) { }, }, ]; - const successData = [ + + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -393,7 +406,8 @@ function openPublicProfilePage(accountID) { }, }, ]; - const failureData = [ + + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -404,15 +418,24 @@ function openPublicProfilePage(accountID) { }, }, ]; - API.read('OpenPublicProfilePage', {accountID}, {optimisticData, successData, failureData}); + + type OpenPublicProfilePageParams = { + accountID: number; + }; + + const parameters: OpenPublicProfilePageParams = {accountID}; + + API.read('OpenPublicProfilePage', parameters, {optimisticData, successData, failureData}); } /** * Updates the user's avatar image - * - * @param {File|Object} file */ -function updateAvatar(file) { +function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { + if (!currentUserAccountID) { + return; + } + const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -453,8 +476,8 @@ function updateAvatar(file) { key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: { [currentUserAccountID]: { - avatar: allPersonalDetails[currentUserAccountID].avatar, - avatarThumbnail: allPersonalDetails[currentUserAccountID].avatarThumbnail || allPersonalDetails[currentUserAccountID].avatar, + avatar: allPersonalDetails?.[currentUserAccountID]?.avatar, + avatarThumbnail: allPersonalDetails?.[currentUserAccountID]?.avatarThumbnail ?? allPersonalDetails?.[currentUserAccountID]?.avatar, pendingFields: { avatar: null, }, @@ -463,13 +486,23 @@ function updateAvatar(file) { }, ]; - API.write('UpdateUserAvatar', {file}, {optimisticData, successData, failureData}); + type UpdateUserAvatarParams = { + file: FileWithUri | CustomRNImageManipulatorResult; + }; + + const parameters: UpdateUserAvatarParams = {file}; + + API.write('UpdateUserAvatar', parameters, {optimisticData, successData, failureData}); } /** * Replaces the user's avatar image with a default avatar */ function deleteAvatar() { + if (!currentUserAccountID) { + return; + } + // We want to use the old dot avatar here as this affects both platforms. const defaultAvatar = UserUtils.getDefaultAvatarURL(currentUserAccountID); @@ -491,20 +524,28 @@ function deleteAvatar() { key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: { [currentUserAccountID]: { - avatar: allPersonalDetails[currentUserAccountID].avatar, - fallbackIcon: allPersonalDetails[currentUserAccountID].fallbackIcon, + avatar: allPersonalDetails?.[currentUserAccountID]?.avatar, + fallbackIcon: allPersonalDetails?.[currentUserAccountID]?.fallbackIcon, }, }, }, ]; - API.write('DeleteUserAvatar', {}, {optimisticData, failureData}); + type DeleteUserAvatarParams = Record; + + const parameters: DeleteUserAvatarParams = {}; + + API.write('DeleteUserAvatar', parameters, {optimisticData, failureData}); } /** * Clear error and pending fields for the current user's avatar */ function clearAvatarErrors() { + if (!currentUserAccountID) { + return; + } + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [currentUserAccountID]: { errorFields: { @@ -519,9 +560,8 @@ function clearAvatarErrors() { /** * Get private personal details value - * @returns {Object} */ -function getPrivatePersonalDetails() { +function getPrivatePersonalDetails(): OnyxEntry | undefined { return privatePersonalDetails; } diff --git a/src/libs/cropOrRotateImage/types.ts b/src/libs/cropOrRotateImage/types.ts index 6abbdab49ea5..09f441bd9324 100644 --- a/src/libs/cropOrRotateImage/types.ts +++ b/src/libs/cropOrRotateImage/types.ts @@ -26,4 +26,4 @@ type CustomRNImageManipulatorResult = RNImageManipulatorResult & {size: number; type CropOrRotateImage = (uri: string, actions: Action[], options: CropOrRotateImageOptions) => Promise; -export type {CropOrRotateImage, CropOptions, Action, FileWithUri, CropOrRotateImageOptions}; +export type {CropOrRotateImage, CropOptions, Action, FileWithUri, CropOrRotateImageOptions, CustomRNImageManipulatorResult}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index cda8c3c1017e..7b7d8d76536a 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -16,6 +16,11 @@ type AddDebitCardForm = Form & { setupComplete: boolean; }; +type DateOfBirthForm = Form & { + /** Date of birth */ + dob?: string; +}; + export default Form; -export type {AddDebitCardForm}; +export type {AddDebitCardForm, DateOfBirthForm}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 64911dbfecb1..818bd8e9f8fc 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -1,3 +1,13 @@ +import * as OnyxCommon from './OnyxCommon'; + +type PersonalDetailsTimezone = { + /** Value of selected timezone */ + selected?: string; + + /** Whether timezone is automatically set */ + automatic?: boolean; +}; + type PersonalDetails = { /** ID of the current user from their personal details */ accountID: number; @@ -20,6 +30,9 @@ type PersonalDetails = { /** Avatar URL of the current user from their personal details */ avatar: string; + /** Avatar thumbnail URL of the current user from their personal details */ + avatarThumbnail?: string; + /** Flag to set when Avatar uploading */ avatarUploading?: boolean; @@ -33,13 +46,20 @@ type PersonalDetails = { localCurrencyCode?: string; /** Timezone of the current user from their personal details */ - timezone?: { - /** Value of selected timezone */ - selected?: string; + timezone?: PersonalDetailsTimezone; + + /** Whether we are loading the data via the API */ + isLoading?: boolean; + + /** Field-specific server side errors keyed by microtime */ + errorFields?: OnyxCommon.ErrorFields; + + /** Field-specific pending states for offline UI status */ + pendingFields?: OnyxCommon.ErrorFields; - /** Whether timezone is automatically set */ - automatic?: boolean; - }; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: string; }; export default PersonalDetails; +export type {PersonalDetailsTimezone}; diff --git a/src/types/onyx/PrivatePersonalDetails.ts b/src/types/onyx/PrivatePersonalDetails.ts index 50ec77212efd..6ef5b75c4a0f 100644 --- a/src/types/onyx/PrivatePersonalDetails.ts +++ b/src/types/onyx/PrivatePersonalDetails.ts @@ -13,6 +13,9 @@ type PrivatePersonalDetails = { /** User's home address */ address?: Address; + + /** Whether we are loading the data via the API */ + isLoading?: boolean; }; export default PrivatePersonalDetails; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 815689efdaaf..bc01d5b829f8 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -40,7 +40,7 @@ import ReportAction from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; import SecurityGroup from './SecurityGroup'; import Transaction from './Transaction'; -import Form, {AddDebitCardForm} from './Form'; +import Form, {AddDebitCardForm, DateOfBirthForm} from './Form'; import RecentWaypoint from './RecentWaypoint'; import RecentlyUsedCategories from './RecentlyUsedCategories'; import RecentlyUsedTags from './RecentlyUsedTags'; @@ -90,6 +90,7 @@ export type { Transaction, Form, AddDebitCardForm, + DateOfBirthForm, OnyxUpdatesFromServer, RecentWaypoint, OnyxUpdateEvent, From 0513362bb655c0a5cc80de4d1fb78a4f5eaff2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 26 Sep 2023 16:26:55 +0100 Subject: [PATCH 04/10] Remove underscore usage --- src/libs/actions/PersonalDetails.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 73ca7149d1c6..6c1c3c924caa 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -1,6 +1,5 @@ import Str from 'expensify-common/lib/str'; import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; -import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; @@ -70,7 +69,7 @@ function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultD // If the user is typing on OldDot, userAccountIDOrLogin will be a string (the user's login), // so Number(string) is NaN. Search for personalDetails by login to get the display name. if (Number.isNaN(accountID)) { - const detailsByLogin = _.findWhere(allPersonalDetails ?? {}, {login: userAccountIDOrLogin}); + const detailsByLogin = Object.entries(allPersonalDetails ?? {}).find(([, value]) => value?.login === userAccountIDOrLogin)?.[1]; return detailsByLogin?.displayName ?? userAccountIDOrLogin; } @@ -108,10 +107,11 @@ function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstN * This is for backward compatibility of stored data before E/App#15507 */ function getCountryISO(countryName: string): string { - if (_.isEmpty(countryName) || countryName.length === 2) { + if (!countryName || countryName.length === 2) { return countryName; } - return _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryName) ?? ''; + + return Object.entries(CONST.ALL_COUNTRIES).find(([, value]) => value === countryName)?.[0] ?? ''; } function updatePronouns(pronouns: string) { From 1f5f2533c353b2bfb54ac51a9cde30e44ae7732f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 26 Sep 2023 18:31:37 +0100 Subject: [PATCH 05/10] Improve PendingFields and ErrorFields --- src/libs/actions/PersonalDetails.ts | 10 +++++----- src/types/onyx/Login.ts | 2 +- src/types/onyx/OnyxCommon.ts | 6 ++++-- src/types/onyx/PersonalDetails.ts | 6 ++++-- src/types/onyx/Transaction.ts | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 6c1c3c924caa..a37360c31eee 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -436,7 +436,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { return; } - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -457,7 +457,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -470,7 +470,7 @@ function updateAvatar(file: FileWithUri | CustomRNImageManipulatorResult) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -506,7 +506,7 @@ function deleteAvatar() { // We want to use the old dot avatar here as this affects both platforms. const defaultAvatar = UserUtils.getDefaultAvatarURL(currentUserAccountID); - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -518,7 +518,7 @@ function deleteAvatar() { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, diff --git a/src/types/onyx/Login.ts b/src/types/onyx/Login.ts index 60ea5985315e..c770e2f81f90 100644 --- a/src/types/onyx/Login.ts +++ b/src/types/onyx/Login.ts @@ -14,7 +14,7 @@ type Login = { errorFields?: OnyxCommon.ErrorFields; /** Field-specific pending states for offline UI status */ - pendingFields?: OnyxCommon.ErrorFields; + pendingFields?: OnyxCommon.PendingFields; }; export default Login; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index db82edcb98ff..66a6055e0067 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -4,7 +4,9 @@ import CONST from '../../CONST'; type PendingAction = ValueOf; -type ErrorFields = Record | null>; +type PendingFields = Record; + +type ErrorFields = Record; type Errors = Record; @@ -14,4 +16,4 @@ type Icon = { name: string; }; -export type {Icon, PendingAction, ErrorFields, Errors}; +export type {Icon, PendingAction, PendingFields, ErrorFields, Errors}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 818bd8e9f8fc..22ddb3e1ee64 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -33,6 +33,8 @@ type PersonalDetails = { /** Avatar thumbnail URL of the current user from their personal details */ avatarThumbnail?: string; + originalFileName?: string; + /** Flag to set when Avatar uploading */ avatarUploading?: boolean; @@ -52,10 +54,10 @@ type PersonalDetails = { isLoading?: boolean; /** Field-specific server side errors keyed by microtime */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: OnyxCommon.ErrorFields<'avatar'>; /** Field-specific pending states for offline UI status */ - pendingFields?: OnyxCommon.ErrorFields; + pendingFields?: OnyxCommon.PendingFields<'avatar' | 'originalFileName'>; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: string; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8327c866df0f..37c95fe460b3 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -30,7 +30,7 @@ type Transaction = { created: string; currency: string; errors: OnyxCommon.Errors; - errorFields?: OnyxCommon.ErrorFields; + errorFields?: OnyxCommon.ErrorFields<'route'>; // The name of the file used for a receipt (formerly receiptFilename) filename?: string; merchant: string; From b76af47dd63fe87b11214e14dd40be6de7e81eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 3 Oct 2023 16:19:38 +0100 Subject: [PATCH 06/10] Improve typings --- src/libs/actions/PersonalDetails.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 510162420c4a..7a55451caf83 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -26,13 +26,13 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxCollection | undefined; +let allPersonalDetails: OnyxCollection = null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), }); -let privatePersonalDetails: OnyxEntry | undefined; +let privatePersonalDetails: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, callback: (val) => (privatePersonalDetails = val), @@ -561,7 +561,7 @@ function clearAvatarErrors() { /** * Get private personal details value */ -function getPrivatePersonalDetails(): OnyxEntry | undefined { +function getPrivatePersonalDetails(): OnyxEntry { return privatePersonalDetails; } From 3992778b81d9c8cae8ebba0d417ea80a7a0b9c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 4 Oct 2023 12:51:46 +0100 Subject: [PATCH 07/10] Minor fixes --- src/libs/actions/PersonalDetails.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 7a55451caf83..717814ee085a 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -16,13 +16,13 @@ type FirstAndLastName = { lastName: string; }; -let currentUserEmail: string | undefined = ''; -let currentUserAccountID: number | undefined; +let currentUserEmail = ''; +let currentUserAccountID = -1; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - currentUserEmail = val ? val.email : ''; - currentUserAccountID = val ? val.accountID : -1; + currentUserEmail = val?.email ?? ''; + currentUserAccountID = val?.accountID ?? -1; }, }); @@ -41,7 +41,7 @@ Onyx.connect({ /** * Returns the displayName for a user */ -function getDisplayName(login: string, personalDetail?: Pick): string { +function getDisplayName(login: string, personalDetail: Pick | null): string { // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. const userLogin = LocalePhoneNumber.formatPhoneNumber(login); From 22509917b0f172c999a8e31ee350521cfa8a3b65 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 9 Oct 2023 10:04:08 +0200 Subject: [PATCH 08/10] fix: resolve comments --- src/libs/actions/PersonalDetails.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 717814ee085a..6600945b8601 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -1,9 +1,9 @@ import Str from 'expensify-common/lib/str'; -import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import ROUTES from '../../ROUTES'; -import * as OnyxTypes from '../../types/onyx'; +import {PersonalDetails, PrivatePersonalDetails, DateOfBirthForm} from '../../types/onyx'; import {Timezone} from '../../types/onyx/PersonalDetails'; import * as API from '../API'; import * as LocalePhoneNumber from '../LocalePhoneNumber'; @@ -26,13 +26,13 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxCollection = null; +let allPersonalDetails: OnyxEntry> = null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), }); -let privatePersonalDetails: OnyxEntry = null; +let privatePersonalDetails: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, callback: (val) => (privatePersonalDetails = val), @@ -41,7 +41,7 @@ Onyx.connect({ /** * Returns the displayName for a user */ -function getDisplayName(login: string, personalDetail: Pick | null): string { +function getDisplayName(login: string, personalDetail: Pick | null): string { // If we have a number like +15857527441@expensify.sms then let's remove @expensify.sms and format it // so that the option looks cleaner in our UI. const userLogin = LocalePhoneNumber.formatPhoneNumber(login); @@ -82,7 +82,7 @@ function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultD * If the login is the same as the displayName, then they don't exist, * so we return empty strings instead. */ -function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}: OnyxTypes.PersonalDetails): FirstAndLastName { +function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}: PersonalDetails): FirstAndLastName { if (firstName ?? lastName) { return {firstName: firstName ?? '', lastName: lastName ?? ''}; } @@ -199,7 +199,7 @@ function updateLegalName(legalFirstName: string, legalLastName: string) { /** * @param dob - date of birth */ -function updateDateOfBirth({dob}: OnyxTypes.DateOfBirthForm) { +function updateDateOfBirth({dob}: DateOfBirthForm) { type UpdateDateOfBirthParams = { dob?: string; }; @@ -561,7 +561,7 @@ function clearAvatarErrors() { /** * Get private personal details value */ -function getPrivatePersonalDetails(): OnyxEntry { +function getPrivatePersonalDetails(): OnyxEntry { return privatePersonalDetails; } From 067cce1ac0fdda3888ac067939df24d173e4149b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 26 Oct 2023 09:57:21 -0300 Subject: [PATCH 09/10] Address review comments --- src/libs/actions/PersonalDetails.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 6600945b8601..e2dbef071f9b 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -70,11 +70,17 @@ function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultD // so Number(string) is NaN. Search for personalDetails by login to get the display name. if (Number.isNaN(accountID)) { const detailsByLogin = Object.entries(allPersonalDetails ?? {}).find(([, value]) => value?.login === userAccountIDOrLogin)?.[1]; - return detailsByLogin?.displayName ?? userAccountIDOrLogin; + + // It's possible for displayName to be empty string, so we must fallback to userAccountIDOrLogin. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return detailsByLogin?.displayName || userAccountIDOrLogin; } const detailsByAccountID = allPersonalDetails?.[accountID]; - return detailsByAccountID?.displayName ?? detailsByAccountID?.login ?? defaultDisplayName; + + // It's possible for displayName to be empty string, so we must fallback to login or defaultDisplayName. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return detailsByAccountID?.displayName || detailsByAccountID?.login || defaultDisplayName; } /** @@ -83,7 +89,9 @@ function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultD * so we return empty strings instead. */ function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}: PersonalDetails): FirstAndLastName { - if (firstName ?? lastName) { + // It's possible for firstName to be empty string, so we must consider lastName instead. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (firstName || lastName) { return {firstName: firstName ?? '', lastName: lastName ?? ''}; } if (login && Str.removeSMSDomain(login) === displayName) { From 58eb8f3e8d100cf88a3a866346b7a9f866e2a9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Fri, 27 Oct 2023 09:33:28 -0300 Subject: [PATCH 10/10] Improve comments --- src/libs/actions/PersonalDetails.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index e2dbef071f9b..90a53a8de8df 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -55,6 +55,7 @@ function getDisplayName(login: string, personalDetail: Pick value?.login === userAccountIDOrLogin)?.[1]; - // It's possible for displayName to be empty string, so we must fallback to userAccountIDOrLogin. + // It's possible for displayName to be empty string, so we must use "||" to fallback to userAccountIDOrLogin. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return detailsByLogin?.displayName || userAccountIDOrLogin; } const detailsByAccountID = allPersonalDetails?.[accountID]; - // It's possible for displayName to be empty string, so we must fallback to login or defaultDisplayName. + // It's possible for displayName to be empty string, so we must use "||" to fallback to login or defaultDisplayName. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return detailsByAccountID?.displayName || detailsByAccountID?.login || defaultDisplayName; } @@ -89,7 +90,7 @@ function getDisplayNameForTypingIndicator(userAccountIDOrLogin: string, defaultD * so we return empty strings instead. */ function extractFirstAndLastNameFromAvailableDetails({login, displayName, firstName, lastName}: PersonalDetails): FirstAndLastName { - // It's possible for firstName to be empty string, so we must consider lastName instead. + // It's possible for firstName to be empty string, so we must use "||" to consider lastName instead. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (firstName || lastName) { return {firstName: firstName ?? '', lastName: lastName ?? ''};