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,