diff --git a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx index a12d0fcd6..ad15c85cb 100644 --- a/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx +++ b/apps/browser-extension-wallet/src/components/DropdownMenu/DropdownMenu.tsx @@ -3,7 +3,6 @@ import cn from 'classnames'; import { Dropdown } from 'antd'; import { Button, addEllipsis } from '@lace/common'; import { DropdownMenuOverlay } from '../MainMenu'; - import ChevronNormal from '../../assets/icons/chevron-down.component.svg'; import ChevronSmall from '../../assets/icons/chevron-down-small.component.svg'; import styles from './DropdownMenu.module.scss'; @@ -12,8 +11,7 @@ import { UserAvatar } from '../MainMenu/DropdownMenuOverlay/components'; import { useAnalyticsContext } from '@providers'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; import { ProfileDropdown } from '@lace/ui'; -import { useGetHandles } from '@hooks'; -import { getAssetImageUrl } from '@src/utils/get-asset-image-url'; +import { useWalletAvatar } from '@hooks'; import { getActiveWalletSubtitle } from '@src/utils/get-wallet-subtitle'; import { getUiWalletType } from '@src/utils/get-ui-wallet-type'; @@ -28,9 +26,9 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement walletUI: { isDropdownMenuOpen = false }, setIsDropdownMenuOpen } = useWalletStore(); - const [handle] = useGetHandles(); - const handleImage = handle?.profilePic; + const Chevron = isPopup ? ChevronSmall : ChevronNormal; + const { activeWalletAvatar } = useWalletAvatar(); const sendAnalyticsEvent = (event: PostHogAction) => { analytics.sendEventToPostHog(event); @@ -66,10 +64,10 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement subtitle={getActiveWalletSubtitle(cardanoWallet.source.account)} active={isDropdownMenuOpen} profile={ - handleImage + activeWalletAvatar ? { fallback: walletName, - imageSrc: getAssetImageUrl(handleImage) + imageSrc: activeWalletAvatar } : undefined } @@ -85,7 +83,7 @@ export const DropdownMenu = ({ isPopup }: DropdownMenuProps): React.ReactElement data-testid="header-menu-button" > - + { - const [handle] = useGetHandles(); - const handleImage = handle?.profilePic; - - return ( -
- {handleImage ? ( - - ) : ( - {walletName[0]?.toUpperCase()} - )} -
- ); -}; +export const UserAvatar = ({ walletName, isPopup, avatar }: UserAvatarProps): React.ReactElement => ( +
+ {avatar ? ( + + ) : ( + {walletName[0]?.toUpperCase()} + )} +
+); diff --git a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx index ab0831627..ef5f056f8 100644 --- a/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx +++ b/apps/browser-extension-wallet/src/components/MainMenu/DropdownMenuOverlay/components/UserInfo.tsx @@ -8,7 +8,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; import { toast, addEllipsis, useObservable } from '@lace/common'; import { WalletStatusContainer } from '@components/WalletStatus'; import { UserAvatar } from './UserAvatar'; -import { useGetHandles, useWalletManager } from '@hooks'; +import { useGetHandles, useWalletAvatar, useWalletManager } from '@hooks'; import { useAnalyticsContext } from '@providers'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; import { ProfileDropdown } from '@lace/ui'; @@ -46,6 +46,8 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf const fullWalletName = cardanoWallet.source.wallet.metadata.name; const activeWalletName = addEllipsis(fullWalletName, WALLET_NAME_MAX_LENGTH, 0); const [handle] = useGetHandles(); + const { activeWalletAvatar, getAvatar } = useWalletAvatar(); + const handleName = handle?.nftMetadata?.name; const activeWalletId = cardanoWallet.source.wallet.walletId; @@ -80,6 +82,8 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf const renderBip32Wallet = useCallback( (wallet: AnyBip32Wallet) => { const lastActiveAccount = getLastActiveAccount(wallet); + const walletAvatar = getAvatar(wallet.walletId); + return ( ); }, - [activateWallet, getLastActiveAccount, onOpenWalletAccounts, setIsDropdownMenuOpen, analytics, t, activeWalletId] + [ + getLastActiveAccount, + getAvatar, + fullWalletName, + onOpenWalletAccounts, + activeWalletId, + analytics, + activateWallet, + setIsDropdownMenuOpen, + t + ] ); const renderWallet = useCallback( @@ -151,7 +173,7 @@ export const UserInfo = ({ onOpenWalletAccounts, avatarVisible = true }: UserInf } >
- {avatarVisible && } + {avatarVisible && }

{activeWalletName} diff --git a/apps/browser-extension-wallet/src/features/nfts/components/NftDetail.tsx b/apps/browser-extension-wallet/src/features/nfts/components/NftDetail.tsx index 83bf97482..72e94db14 100644 --- a/apps/browser-extension-wallet/src/features/nfts/components/NftDetail.tsx +++ b/apps/browser-extension-wallet/src/features/nfts/components/NftDetail.tsx @@ -35,7 +35,8 @@ export const NftDetail = (): React.ReactElement => { const nftDetailTranslation = { tokenInformation: t('core.nftDetail.tokenInformation'), - attributes: t('core.nftDetail.attributes') + attributes: t('core.nftDetail.attributes'), + setAsAvatar: t('core.nftDetail.setAsAvatar') }; const handleOpenSend = () => { diff --git a/apps/browser-extension-wallet/src/hooks/index.ts b/apps/browser-extension-wallet/src/hooks/index.ts index fccba25c0..deb0270bc 100644 --- a/apps/browser-extension-wallet/src/hooks/index.ts +++ b/apps/browser-extension-wallet/src/hooks/index.ts @@ -22,3 +22,4 @@ export * from './useUpdateAddressStatus'; export * from './useOnAddressSave'; export * from './useAppInit'; export * from './useChainHistoryProvider'; +export * from './useWalletAvatar'; diff --git a/apps/browser-extension-wallet/src/hooks/useWalletAvatar.ts b/apps/browser-extension-wallet/src/hooks/useWalletAvatar.ts new file mode 100644 index 000000000..dcf94055e --- /dev/null +++ b/apps/browser-extension-wallet/src/hooks/useWalletAvatar.ts @@ -0,0 +1,37 @@ +import { useLocalStorage } from '@hooks/useLocalStorage'; +import { getAssetImageUrl } from '@utils/get-asset-image-url'; +import { useWalletStore } from '@stores'; +import { useGetHandles } from '@hooks/useGetHandles'; +import { useCallback } from 'react'; + +interface UseWalletAvatar { + activeWalletAvatar: string; + setAvatar: (image: string) => void; + getAvatar: (walletId: string) => string; +} + +export const useWalletAvatar = (): UseWalletAvatar => { + const { cardanoWallet, environmentName } = useWalletStore(); + const [handle] = useGetHandles(); + const [avatars, { updateLocalStorage: setUserAvatar }] = useLocalStorage('userAvatar'); + + const activeWalletId = cardanoWallet.source.wallet.walletId; + const handleImage = handle?.profilePic; + const activeWalletAvatar = + (environmentName && avatars?.[`${environmentName}${activeWalletId}`]) || + (handleImage && getAssetImageUrl(handleImage)); + + const setAvatar = useCallback( + (image: string) => { + setUserAvatar({ ...avatars, [`${environmentName}${activeWalletId}`]: image }); + }, + [setUserAvatar, avatars, environmentName, activeWalletId] + ); + + const getAvatar = useCallback( + (walletId: string) => avatars?.[`${environmentName}${walletId}`], + [avatars, environmentName] + ); + + return { activeWalletAvatar, setAvatar, getAvatar }; +}; diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index ce7bf838d..2b7fd4f39 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1230,6 +1230,8 @@ "core.nftDetail.tokenInformation": "Token Information", "core.nftDetail.attributes": "Attributes", "core.nftDetail.sendNFT": "Send NFT", + "core.nftDetail.setAsAvatar": "Set as your wallet avatar", + "core.nftDetail.avatarUpdated": "User avatar updated", "core.outputSummaryList.recipientAddress": "Recipient address", "core.outputSummaryList.sending": "Sending", "core.outputSummaryList.txFee": "Transaction fee", diff --git a/apps/browser-extension-wallet/src/types/local-storage.ts b/apps/browser-extension-wallet/src/types/local-storage.ts index 79ed38b1d..c513d2cd4 100644 --- a/apps/browser-extension-wallet/src/types/local-storage.ts +++ b/apps/browser-extension-wallet/src/types/local-storage.ts @@ -3,6 +3,7 @@ import { EnhancedAnalyticsOptInStatus, TxCreationType } from '../providers/Analy import { StakingBrowserPreferences } from '@lace/staking'; import { currencyCode } from '@providers/currency/constants'; import { ADASymbols } from '@src/utils/constants'; +import { EnvironmentTypes } from '@stores'; export interface WalletStorage { name: string; @@ -57,4 +58,5 @@ export interface ILocalStorage { stakingBrowserPreferences: StakingBrowserPreferences; showPinExtension?: boolean; showMultiAddressModal?: boolean; + userAvatar?: Record<`${EnvironmentTypes}${string}`, string>; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/nfts/components/DetailsDrawer.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/nfts/components/DetailsDrawer.tsx index 1de736614..356c8611c 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/nfts/components/DetailsDrawer.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/nfts/components/DetailsDrawer.tsx @@ -4,12 +4,14 @@ import { useTranslation } from 'react-i18next'; import { NftDetail } from '@lace/core'; import { Wallet } from '@lace/cardano'; import isNil from 'lodash/isNil'; -import { Button, Drawer, DrawerHeader, DrawerNavigation } from '@lace/common'; +import { Button, Drawer, DrawerHeader, DrawerNavigation, PostHogAction, toast } from '@lace/common'; import { buttonIds } from '@hooks/useEnterKeyPress'; import { NFT } from '@src/utils/get-token-list'; import { nftDetailSelector, nftNameSelector } from '../selectors'; import styles from './DetailsDrawer.module.scss'; import { useWalletStore } from '@stores'; +import { useWalletAvatar } from '@hooks'; +import { useAnalyticsContext } from '@providers'; interface GeneralSettingsDrawerProps { onClose: () => void; @@ -30,14 +32,24 @@ export const DetailsDrawer = ({ () => (isNil(assetsInfo) ? undefined : assetsInfo.get(selectedNft?.assetId)), [selectedNft, assetsInfo] ); + const { setAvatar } = useWalletAvatar(); + const analytics = useAnalyticsContext(); + const nftDetailTranslation = { tokenInformation: t('core.nftDetail.tokenInformation'), - attributes: t('core.nftDetail.attributes') + attributes: t('core.nftDetail.attributes'), + setAsAvatar: t('core.nftDetail.setAsAvatar') + }; + + const handleSetAsAvatar = (image: string) => { + setAvatar(image); + toast.notify({ text: t('core.nftDetail.avatarUpdated') }); + void analytics.sendEventToPostHog(PostHogAction.NFTDetailSetAsAvatarClick); }; return ( : undefined} navigation={ @@ -58,6 +70,7 @@ export const DetailsDrawer = ({ {...nftDetailSelector(assetInfo)} amount={selectedNft.amount} translations={nftDetailTranslation} + onSetAsAvatar={handleSetAsAvatar} />

)} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/nfts/selectors.ts b/apps/browser-extension-wallet/src/views/browser-view/features/nfts/selectors.ts index 34c855542..544b34248 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/nfts/selectors.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/nfts/selectors.ts @@ -48,7 +48,8 @@ export const nftDetailSelector = (asset: AssetOrHandleInfo): NftDetailProps => { attributes: otherProperties ? nftAttributesSelector(otherProperties) : undefined, translations: { tokenInformation: i18n.t('core.nftDetail.tokenInformation'), - attributes: i18n.t('core.nftDetail.attributes') + attributes: i18n.t('core.nftDetail.attributes'), + setAsAvatar: i18n.t('core.nftDetail.setAsAvatar') } }; }; diff --git a/packages/common/src/analytics/types.ts b/packages/common/src/analytics/types.ts index d20a6ba0c..abf9901aa 100644 --- a/packages/common/src/analytics/types.ts +++ b/packages/common/src/analytics/types.ts @@ -148,6 +148,7 @@ export enum PostHogAction { NFTsCreateFolderClick = 'nft | nfts | create folder | click', NFTCreateFolderNameYourFolderNextClick = 'nft | create folder | name your folder | next | click', NFTCreateFolderSelectNftsNextClick = 'nft | create folder | select nfts | next | click', + NFTDetailSetAsAvatarClick = 'nft | nft detail | set as your wallet avatar | click', // Address book AddressBookAddAddressClick = 'address book | add address | click', AddressBookAddNewAddressSaveAddressClick = 'address book | add new address | save address | click', diff --git a/packages/core/src/ui/assets/icons/profile-icon.component.svg b/packages/core/src/ui/assets/icons/profile-icon.component.svg new file mode 100644 index 000000000..b4e748782 --- /dev/null +++ b/packages/core/src/ui/assets/icons/profile-icon.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/components/Nft/NftDetail.tsx b/packages/core/src/ui/components/Nft/NftDetail.tsx index aef67c087..49fed3d6e 100644 --- a/packages/core/src/ui/components/Nft/NftDetail.tsx +++ b/packages/core/src/ui/components/Nft/NftDetail.tsx @@ -3,6 +3,8 @@ import React, { ReactNode } from 'react'; import styles from './NftDetail.module.scss'; import { NftImage } from './NftImage'; import { TranslationsFor } from '@ui/utils/types'; +import { ControlButton } from '@lace/ui'; +import { ReactComponent as ProfileIcon } from '../../assets/icons/profile-icon.component.svg'; export interface NftDetailProps { title?: ReactNode; @@ -10,7 +12,8 @@ export interface NftDetailProps { tokenInformation: LabeledInfo[]; attributes?: string; amount?: number | string; - translations: TranslationsFor<'tokenInformation' | 'attributes'>; + translations: TranslationsFor<'tokenInformation' | 'attributes' | 'setAsAvatar'>; + onSetAsAvatar?: (image: string) => void; } const JSON_INDENTATION = 2; @@ -29,7 +32,8 @@ export const NftDetail = ({ tokenInformation, attributes, amount, - translations + translations, + onSetAsAvatar }: NftDetailProps): React.ReactElement => (
{title} @@ -43,6 +47,12 @@ export const NftDetail = ({
+ } + onClick={() => onSetAsAvatar(image)} + />

{translations.tokenInformation}