diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg
index 2bccc8f87fb6..d1fdf1932f04 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg
index 5c0c58c02339..6b01de919f6c 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg
index a096c25bdaa8..25fff84822fd 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg
index 8cb002c436ee..995325846baa 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg
index ca3e8d4126d9..9b3195d662c9 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg
index 68238f454517..61c3d550325e 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg
index 63c2d988fd6a..ab63f0f4a70c 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg b/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg
index a981bc14414e..ea89e02a0c25 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg b/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg
index 6147d867fc21..6d787f0947a4 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg b/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg
index 21510d5a4fb6..bc1172e497e7 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg
index 9b4111e88c92..05a5350198b5 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg
index 249b9d1b272e..78727a3ab81a 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg
index 6f627a833b08..5953e4527cc6 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg
index 233fe74a2719..1ffceea3fa3a 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg
index 36b6df6dbd6a..8e0028477bf7 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg
index 8e4fbde94bd7..ec7d8466b0ab 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg b/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg
index 62103049b21c..bf32a8369206 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg b/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg
index 5fce2902e9df..e6c36b47319f 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg b/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg
index d55e12c94e64..f302095f2c8e 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg b/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg
index c7e96b8de37b..a8a0462bca23 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg b/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg
index 401186840f85..1928a77fff6b 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg
index 9ceaaeee0274..afebe179ea51 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg b/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg
index 5c0a292e3676..d04e9ed889b8 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg b/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg
index 85b82eda71b7..f80679fbfdca 100644
--- a/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg
+++ b/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx
index bdaeadd70ed9..68097f74bcee 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.tsx
+++ b/src/components/AvatarCropModal/AvatarCropModal.tsx
@@ -41,9 +41,6 @@ type AvatarCropModalProps = {
/** Callback to be called when user closes the modal */
onClose?: () => void;
- /** Callback to be called when user presses the back button */
- onBackButtonPress?: () => void;
-
/** Callback to be called when user saves the image */
onSave?: (newImage: File | CustomRNImageManipulatorResult) => void;
@@ -58,7 +55,7 @@ type AvatarCropModalProps = {
};
// This component can't be written using class since reanimated API uses hooks.
-function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, onBackButtonPress, isVisible, maskImage, buttonLabel}: AvatarCropModalProps) {
+function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, isVisible, maskImage, buttonLabel}: AvatarCropModalProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -374,7 +371,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose
{shouldUseNarrowLayout && }
{translate('avatarCropModal.description')}
{label}
)}
+ {CUSTOM_AVATAR_CATALOG.map(({id, local}) => {
+ const isSelected = selectedID === id;
+
+ return (
+ onSelect(id)}
+ style={[styles.avatarSelectorWrapper, isSelected && {borderColor: theme.success, borderWidth: 2}]}
+ >
+
+
+ );
+ })}
{avatarList.map(({id, StyledLetterAvatar}) => {
const isSelected = selectedID === id;
diff --git a/src/hooks/useAvatarMenu.ts b/src/hooks/useAvatarMenu.ts
index d29162cb3ab9..fa53973385c0 100644
--- a/src/hooks/useAvatarMenu.ts
+++ b/src/hooks/useAvatarMenu.ts
@@ -46,25 +46,22 @@ function useAvatarMenu({isUsingDefaultAvatar, isAvatarSelected, accountID, onIma
value: null,
},
];
- // If current avatar is a default avatar and for no avatar is selected in the form, only show upload option
- if (isUsingDefaultAvatar && !isAvatarSelected) {
- return menuItems;
- }
- menuItems.push({
- icon: Expensicons.Trashcan,
- text: translate('avatarWithImagePicker.removePhoto'),
- value: null,
- onSelected: () => {
- clearError();
- onImageRemoved();
- },
- });
- // If an avatar is selected in the form do NOT show view photo
- if (isAvatarSelected) {
+ // If current avatar is a default avatar and for avatar is selected in the form, only show upload option
+ if (isUsingDefaultAvatar || isAvatarSelected) {
return menuItems;
}
+
return [
...menuItems,
+ {
+ icon: Expensicons.Trashcan,
+ text: translate('avatarWithImagePicker.removePhoto'),
+ value: null,
+ onSelected: () => {
+ clearError();
+ onImageRemoved();
+ },
+ },
{
value: null,
icon: Expensicons.Eye,
diff --git a/src/libs/API/parameters/UpdateUserAvatarParams.ts b/src/libs/API/parameters/UpdateUserAvatarParams.ts
index 2dce38e8763c..e487f39f0414 100644
--- a/src/libs/API/parameters/UpdateUserAvatarParams.ts
+++ b/src/libs/API/parameters/UpdateUserAvatarParams.ts
@@ -1,7 +1,8 @@
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
type UpdateUserAvatarParams = {
- file: File | CustomRNImageManipulatorResult;
+ file?: File | CustomRNImageManipulatorResult;
+ customExpensifyAvatarID?: string;
};
export default UpdateUserAvatarParams;
diff --git a/src/libs/Avatars/CustomAvatarCatalog.ts b/src/libs/Avatars/CustomAvatarCatalog.ts
index 817baf180deb..bff17cf6e057 100644
--- a/src/libs/Avatars/CustomAvatarCatalog.ts
+++ b/src/libs/Avatars/CustomAvatarCatalog.ts
@@ -86,7 +86,7 @@ const SEASON_F1: Record = {
'wrenches-pink600': {local: SeasonF1.WrenchesPink600, url: `${CDN_SEASON_F1}/wrenches-pink600.png`},
};
-const LETTER_DEFAULTS: Record = {
+const LETTER_DEFAULTS: Record> = {
'letter-default-avatar_0': {local: LetterDefaultAvatars.Workspace0},
'letter-default-avatar_1': {local: LetterDefaultAvatars.Workspace1},
'letter-default-avatar_2': {local: LetterDefaultAvatars.Workspace2},
@@ -217,4 +217,13 @@ const CUSTOM_AVATAR_CATALOG = buildOrderedAvatars();
const getAvatarLocal = (id: CustomAvatarID) => ALL_CUSTOM_AVATARS[id].local;
const getAvatarURL = (id: CustomAvatarID) => ALL_CUSTOM_AVATARS[id].url;
-export {ALL_CUSTOM_AVATARS, CUSTOM_AVATAR_CATALOG, LETTER_AVATAR_COLOR_OPTIONS, LETTER_DEFAULTS, getAvatarLocal, getAvatarURL, getLetterAvatar};
+/**
+ * Type guard to check if a value is a valid CustomAvatarID
+ * @param value - The value to check
+ * @returns True if the value is a valid CustomAvatarID
+ */
+function isCustomAvatarID(value: unknown): value is CustomAvatarID {
+ return typeof value === 'string' && value in ALL_CUSTOM_AVATARS;
+}
+
+export {ALL_CUSTOM_AVATARS, CUSTOM_AVATAR_CATALOG, LETTER_AVATAR_COLOR_OPTIONS, LETTER_DEFAULTS, getAvatarLocal, getAvatarURL, getLetterAvatar, isCustomAvatarID};
diff --git a/src/libs/Avatars/CustomAvatarCatalog.types.ts b/src/libs/Avatars/CustomAvatarCatalog.types.ts
index b93d5a719ded..ef8571dec2c7 100644
--- a/src/libs/Avatars/CustomAvatarCatalog.types.ts
+++ b/src/libs/Avatars/CustomAvatarCatalog.types.ts
@@ -91,7 +91,7 @@ type LetterAvatarIDs =
| 'letter-default-avatar_z';
type LetterAvatarColorStyle = {backgroundColor: string; fillColor: string};
-type AvatarEntry = {local: React.FC; url?: string};
+type AvatarEntry = {local: React.FC; url: string};
type CustomAvatarID = DefaultAvatarIDs | SeasonF1AvatarIDs;
type LetterAvatarVariant = {
diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts
index 0b3462d7c6bf..9a52f1d65f13 100644
--- a/src/libs/UserUtils.ts
+++ b/src/libs/UserUtils.ts
@@ -8,6 +8,8 @@ import type {LoginList, PrivatePersonalDetails, VacationDelegate} from '@src/typ
import type Login from '@src/types/onyx/Login';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
+import {ALL_CUSTOM_AVATARS, getAvatarLocal} from './Avatars/CustomAvatarCatalog';
+import type {CustomAvatarID} from './Avatars/CustomAvatarCatalog.types';
import hashCode from './hashCode';
import {formatPhoneNumber} from './LocalePhoneNumber';
import {translateLocal} from './Localize';
@@ -133,7 +135,7 @@ function getAccountIDHashBucket(accountID = -1, avatarURL?: string) {
/**
* Helper method to return the default avatar associated with the given accountID
*/
-function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset | undefined {
+function getDefaultAvatar(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): IconAsset | undefined {
if (accountID === CONST.ACCOUNT_ID.CONCIERGE) {
return ConciergeAvatar;
}
@@ -149,18 +151,42 @@ function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset | undef
return defaultAvatars[`Avatar${accountIDHashBucket}`];
}
+/**
+ * Helper method to return default avatar name associated with the accountID
+ */
+function getDefaultAvatarName(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): string {
+ const accountIDHashBucket = getAccountIDHashBucket(accountID, avatarURL);
+ const avatarPrefix = `default-avatar`;
+
+ return `${avatarPrefix}_${accountIDHashBucket}`;
+}
+
/**
* Helper method to return default avatar URL associated with the accountID
*/
-function getDefaultAvatarURL(accountID: string | number = '', avatarURL?: string): string {
+function getDefaultAvatarURL(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): string {
if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) {
return CONST.CONCIERGE_ICON_URL;
}
- const accountIDHashBucket = getAccountIDHashBucket(Number(accountID) || -1, avatarURL);
- const avatarPrefix = `default-avatar`;
+ return `${CONST.CLOUDFRONT_URL}/images/avatars/${getDefaultAvatarName(accountID, avatarURL)}.png`;
+}
- return `${CONST.CLOUDFRONT_URL}/images/avatars/${avatarPrefix}_${accountIDHashBucket}.png`;
+/**
+ * Helper method to extract the avatar name from a default avatar URL
+ * @param avatarURL - the URL returned by getDefaultAvatarURL
+ * @returns the avatar name (e.g., 'default-avatar_5', 'concierge') or undefined if not a valid default avatar URL
+ */
+function getDefaultAvatarNameFromURL(avatarURL?: AvatarSource): string | undefined {
+ if (!avatarURL || typeof avatarURL !== 'string' || avatarURL === CONST.CONCIERGE_ICON_URL) {
+ return undefined;
+ }
+
+ // Extract avatar name from CloudFront URL and make sure it's one of defaults
+ const match = avatarURL.split('/').at(-1)?.split('.')?.[0] ?? '';
+ if (ALL_CUSTOM_AVATARS[match as CustomAvatarID]) {
+ return match;
+ }
}
/**
@@ -225,6 +251,10 @@ function getSmallSizeAvatar(avatarSource?: AvatarSource, accountID?: number): Av
if (typeof source !== 'string') {
return source;
}
+ const maybeDefaultAvatarName = getDefaultAvatarNameFromURL(avatarSource);
+ if (maybeDefaultAvatarName) {
+ return getAvatarLocal(maybeDefaultAvatarName as CustomAvatarID);
+ }
// Because other urls than CloudFront do not support dynamic image sizing (_SIZE suffix), the current source is already what we want to use here.
if (!CONST.CLOUDFRONT_DOMAIN_REGEX.test(source)) {
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 1556f0060d5e..9751e46a28c1 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -357,10 +357,22 @@ function openPublicProfilePage(accountID: number) {
API.read(READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE, parameters, {optimisticData, successData, failureData});
}
+type DefaultAvatarResult = {uri: string; name: string; customExpensifyAvatarID: string};
+
+/**
+ * Type guard to check if a file object is a DefaultAvatarResult
+ */
+function isDefaultAvatarResult(file: File | CustomRNImageManipulatorResult | DefaultAvatarResult): file is DefaultAvatarResult {
+ return 'customExpensifyAvatarID' in file && typeof file.customExpensifyAvatarID === 'string';
+}
+
/**
* Updates the user's avatar image
*/
-function updateAvatar(file: File | CustomRNImageManipulatorResult, currentUserPersonalDetails: Pick) {
+function updateAvatar(
+ file: File | CustomRNImageManipulatorResult | DefaultAvatarResult,
+ currentUserPersonalDetails: Pick,
+) {
if (!currentUserPersonalDetails.accountID) {
return;
}
@@ -415,7 +427,7 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult, currentUserPe
},
];
- const parameters: UpdateUserAvatarParams = {file};
+ const parameters: UpdateUserAvatarParams = isDefaultAvatarResult(file) ? {customExpensifyAvatarID: file.customExpensifyAvatarID} : {file};
API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData});
}
diff --git a/src/pages/settings/Profile/Avatar/AvatarPage.tsx b/src/pages/settings/Profile/Avatar/AvatarPage.tsx
index efa9f2d3c711..7ff7798d8745 100644
--- a/src/pages/settings/Profile/Avatar/AvatarPage.tsx
+++ b/src/pages/settings/Profile/Avatar/AvatarPage.tsx
@@ -17,9 +17,11 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useLetterAvatars from '@hooks/useLetterAvatars';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getAvatarLocal, getAvatarURL, isCustomAvatarID} from '@libs/Avatars/CustomAvatarCatalog';
import {validateAvatarImage} from '@libs/AvatarUtils';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import Navigation from '@libs/Navigation/Navigation';
+import type {AvatarSource} from '@libs/UserUtils';
import {isDefaultAvatar} from '@libs/UserUtils';
import DiscardChangesConfirmation from '@pages/iou/request/step/DiscardChangesConfirmation';
import {deleteAvatar, updateAvatar} from '@userActions/PersonalDetails';
@@ -29,8 +31,6 @@ import type {FileObject} from '@src/types/utils/Attachment';
import AvatarCapture from './AvatarCapture';
import type {AvatarCaptureHandle} from './AvatarCapture/types';
-const EMPTY_FILE = {uri: '', name: '', type: '', file: null};
-
type ImageData = {
uri: string;
name: string;
@@ -43,16 +43,23 @@ type ErrorData = {
phraseParam: Record;
};
+const EMPTY_FILE = {uri: '', name: '', type: '', file: null};
+
function ProfileAvatar() {
const [errorData, setErrorData] = useState({validationError: null, phraseParam: {}});
const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false);
const [selected, setSelected] = useState();
const avatarCaptureRef = useRef(null);
+ const isSavingRef = useRef(false);
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const [cropImageData, setCropImageData] = useState({...EMPTY_FILE});
const [imageData, setImageData] = useState({...EMPTY_FILE});
+
+ const isDirty = imageData.uri !== '' || !!selected;
+
const avatarStyle = [styles.avatarXLarge, styles.alignSelfStart, styles.alignSelfCenter];
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
@@ -60,9 +67,17 @@ function ProfileAvatar() {
const accountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID;
// eslint-disable-next-line no-nested-ternary
- const avatarURL = selected ? avatars[selected] : imageData.uri !== '' ? imageData.uri : (currentUserPersonalDetails?.avatar ?? '');
+ let avatarURL: AvatarSource = '';
+ if (selected && isCustomAvatarID(selected)) {
+ avatarURL = getAvatarLocal(selected);
+ } else if (selected) {
+ avatarURL = avatars[selected];
+ } else if (imageData.uri) {
+ avatarURL = imageData.uri;
+ } else {
+ avatarURL = currentUserPersonalDetails?.avatar ?? '';
+ }
const isUsingDefaultAvatar = isDefaultAvatar(currentUserPersonalDetails?.avatar ?? '');
- const isDirty = !!imageData.uri || !!selected;
const setError = (error: TranslationPaths | null, phraseParam: Record) => {
setErrorData({
@@ -84,7 +99,7 @@ function ProfileAvatar() {
setIsAvatarCropModalOpen(true);
setError(null, {});
- setImageData({
+ setCropImageData({
uri: image.uri ?? '',
name: image.name ?? '',
type: image.type ?? '',
@@ -99,8 +114,8 @@ function ProfileAvatar() {
const onImageSelected = useCallback((file: File | CustomRNImageManipulatorResult) => {
setSelected(undefined);
setImageData({
- uri: file.uri ?? '',
- name: file.name,
+ uri: file?.uri ?? '',
+ name: file?.name,
file,
type: '',
});
@@ -137,6 +152,8 @@ function ProfileAvatar() {
});
const onPress = useCallback(() => {
+ isSavingRef.current = true;
+
if (imageData.file) {
updateAvatar(imageData.file, {
avatar: currentUserPersonalDetails?.avatar,
@@ -145,25 +162,43 @@ function ProfileAvatar() {
});
setImageData({...EMPTY_FILE});
Navigation.dismissModal();
+ isSavingRef.current = false;
+ return;
+ }
+
+ if (selected && isCustomAvatarID(selected)) {
+ updateAvatar(
+ {
+ uri: getAvatarURL(selected),
+ name: selected,
+ customExpensifyAvatarID: selected,
+ },
+ {
+ avatar: currentUserPersonalDetails?.avatar,
+ avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail,
+ accountID: currentUserPersonalDetails?.accountID,
+ },
+ );
+ setSelected(undefined);
+ Navigation.dismissModal();
+ isSavingRef.current = false;
return;
}
if (!selected || !avatarCaptureRef.current) {
+ isSavingRef.current = false;
return;
}
// User selected a letter avatar
- avatarCaptureRef.current
- .capture()
- ?.then((file) => {
- updateAvatar(file, {
- avatar: currentUserPersonalDetails?.avatar,
- avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail,
- accountID: currentUserPersonalDetails?.accountID,
- });
- setSelected(undefined);
- })
- .then(() => {
- Navigation.dismissModal();
+ avatarCaptureRef.current.capture()?.then((file) => {
+ updateAvatar(file, {
+ avatar: currentUserPersonalDetails?.avatar,
+ avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail,
+ accountID: currentUserPersonalDetails?.accountID,
});
+ setSelected(undefined);
+ Navigation.dismissModal();
+ isSavingRef.current = false;
+ });
}, [currentUserPersonalDetails?.accountID, currentUserPersonalDetails?.avatar, currentUserPersonalDetails?.avatarThumbnail, imageData.file, selected]);
return (
@@ -235,7 +270,7 @@ function ProfileAvatar() {
contentContainerStyle={styles.flexGrow1}
keyboardShouldPersistTaps="handled"
>
-
+
-
+
- isDirty} />
{
- if (!isAvatarCropModalOpen) {
- return;
- }
- setImageData({...EMPTY_FILE});
- setIsAvatarCropModalOpen(false);
- }}
onClose={() => {
if (!isAvatarCropModalOpen) {
return;
}
+ setCropImageData({...EMPTY_FILE});
setIsAvatarCropModalOpen(false);
}}
isVisible={isAvatarCropModalOpen}
onSave={onImageSelected}
- imageUri={imageData.uri}
- imageName={imageData.name}
- imageType={imageData.type}
+ imageUri={cropImageData.uri}
+ imageName={cropImageData.name}
+ imageType={cropImageData.type}
buttonLabel={translate('avatarPage.upload')}
/>
+ !isSavingRef.current && isDirty} />
);
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 54feac374d6c..83f1c2fe187c 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5222,7 +5222,8 @@ const staticStyles = (theme: ThemeColors) =>
avatarSelectorListContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
- gap: 2,
+ gap: 0,
+ justifyContent: 'space-between',
},
avatarSelected: {borderColor: theme.success, borderWidth: 2},
diff --git a/tests/actions/PersonalDetailsTest.ts b/tests/actions/PersonalDetailsTest.ts
index 262c3a714305..9b5b152c396c 100644
--- a/tests/actions/PersonalDetailsTest.ts
+++ b/tests/actions/PersonalDetailsTest.ts
@@ -570,6 +570,81 @@ describe('actions/PersonalDetails', () => {
);
});
+ it('should call API.write with correct parameters and optimistic data for DefaultAvatarResult', async () => {
+ const mockFile = {
+ uri: 'file://test-avatar.jpg',
+ name: 'test-avatar.jpg',
+ customExpensifyAvatarID: 'default-avatar_7',
+ };
+ const currentUserPersonalDetail: Pick = {
+ avatar: 'old-avatar.jpg',
+ avatarThumbnail: 'old-avatar-thumb.jpg',
+ accountID: 123,
+ };
+
+ PersonalDetailsActions.updateAvatar(mockFile, currentUserPersonalDetail);
+ await waitForBatchedUpdates();
+
+ expect(mockAPI.write).toHaveBeenCalledWith(
+ WRITE_COMMANDS.UPDATE_USER_AVATAR,
+ {customExpensifyAvatarID: mockFile.customExpensifyAvatarID},
+ {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 123: {
+ avatar: mockFile.uri,
+ avatarThumbnail: mockFile.uri,
+ originalFileName: mockFile.name,
+ errorFields: {
+ avatar: null,
+ },
+ pendingFields: {
+ avatar: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ originalFileName: null,
+ },
+ fallbackIcon: mockFile.uri,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 123: {
+ pendingFields: {
+ avatar: null,
+ },
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 123: {
+ avatar: currentUserPersonalDetail.avatar,
+ avatarThumbnail: currentUserPersonalDetail.avatarThumbnail ?? currentUserPersonalDetail.avatar,
+ pendingFields: {
+ avatar: null,
+ },
+ },
+ },
+ },
+ ],
+ },
+ );
+ });
+
it('should handle null avatarThumbnail in failure data', async () => {
const mockFile = {
uri: 'file://test-avatar.jpg',
diff --git a/tests/ui/AvatarSelector.test.tsx b/tests/ui/AvatarSelector.test.tsx
index 4a25d23bfb96..f31e785ff872 100644
--- a/tests/ui/AvatarSelector.test.tsx
+++ b/tests/ui/AvatarSelector.test.tsx
@@ -73,8 +73,7 @@ describe('AvatarSelector', () => {
});
});
- // TODO uncomment when we add ALL_CUSTOM_AVATARS https://github.com/Expensify/App/pull/72542
- xdescribe('CUSTOM_AVATAR_CATALOG avatars', () => {
+ describe('CUSTOM_AVATAR_CATALOG avatars', () => {
it('renders all avatars from custom catalog', async () => {
renderAvatarSelector();
await waitForBatchedUpdates();
@@ -181,8 +180,7 @@ describe('AvatarSelector', () => {
expect(selectedAvatar).toBeOnTheScreen();
});
- // TODO uncomment when we add ALL_CUSTOM_AVATARS https://github.com/Expensify/App/pull/72542
- xit('renders both custom and letter avatars when firstName is provided', async () => {
+ it('renders both custom and letter avatars when firstName is provided', async () => {
renderAvatarSelector({name: mockName});
await waitForBatchedUpdates();