diff --git a/examples/homepage/src/app/components/features/FeaturesToTry/FeaturesToTry.tsx b/examples/homepage/src/app/components/features/FeaturesToTry/FeaturesToTry.tsx
index aff2894..ebb0037 100644
--- a/examples/homepage/src/app/components/features/FeaturesToTry/FeaturesToTry.tsx
+++ b/examples/homepage/src/app/components/features/FeaturesToTry/FeaturesToTry.tsx
@@ -16,18 +16,22 @@ import {
useEmbeddedWalletSettingsModal,
useExploreEcosystemModal,
useNotificationsModal,
+ useAccountCustomizationModal,
useFAQModal,
} from '@vechain/vechain-kit';
import { FeatureCard } from './FeatureCard';
import { GithubCard } from './GithubCard';
import { LanguageCard } from './LanguageCard';
import { ThemeCard } from './ThemeCard';
+import { CgProfile } from 'react-icons/cg';
export function FeaturesToTry() {
const { account } = useWallet();
// Use the modal hooks
const { open: openChooseNameModal } = useChooseNameModal();
+ const { open: openAccountCustomizationModal } =
+ useAccountCustomizationModal();
const { open: openSendTokenModal } = useSendTokenModal();
const { open: openEmbeddedWalletSettingsModal } =
useEmbeddedWalletSettingsModal();
@@ -45,6 +49,15 @@ export function FeaturesToTry() {
link: '#',
content: openChooseNameModal,
},
+ {
+ title: 'Set Profile Image',
+ description:
+ 'Customize your account with a profile image to enhance your identity across VeChain applications.',
+ icon: CgProfile,
+ highlight: !account?.domain,
+ link: '#',
+ content: openAccountCustomizationModal,
+ },
{
title: 'Transfer Assets',
description:
@@ -89,7 +102,7 @@ export function FeaturesToTry() {
return (
- Features (click to try!)
+ Features
@@ -97,8 +110,8 @@ export function FeaturesToTry() {
))}
-
+
);
diff --git a/examples/homepage/src/app/pages/Home.tsx b/examples/homepage/src/app/pages/Home.tsx
index aa94e7b..323c088 100644
--- a/examples/homepage/src/app/pages/Home.tsx
+++ b/examples/homepage/src/app/pages/Home.tsx
@@ -234,7 +234,15 @@ const Logo = () => {
transition: 'opacity 0.2s ease-in-out',
}}
>
-
+
+
+ Powered by
+
+
+
);
};
diff --git a/packages/vechain-kit/package.json b/packages/vechain-kit/package.json
index e410df5..fc077ab 100644
--- a/packages/vechain-kit/package.json
+++ b/packages/vechain-kit/package.json
@@ -38,6 +38,7 @@
"@vechain/vebetterdao-contracts": "^4.1.0",
"@wagmi/core": "^2.13.4",
"bignumber.js": "^9.1.2",
+ "browser-image-compression": "^2.0.2",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.4.7",
diff --git a/packages/vechain-kit/src/components/AccountModal/AccountModal.tsx b/packages/vechain-kit/src/components/AccountModal/AccountModal.tsx
index 2d91882..65846a2 100644
--- a/packages/vechain-kit/src/components/AccountModal/AccountModal.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/AccountModal.tsx
@@ -23,6 +23,7 @@ import { NotificationsContent } from './Contents/Notifications/NotificationConte
import { ExploreEcosystemContent } from './Contents/Ecosystem/ExploreEcosystemContent';
import { AppOverviewContent } from './Contents/Ecosystem/AppOverviewContent';
import { DisconnectConfirmContent } from './Contents/Account/DisconnectConfirmContent';
+import { AccountCustomizationContent } from './Contents/Account/AccountCustomizationContent';
type Props = {
isOpen: boolean;
@@ -144,6 +145,12 @@ export const AccountModal = ({
setCurrentContent={setCurrentContent}
/>
);
+ case 'account-customization':
+ return (
+
+ );
}
};
diff --git a/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx b/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
index 4703a85..c60af9c 100644
--- a/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
@@ -38,7 +38,7 @@ export const AccountSelector = ({
rounded="full"
/>
- {humanDomain(wallet?.domain ?? '', 11, 4) ||
+ {humanDomain(wallet?.domain ?? '', 6, 4) ||
humanAddress(wallet?.address ?? '', 6, 4)}
diff --git a/packages/vechain-kit/src/components/AccountModal/Components/ActionButton.tsx b/packages/vechain-kit/src/components/AccountModal/Components/ActionButton.tsx
index 9e06d0b..91c840e 100644
--- a/packages/vechain-kit/src/components/AccountModal/Components/ActionButton.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Components/ActionButton.tsx
@@ -8,16 +8,17 @@ import {
Image,
Tag,
useColorMode,
+ ButtonProps,
} from '@chakra-ui/react';
-import { ElementType } from 'react';
import { useTranslation } from 'react-i18next';
+import { IconType } from 'react-icons';
-interface ActionButtonProps {
+type ActionButtonProps = {
title: string;
description: string;
onClick: () => void;
- leftIcon?: ElementType;
- rightIcon?: ElementType;
+ leftIcon?: IconType;
+ rightIcon?: IconType;
leftImage?: string;
backgroundColor?: string;
border?: string;
@@ -26,7 +27,10 @@ interface ActionButtonProps {
showComingSoon?: boolean;
isDisabled?: boolean;
stacked?: boolean;
-}
+ isLoading?: boolean;
+ loadingText?: string;
+ style?: ButtonProps;
+};
export const ActionButton = ({
leftIcon,
@@ -41,6 +45,9 @@ export const ActionButton = ({
_hover,
isDisabled = false,
stacked = false,
+ isLoading,
+ loadingText,
+ style,
}: ActionButtonProps) => {
const { t } = useTranslation();
const { colorMode } = useColorMode();
@@ -57,13 +64,23 @@ export const ActionButton = ({
onClick={onClick}
display={hide ? 'none' : 'flex'}
isDisabled={showComingSoon || isDisabled}
+ isLoading={isLoading}
+ loadingText={loadingText}
bgColor={baseBackgroundColor}
_hover={_hover}
+ {...style}
>
{leftImage ? (
-
+
) : (
)}
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/Account/AccountCustomizationContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/Account/AccountCustomizationContent.tsx
new file mode 100644
index 0000000..6048b45
--- /dev/null
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/Account/AccountCustomizationContent.tsx
@@ -0,0 +1,208 @@
+import {
+ ModalBody,
+ ModalCloseButton,
+ ModalHeader,
+ VStack,
+ Text,
+ Button,
+} from '@chakra-ui/react';
+import { ModalBackButton, StickyHeaderContainer } from '@/components/common';
+import { AccountModalContentTypes } from '../../Types';
+import { useTranslation } from 'react-i18next';
+import { useVeChainKitConfig } from '@/providers';
+import { getAvatarQueryKey, useWallet } from '@/hooks';
+import { MdOutlineNavigateNext } from 'react-icons/md';
+import { ActionButton } from '../../Components';
+import { useSingleImageUpload } from '@/hooks/api/ipfs';
+import { useUpdateAvatarRecord } from '@/hooks';
+import { useQueryClient } from '@tanstack/react-query';
+import { useRef, useState } from 'react';
+import { uploadBlobToIPFS } from '@/utils/ipfs';
+import { FaRegAddressCard } from 'react-icons/fa';
+
+type Props = {
+ setCurrentContent: React.Dispatch<
+ React.SetStateAction
+ >;
+};
+
+export const AccountCustomizationContent = ({ setCurrentContent }: Props) => {
+ const { t } = useTranslation();
+ const { network, darkMode: isDark } = useVeChainKitConfig();
+ const { account } = useWallet();
+ const fileInputRef = useRef(null);
+ const [isUploading, setIsUploading] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [showFullText, setShowFullText] = useState(false);
+ const queryClient = useQueryClient();
+ const { onUpload } = useSingleImageUpload({
+ compressImage: true,
+ });
+
+ const { updateAvatar } = useUpdateAvatarRecord({
+ onSuccess: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: getAvatarQueryKey(account?.domain ?? ''),
+ });
+
+ await queryClient.refetchQueries({
+ queryKey: getAvatarQueryKey(account?.domain ?? ''),
+ });
+
+ setIsProcessing(false);
+ },
+ onError: () => {
+ setIsProcessing(false);
+ },
+ });
+
+ const handleImageUpload = async (
+ event: React.ChangeEvent,
+ ) => {
+ const file = event.target.files?.[0];
+ if (!file) return;
+
+ try {
+ setIsUploading(true);
+ setIsProcessing(true);
+ const uploadedImage = await onUpload(file);
+ if (!uploadedImage) throw new Error('Failed to compress image');
+
+ const ipfsHash = await uploadBlobToIPFS(
+ uploadedImage.file,
+ file.name,
+ network.type,
+ );
+ await updateAvatar(account?.domain ?? '', 'ipfs://' + ipfsHash);
+ } catch (error) {
+ console.error('Error uploading image:', error);
+ setIsProcessing(false);
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+ {t('Customize Account')}
+
+ setCurrentContent('settings')}
+ />
+
+
+
+
+
+
+
+ {t(
+ 'Customize your account with a unique .vet domain name and profile image to enhance your identity across VeChain applications.',
+ )}
+
+
+ {showFullText && (
+ <>
+
+ {t(
+ 'Your customizations are linked to your .vet domain name, making them portable across different applications.',
+ )}
+
+
+ {t(
+ 'To get started with customization, first secure your .vet domain name. Once you have a domain, you can add a profile image that will be visible wherever you use your account.',
+ )}
+
+
+ {t(
+ 'Changing your domain name will update also your profile image.',
+ )}
+
+ >
+ )}
+
+
+
+
+ {
+ if (account?.domain) {
+ setCurrentContent({
+ type: 'choose-name-search',
+ props: {
+ name: '',
+ setCurrentContent,
+ },
+ });
+ } else {
+ setCurrentContent('choose-name');
+ }
+ }}
+ leftIcon={FaRegAddressCard}
+ rightIcon={MdOutlineNavigateNext}
+ />
+
+ fileInputRef.current?.click()}
+ leftImage={account?.image}
+ isLoading={isUploading || isProcessing}
+ isDisabled={!account?.domain}
+ loadingText={
+ isUploading
+ ? t('Uploading image')
+ : t('Setting image')
+ }
+ />
+
+
+ await handleImageUpload(event)
+ }
+ />
+
+
+ >
+ );
+};
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/Account/EmbeddedWalletContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/Account/EmbeddedWalletContent.tsx
index 23d2dab..fea06a2 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/Account/EmbeddedWalletContent.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/Account/EmbeddedWalletContent.tsx
@@ -73,7 +73,7 @@ export const EmbeddedWalletContent = ({ setCurrentContent }: Props) => {
@@ -90,7 +90,7 @@ export const EmbeddedWalletContent = ({ setCurrentContent }: Props) => {
<>
{t(
- 'This is your main wallet and identity. Please be sure to keep it safe and backed up. Go to {{element}} website to manage your login methods and security settings.',
+ 'This is your main wallet, created by {{element}} and secured by Privy. This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions. Please be sure to keep it safe and backed up.',
{
element:
connectionCache?.ecosystemApp?.name,
@@ -115,6 +115,11 @@ export const EmbeddedWalletContent = ({ setCurrentContent }: Props) => {
{showFullText && (
<>
+
+ {t(
+ 'This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions.',
+ )}
+
{t(
'We highly recommend exporting your private key to back up your wallet. This ensures you can restore it if needed or transfer it to self-custody using',
@@ -147,11 +152,6 @@ export const EmbeddedWalletContent = ({ setCurrentContent }: Props) => {
'to learn more about embedded wallets.',
)}
-
- {t(
- 'A smart account is being used as a gateway for blockchain interactions.',
- )}
-
>
)}
@@ -168,6 +168,8 @@ export const EmbeddedWalletContent = ({ setCurrentContent }: Props) => {
>
)}
+ {/* TODO: Go to {{element}} website to manage your login methods and security settings. */}
+
{
leftIcon={MdManageAccounts}
rightIcon={MdOutlineNavigateNext}
/>
-
- {/* {connection.isConnectedWithSocialLogin &&
- privy?.allowPasskeyLinking && (
- {
- linkPasskey();
- }}
- leftIcon={IoIosFingerPrint}
- rightIcon={MdOutlineNavigateNext}
- />
- )} */}
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/Account/WalletSettingsContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/Account/WalletSettingsContent.tsx
index d0360e7..fb4c6e2 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/Account/WalletSettingsContent.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/Account/WalletSettingsContent.tsx
@@ -23,13 +23,13 @@ import {
} from '@/components/common';
import { useVeChainKitConfig } from '@/providers/VeChainKitProvider';
import { AccountModalContentTypes } from '../../Types';
-import { FaRegAddressCard } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { VscDebugDisconnect } from 'react-icons/vsc';
import { HiOutlineWallet } from 'react-icons/hi2';
import { useEffect, useRef } from 'react';
import { RiLogoutBoxLine } from 'react-icons/ri';
import { BsQuestionCircle } from 'react-icons/bs';
+import { GiPaintBrush } from 'react-icons/gi';
type Props = {
setCurrentContent: React.Dispatch<
@@ -49,8 +49,6 @@ export const WalletSettingsContent = ({
const { connection, disconnect, account } = useWallet();
- const hasExistingDomain = !!account?.domain;
-
const { getConnectionCache } = useCrossAppConnectionCache();
const connectionCache = getConnectionCache();
@@ -130,28 +128,14 @@ export const WalletSettingsContent = ({
/>
{
- if (hasExistingDomain) {
- setCurrentContent({
- type: 'choose-name-search',
- props: {
- name: '',
- setCurrentContent,
- },
- });
- } else {
- setCurrentContent('choose-name');
- }
+ setCurrentContent('account-customization');
}}
- leftIcon={FaRegAddressCard}
+ leftIcon={GiPaintBrush}
rightIcon={MdOutlineNavigateNext}
/>
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/Account/index.ts b/packages/vechain-kit/src/components/AccountModal/Contents/Account/index.ts
index b00c215..cca8213 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/Account/index.ts
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/Account/index.ts
@@ -1,3 +1,4 @@
export * from './AccountMainContent';
export * from './EmbeddedWalletContent';
export * from './WalletSettingsContent';
+export * from './AccountCustomizationContent';
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSearchContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSearchContent.tsx
index dc8878f..c2f70f3 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSearchContent.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSearchContent.tsx
@@ -158,7 +158,7 @@ export const ChooseNameSearchContent = ({
onClick={() =>
// if the user has a domain, go to accounts
account?.domain
- ? setCurrentContent('settings')
+ ? setCurrentContent('account-customization')
: setCurrentContent('choose-name')
}
/>
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSummaryContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSummaryContent.tsx
index e0626c0..61e128f 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSummaryContent.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/ChooseName/ChooseNameSummaryContent.tsx
@@ -121,7 +121,7 @@ export const ChooseNameSummaryContent = ({
isOpen={transactionModal.isOpen}
onClose={() => {
transactionModal.onClose();
- setCurrentContent('main');
+ setCurrentContent('account-customization');
}}
status={status}
txId={txReceipt?.meta.txID}
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/PrivyLinkedAccounts/PrivyLinkedAccounts.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/PrivyLinkedAccounts/PrivyLinkedAccounts.tsx
index f9a7b9b..df7675f 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/PrivyLinkedAccounts/PrivyLinkedAccounts.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/PrivyLinkedAccounts/PrivyLinkedAccounts.tsx
@@ -37,6 +37,7 @@ import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { useVeChainKitConfig } from '@/providers';
import { IoIosFingerPrint } from 'react-icons/io';
+import { MdOutlineNavigateNext } from 'react-icons/md';
type ConfirmUnlinkProps = {
accountType: string;
@@ -306,6 +307,19 @@ export const PrivyLinkedAccounts = ({ onBack }: PrivyLinkedAccountsProps) => {
+ {
+ linkPasskey();
+ }}
+ leftIcon={IoIosFingerPrint}
+ rightIcon={MdOutlineNavigateNext}
+ isDisabled={!privy?.allowPasskeyLinking}
+ />
+
{canLinkGoogle && (
{
const { account } = useWallet();
const [isDesktop] = useMediaQuery('(min-width: 768px)');
diff --git a/packages/vechain-kit/src/config/index.ts b/packages/vechain-kit/src/config/index.ts
index aa2b37f..f1defe6 100644
--- a/packages/vechain-kit/src/config/index.ts
+++ b/packages/vechain-kit/src/config/index.ts
@@ -34,6 +34,7 @@ export type AppConfig = {
vetDomainsPublicResolverAddress: string;
vetDomainsReverseRegistrarAddress: string;
vnsResolverAddress: string;
+ vetDomainAvatarUrl: string;
nodeUrl: string;
indexerUrl: string;
b3trIndexerUrl: string;
diff --git a/packages/vechain-kit/src/config/mainnet.ts b/packages/vechain-kit/src/config/mainnet.ts
index a8658f0..64b94b7 100644
--- a/packages/vechain-kit/src/config/mainnet.ts
+++ b/packages/vechain-kit/src/config/mainnet.ts
@@ -87,5 +87,6 @@ const config: AppConfig = {
vetDomainsReverseRegistrarAddress:
'0x5c970901a587BA3932C835D4ae5FAE2BEa7e78Bc',
vnsResolverAddress: '0xA11413086e163e41901bb81fdc5617c975Fa5a1A',
+ vetDomainAvatarUrl: 'https://vet.domains/api/avatar',
};
export default config;
diff --git a/packages/vechain-kit/src/config/solo.ts b/packages/vechain-kit/src/config/solo.ts
index 54bc47f..bfffe74 100644
--- a/packages/vechain-kit/src/config/solo.ts
+++ b/packages/vechain-kit/src/config/solo.ts
@@ -41,6 +41,7 @@ const config: AppConfig = {
vetDomainsReverseRegistrarAddress:
'0x5c970901a587BA3932C835D4ae5FAE2BEa7e78Bc',
vnsResolverAddress: '0x0000000000000000000000000000000000000000',
+ vetDomainAvatarUrl: 'https://testnet.vet.domains/api/avatar',
indexerUrl: 'https://b3tr.testnet.vechain.org/api/v1',
b3trIndexerUrl: 'https://b3tr.testnet.vechain.org/api/v1',
graphQlIndexerUrl: 'https://graph.vet/subgraphs/name/vns',
diff --git a/packages/vechain-kit/src/config/testnet.ts b/packages/vechain-kit/src/config/testnet.ts
index 96fccad..c1a5a6e 100644
--- a/packages/vechain-kit/src/config/testnet.ts
+++ b/packages/vechain-kit/src/config/testnet.ts
@@ -41,6 +41,7 @@ const config: AppConfig = {
vetDomainsReverseRegistrarAddress:
'0x6878f1aD5e3015310CfE5B38d7B7071C5D8818Ca',
vnsResolverAddress: '0xc403b8EA53F707d7d4de095f0A20bC491Cf2bc94',
+ vetDomainAvatarUrl: 'https://testnet.vet.domains/api/avatar',
indexerUrl: 'https://indexer.testnet.vechain.org/api/v1',
b3trIndexerUrl: 'https://b3tr.testnet.vechain.org/api/v1',
graphQlIndexerUrl: 'https://graph.vet/subgraphs/name/vns',
diff --git a/packages/vechain-kit/src/hooks/api/ipfs/index.ts b/packages/vechain-kit/src/hooks/api/ipfs/index.ts
index 4176e61..c4a3eae 100644
--- a/packages/vechain-kit/src/hooks/api/ipfs/index.ts
+++ b/packages/vechain-kit/src/hooks/api/ipfs/index.ts
@@ -1,3 +1,5 @@
-export * from "./useIpfsMetadata"
-export * from "./useIpfsImage"
-export * from "./useIpfsMetadatas"
+export * from './useIpfsMetadata';
+export * from './useIpfsImage';
+export * from './useIpfsMetadatas';
+export * from './useUploadImages';
+export * from './useSingleImageUpload';
diff --git a/packages/vechain-kit/src/hooks/api/ipfs/useIpfsImage.ts b/packages/vechain-kit/src/hooks/api/ipfs/useIpfsImage.ts
index a2e0824..ecee575 100644
--- a/packages/vechain-kit/src/hooks/api/ipfs/useIpfsImage.ts
+++ b/packages/vechain-kit/src/hooks/api/ipfs/useIpfsImage.ts
@@ -23,7 +23,7 @@ export const getIpfsImage = async (
): Promise => {
if (!uri) throw new Error('IPFS URI is required');
- const response = await fetch(convertUriToUrl(uri, networkType));
+ const response = await fetch(convertUriToUrl(uri, networkType) ?? '');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
diff --git a/packages/vechain-kit/src/hooks/api/ipfs/useIpfsMetadata.ts b/packages/vechain-kit/src/hooks/api/ipfs/useIpfsMetadata.ts
index 411bb57..dfe57bc 100644
--- a/packages/vechain-kit/src/hooks/api/ipfs/useIpfsMetadata.ts
+++ b/packages/vechain-kit/src/hooks/api/ipfs/useIpfsMetadata.ts
@@ -18,6 +18,8 @@ export const getIpfsMetadata = async (
): Promise => {
if (!uri) throw new Error('No URI provided');
const newUri = convertUriToUrl(uri, networkType);
+ if (!newUri) throw new Error('Invalid URI');
+
const response = await fetch(newUri);
const data = await response.text();
diff --git a/packages/vechain-kit/src/hooks/api/ipfs/useSingleImageUpload.ts b/packages/vechain-kit/src/hooks/api/ipfs/useSingleImageUpload.ts
new file mode 100644
index 0000000..68df1fc
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/api/ipfs/useSingleImageUpload.ts
@@ -0,0 +1,59 @@
+import { useState, useCallback, useEffect } from 'react';
+import imageCompression from 'browser-image-compression';
+import { imageCompressionOptions, UploadedImage } from './useUploadImages';
+
+type Props = {
+ compressImage?: boolean;
+ defaultImage?: UploadedImage;
+};
+/**
+ * Hook to handle image uploads and compressions in a dropzone
+ * @param param0 compressImags: boolean to indicate if the image should be compressed (default: true)
+ * @param param1 defaultImage: default image to be displayed
+ * @returns uploaded image, setUploadedImage: function to set the uploaded image, onDrop: function to handle the drop event
+ */
+
+export const useSingleImageUpload = ({
+ compressImage,
+ defaultImage,
+}: Props) => {
+ const [uploadedImage, setUploadedImage] = useState<
+ UploadedImage | undefined
+ >(defaultImage);
+
+ useEffect(() => {
+ if (defaultImage) {
+ setUploadedImage(defaultImage);
+ }
+ }, [defaultImage]);
+
+ const onRemove = useCallback(() => setUploadedImage(undefined), []);
+
+ const onUpload = useCallback(
+ async (acceptedFile: File) => {
+ let parsedFile = acceptedFile;
+ if (compressImage) {
+ parsedFile = await imageCompression(
+ parsedFile,
+ imageCompressionOptions,
+ );
+ }
+
+ const image: UploadedImage = {
+ file: parsedFile,
+ image: URL.createObjectURL(parsedFile),
+ };
+
+ setUploadedImage(image);
+ return image;
+ },
+ [compressImage],
+ );
+
+ return {
+ uploadedImage,
+ setUploadedImage,
+ onUpload,
+ onRemove,
+ };
+};
diff --git a/packages/vechain-kit/src/hooks/api/ipfs/useUploadImages.ts b/packages/vechain-kit/src/hooks/api/ipfs/useUploadImages.ts
new file mode 100644
index 0000000..7c10183
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/api/ipfs/useUploadImages.ts
@@ -0,0 +1,107 @@
+'use client';
+import { useState, useCallback, useEffect } from 'react';
+import imageCompression, {
+ Options as CompressOptions,
+} from 'browser-image-compression';
+
+export const imageCompressionOptions: CompressOptions = {
+ maxSizeMB: 0.4,
+ maxWidthOrHeight: 1920,
+ useWebWorker: true,
+};
+
+export const compressImages = async (images: UploadedImage[]) => {
+ const compressedImages: File[] = [];
+ try {
+ for (const image of images) {
+ const parsedFile = await imageCompression(
+ image.file,
+ imageCompressionOptions,
+ );
+
+ compressedImages.push(parsedFile);
+ }
+ return compressedImages;
+ } catch (e) {
+ console.error('compress error', e);
+ throw e;
+ }
+};
+
+type Props = {
+ compressImages?: boolean;
+ defaultImages?: UploadedImage[];
+};
+/**
+ * Hook to handle image uploads and compressions in a dropzone
+ * @param param0 compressImages: boolean to indicate if the images should be compressed (default: true)
+ * @returns uploadedImages: array of uploaded images, setUploadedImages: function to set the uploaded images, onDrop: function to handle the drop event
+ */
+
+export type UploadedImage = {
+ file: File;
+ image: string;
+};
+export const useUploadImages = ({ compressImages, defaultImages }: Props) => {
+ const [uploadedImages, setUploadedImages] = useState(
+ defaultImages ?? [],
+ );
+
+ useEffect(() => {
+ if (defaultImages) {
+ setUploadedImages(defaultImages);
+ }
+ }, [defaultImages]);
+
+ const [invalidDateError, setInvalidDateError] = useState([]);
+
+ const onRemove = useCallback(
+ (index: number) =>
+ setUploadedImages((s) => s.filter((_, i) => i !== index)),
+ [],
+ );
+
+ const onUpload = useCallback(
+ async (acceptedFiles: File[], keepCurrent = true) => {
+ setInvalidDateError([]);
+
+ const parsedUploads: UploadedImage[] = [];
+ for (const file of acceptedFiles) {
+ let parsedFile = file;
+ if (compressImages) {
+ parsedFile = await imageCompression(
+ file,
+ imageCompressionOptions,
+ );
+ }
+
+ const image: UploadedImage = {
+ file: parsedFile,
+ image: URL.createObjectURL(file),
+ };
+ parsedUploads.push(image);
+ }
+
+ setUploadedImages((s) => [
+ ...parsedUploads,
+ ...(!keepCurrent
+ ? []
+ : s.filter(
+ (f) =>
+ !parsedUploads.some(
+ (p) => p.file.name === f.file.name,
+ ),
+ )),
+ ]);
+ },
+ [compressImages],
+ );
+
+ return {
+ uploadedImages,
+ setUploadedImages,
+ onUpload,
+ onRemove,
+ invalidDateError,
+ };
+};
diff --git a/packages/vechain-kit/src/hooks/api/vebetterdao/xApps/getXAppMetadata.ts b/packages/vechain-kit/src/hooks/api/vebetterdao/xApps/getXAppMetadata.ts
index 1d5e679..cc03c00 100644
--- a/packages/vechain-kit/src/hooks/api/vebetterdao/xApps/getXAppMetadata.ts
+++ b/packages/vechain-kit/src/hooks/api/vebetterdao/xApps/getXAppMetadata.ts
@@ -41,7 +41,10 @@ export const getXAppMetadata = async (
uri: string,
networkType: NETWORK_TYPE,
): Promise => {
- const response = await fetch(convertUriToUrl(uri, networkType));
+ const url = convertUriToUrl(uri, networkType);
+ if (!url) return undefined;
+
+ const response = await fetch(url);
if (!response.ok) {
return undefined;
diff --git a/packages/vechain-kit/src/hooks/api/vetDomains/index.ts b/packages/vechain-kit/src/hooks/api/vetDomains/index.ts
index 810a3a6..32839b0 100644
--- a/packages/vechain-kit/src/hooks/api/vetDomains/index.ts
+++ b/packages/vechain-kit/src/hooks/api/vetDomains/index.ts
@@ -3,3 +3,5 @@ export * from './useEnsRecordExists';
export * from './useClaimVeWorldSubdomain';
export * from './useIsDomainProtected';
export * from './useGetDomainsOfAddress';
+export * from './useGetAvatar';
+export * from './useUpdateAvatarRecord';
diff --git a/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatar.ts b/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatar.ts
new file mode 100644
index 0000000..da158fb
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatar.ts
@@ -0,0 +1,123 @@
+import { useQuery } from '@tanstack/react-query';
+import { useVeChainKitConfig } from '@/providers';
+import { Interface, namehash } from 'ethers';
+import { NETWORK_TYPE } from '@/config/network';
+import { getConfig } from '@/config';
+
+const nameInterface = new Interface([
+ 'function resolver(bytes32 node) returns (address resolverAddress)',
+ 'function text(bytes32 node, string key) returns (string avatar)',
+]);
+
+/**
+ * Fetches the avatar for a given VET domain name
+ * @param networkType - The network type ('main' or 'test')
+ * @param nodeUrl - The node URL
+ * @param name - The VET domain name
+ * @returns The avatar URL from the response
+ */
+export const getAvatar = async (
+ networkType: NETWORK_TYPE,
+ nodeUrl: string,
+ name: string,
+): Promise => {
+ if (!name) throw new Error('Name is required');
+
+ const node = namehash(name);
+
+ try {
+ // Get resolver address
+ const resolverResponse = await fetch(`${nodeUrl}/accounts/*`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ clauses: [
+ {
+ to: getConfig(networkType).vetDomainsContractAddress,
+ data: nameInterface.encodeFunctionData('resolver', [
+ node,
+ ]),
+ },
+ ],
+ }),
+ });
+
+ const [{ data: resolverData, reverted: noResolver }] =
+ await resolverResponse.json();
+
+ if (noResolver) {
+ return null;
+ }
+
+ const { resolverAddress } = nameInterface.decodeFunctionResult(
+ 'resolver',
+ resolverData,
+ );
+
+ // Get avatar
+ const avatarResponse = await fetch(`${nodeUrl}/accounts/*`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ clauses: [
+ {
+ to: resolverAddress,
+ data: nameInterface.encodeFunctionData('text', [
+ node,
+ 'avatar',
+ ]),
+ },
+ ],
+ }),
+ });
+
+ const [{ data: lookupData, reverted: noLookup }] =
+ await avatarResponse.json();
+
+ if (noLookup) {
+ return null;
+ }
+
+ const { avatar } = nameInterface.decodeFunctionResult(
+ 'text',
+ lookupData,
+ );
+
+ return avatar === '' ? null : avatar;
+ } catch (error) {
+ console.error('Error fetching avatar:', error);
+ throw error;
+ }
+};
+
+export const getAvatarQueryKey = (name: string) => [
+ 'VECHAIN_KIT',
+ 'VET_DOMAINS',
+ 'AVATAR',
+ name,
+];
+
+/**
+ * Hook to fetch the avatar URL for a VET domain name
+ * @param name - The VET domain name
+ * @returns The resolved avatar URL
+ */
+export const useGetAvatar = (name?: string) => {
+ const { network } = useVeChainKitConfig();
+ const nodeUrl = network.nodeUrl ?? getConfig(network.type).nodeUrl;
+
+ const avatarQuery = useQuery({
+ queryKey: getAvatarQueryKey(name ?? ''),
+ queryFn: () => getAvatar(network.type, nodeUrl, name!),
+ enabled: !!name && !!nodeUrl && !!network.type,
+ retry: 3,
+ retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ });
+
+ return avatarQuery;
+};
diff --git a/packages/vechain-kit/src/hooks/api/vetDomains/useUpdateAvatarRecord.ts b/packages/vechain-kit/src/hooks/api/vetDomains/useUpdateAvatarRecord.ts
new file mode 100644
index 0000000..7bbcd02
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/api/vetDomains/useUpdateAvatarRecord.ts
@@ -0,0 +1,86 @@
+import { Interface, namehash } from 'ethers';
+import { useVeChainKitConfig } from '@/providers';
+import { getConfig } from '@/config';
+import { useCallback } from 'react';
+import { useSendTransaction } from '@/hooks/transactions/useSendTransaction';
+
+const nameInterface = new Interface([
+ 'function resolver(bytes32 node) returns (address resolverAddress)',
+ 'function setText(bytes32 node, string key, string value) external',
+]);
+
+type UseUpdateAvatarRecordProps = {
+ onSuccess?: () => void;
+ onError?: () => void;
+};
+
+export const useUpdateAvatarRecord = ({
+ onSuccess,
+ onError,
+}: UseUpdateAvatarRecordProps) => {
+ const { network } = useVeChainKitConfig();
+ const nodeUrl = network.nodeUrl ?? getConfig(network.type).nodeUrl;
+ const { sendTransaction } = useSendTransaction({
+ onTxConfirmed: onSuccess,
+ onTxFailedOrCancelled: onError,
+ });
+
+ const updateAvatar = useCallback(
+ async (domain: string, ipfsUri: string) => {
+ if (!domain) throw new Error('Domain is required');
+ // Remove the IPFS URI validation to allow empty strings, so we can reset the avatar
+ // if (!ipfsUri) throw new Error('IPFS URI is required');
+
+ const node = namehash(domain);
+
+ // Get resolver address using read-only call
+ const resolverResponse = await fetch(`${nodeUrl}/accounts/*`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ clauses: [
+ {
+ to: getConfig(network.type)
+ .vetDomainsContractAddress,
+ data: nameInterface.encodeFunctionData('resolver', [
+ node,
+ ]),
+ },
+ ],
+ }),
+ });
+
+ const [{ data: resolverData, reverted: noResolver }] =
+ await resolverResponse.json();
+ if (noResolver) {
+ throw new Error('Failed to get resolver address');
+ }
+
+ const { resolverAddress } = nameInterface.decodeFunctionResult(
+ 'resolver',
+ resolverData,
+ );
+
+ // Send transaction to update avatar text record
+ const setTextClause = {
+ to: resolverAddress,
+ data: nameInterface.encodeFunctionData('setText', [
+ node,
+ 'avatar',
+ ipfsUri,
+ ]),
+ value: '0',
+ comment: 'Update avatar record',
+ };
+
+ await sendTransaction([setTextClause]);
+ },
+ [network.type, nodeUrl, sendTransaction],
+ );
+
+ return {
+ updateAvatar,
+ };
+};
diff --git a/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts b/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
index 0424298..bfaeccf 100644
--- a/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
+++ b/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
@@ -2,17 +2,8 @@
import { useLoginWithOAuth, usePrivy, User } from '@privy-io/react-auth';
import { useWallet as useDappKitWallet } from '@vechain/dapp-kit-react';
-import {
- useVechainDomain,
- useGetChainId,
- useGetNodeUrl,
- useContractVersion,
-} from '@/hooks';
-import {
- compareAddresses,
- getPicassoImage,
- VECHAIN_PRIVY_APP_ID,
-} from '@/utils';
+import { useGetChainId, useGetNodeUrl, useContractVersion } from '@/hooks';
+import { compareAddresses, VECHAIN_PRIVY_APP_ID } from '@/utils';
import { ConnectionSource, SmartAccount, Wallet } from '@/types';
import { useSmartAccount } from '.';
import { useVeChainKitConfig } from '@/providers';
@@ -21,6 +12,7 @@ import { useAccount } from 'wagmi';
import { usePrivyCrossAppSdk } from '@/providers/PrivyCrossAppProvider';
import { useCallback, useEffect, useState } from 'react';
import { useCrossAppConnectionCache } from '@/hooks';
+import { useWalletMetadata } from './useWalletMetadata';
export type UseWalletReturnType = {
// This will be the smart account if connected with privy, otherwise it will be wallet connected with dappkit
@@ -32,15 +24,6 @@ export type UseWalletReturnType = {
// Every user connected with privy has one
smartAccount: SmartAccount;
- // When user connects with a wallet
- dappKitWallet?: Wallet;
-
- // wallet created by the social login provider
- embeddedWallet?: Wallet;
-
- // Wallet of the user connected with a cross app provider
- crossAppWallet?: Wallet;
-
// Privy user if user is connected with privy
privyUser: User | null;
@@ -128,21 +111,23 @@ export const useWallet = (): UseWalletReturnType => {
isConnectedWithSocialLogin ||
isConnectedWithCrossApp;
- setIsConnected(isNowConnected);
+ if (isConnected !== isNowConnected) {
+ setIsConnected(isNowConnected);
- // Force re-render of dependent components
- if (!isNowConnected) {
- // Clear any cached wallet data
- clearConnectionCache();
- // Dispatch event to trigger re-renders
- window.dispatchEvent(new Event('wallet_disconnected'));
+ // Only clear cache and dispatch event when disconnecting
+ if (!isNowConnected) {
+ // Clear any cached wallet data
+ clearConnectionCache();
+ // Dispatch event to trigger re-renders
+ window.dispatchEvent(new Event('wallet_disconnected'));
+ }
}
}, [
isConnectedWithDappKit,
isConnectedWithSocialLogin,
isConnectedWithCrossApp,
clearConnectionCache,
- connectionSource,
+ isConnected,
]);
// Get embedded wallet
@@ -163,38 +148,42 @@ export const useWallet = (): UseWalletReturnType => {
? dappKitAccount
: smartAccount?.address;
- const accountDomain = useVechainDomain(activeAddress ?? '').data?.domain;
+ const activeMetadata = useWalletMetadata(activeAddress, network.type);
+ const connectedMetadata = useWalletMetadata(
+ connectedWalletAddress,
+ network.type,
+ );
+ const smartAccountMetadata = useWalletMetadata(
+ smartAccount?.address,
+ network.type,
+ );
+
const account = activeAddress
? {
address: activeAddress,
- domain: accountDomain,
- image: getPicassoImage(activeAddress),
+ domain: activeMetadata.domain,
+ image: activeMetadata.image,
}
: null;
- const connectedWalletDomain = useVechainDomain(connectedWalletAddress ?? '')
- .data?.domain;
const connectedWallet = connectedWalletAddress
? {
address: connectedWalletAddress,
- domain: connectedWalletDomain,
- image: getPicassoImage(connectedWalletAddress),
+ domain: connectedMetadata.domain,
+ image: connectedMetadata.image,
}
: null;
- //TODO: add isLoading for each domain
- // Use cached domain lookups for each address
- const walletDomain = useVechainDomain(dappKitAccount ?? '').data?.domain;
- const smartAccountDomain = useVechainDomain(smartAccount?.address ?? '')
- .data?.domain;
- const embeddedWalletDomain = useVechainDomain(privyEmbeddedWallet ?? '')
- .data?.domain;
- const crossAppAccountDomain = useVechainDomain(crossAppAddress ?? '').data
- ?.domain;
+ // Get smart account version
const { data: smartAccountVersion } = useContractVersion(
smartAccount?.address ?? '',
);
+ const hasActiveSmartAccount =
+ !!smartAccount?.address &&
+ !!account?.address &&
+ compareAddresses(smartAccount?.address, account?.address);
+
// Modify the disconnect function to ensure state updates
const disconnect = useCallback(async () => {
try {
@@ -225,43 +214,16 @@ export const useWallet = (): UseWalletReturnType => {
clearConnectionCache,
]);
- const hasActiveSmartAccount =
- !!smartAccount?.address &&
- !!account?.address &&
- compareAddresses(smartAccount?.address, account?.address);
-
return {
account,
smartAccount: {
address: smartAccount?.address ?? '',
- domain: smartAccountDomain,
- image: getPicassoImage(smartAccount?.address ?? ''),
+ domain: smartAccountMetadata.domain,
+ image: smartAccountMetadata.image,
isDeployed: smartAccount?.isDeployed ?? false,
isActive: hasActiveSmartAccount,
version: smartAccountVersion ?? null,
},
- dappKitWallet: isConnectedWithDappKit
- ? {
- address: dappKitAccount,
- domain: walletDomain,
- image: getPicassoImage(dappKitAccount ?? ''),
- }
- : undefined,
- embeddedWallet: privyEmbeddedWallet
- ? {
- address: privyEmbeddedWallet,
- domain: embeddedWalletDomain,
- image: getPicassoImage(privyEmbeddedWallet),
- }
- : undefined,
- crossAppWallet: crossAppAddress
- ? {
- address: crossAppAddress,
- domain: crossAppAccountDomain,
- image: getPicassoImage(crossAppAddress),
- }
- : undefined,
-
connectedWallet,
privyUser: user,
connection: {
diff --git a/packages/vechain-kit/src/hooks/api/wallet/useWalletMetadata.ts b/packages/vechain-kit/src/hooks/api/wallet/useWalletMetadata.ts
new file mode 100644
index 0000000..f250e64
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/api/wallet/useWalletMetadata.ts
@@ -0,0 +1,17 @@
+import { NETWORK_TYPE } from '@/config/network';
+import { useGetAvatar, useVechainDomain } from '../vetDomains';
+import { convertUriToUrl, getPicassoImage } from '@/utils';
+
+export const useWalletMetadata = (
+ address: string | null | undefined,
+ networkType: NETWORK_TYPE,
+) => {
+ const { data: domain } = useVechainDomain(address ?? '');
+ const { data: avatar } = useGetAvatar(domain?.domain);
+ const avatarUrl = convertUriToUrl(avatar ?? '', networkType);
+
+ return {
+ domain: domain?.domain,
+ image: avatarUrl ?? getPicassoImage(address ?? ''),
+ };
+};
diff --git a/packages/vechain-kit/src/hooks/modals/index.ts b/packages/vechain-kit/src/hooks/modals/index.ts
index bd917e6..ca9c953 100644
--- a/packages/vechain-kit/src/hooks/modals/index.ts
+++ b/packages/vechain-kit/src/hooks/modals/index.ts
@@ -9,3 +9,4 @@ export * from './useEmbeddedWalletSettingsModal';
export * from './useExploreEcosystemModal';
export * from './useNotificationsModal';
export * from './useFAQModal';
+export * from './useAccountCustomizationModal';
diff --git a/packages/vechain-kit/src/hooks/modals/useAccountCustomizationModal.tsx b/packages/vechain-kit/src/hooks/modals/useAccountCustomizationModal.tsx
new file mode 100644
index 0000000..045b078
--- /dev/null
+++ b/packages/vechain-kit/src/hooks/modals/useAccountCustomizationModal.tsx
@@ -0,0 +1,32 @@
+import { useVeChainKitConfig } from '@/providers';
+import { ReactNode } from 'react';
+
+export const useAccountCustomizationModal = () => {
+ const {
+ openAccountModal,
+ closeAccountModal,
+ isAccountModalOpen,
+ setAccountModalContent,
+ } = useVeChainKitConfig();
+
+ const open = () => {
+ setAccountModalContent('account-customization');
+ openAccountModal();
+ };
+
+ const close = () => {
+ closeAccountModal();
+ };
+
+ return {
+ open,
+ close,
+ isOpen: isAccountModalOpen,
+ };
+};
+
+export const AccountCustomizationModalProvider = ({
+ children,
+}: {
+ children: ReactNode;
+}) => <>{children}>;
diff --git a/packages/vechain-kit/src/languages/en.json b/packages/vechain-kit/src/languages/en.json
index 81635c2..fc336e2 100644
--- a/packages/vechain-kit/src/languages/en.json
+++ b/packages/vechain-kit/src/languages/en.json
@@ -308,5 +308,29 @@
"{{element}} website": "{{element}} website",
"{{name}}": "{{name}}",
"Loading your domains...": "Loading your domains...",
- "Your existing domains": "Your existing domains"
+ "Your existing domains": "Your existing domains",
+ "Customize account": "Customize account",
+ "Customize Account": "Customize Account",
+ "Customize your account with a nickname and a picture to easily identify it.": "Customize your account with a nickname and a picture to easily identify it.",
+ "Choose a unique .vet domain name for your account.": "Choose a unique .vet domain name for your account.",
+ "Profile Picture": "Profile Picture",
+ "Customize your account profile picture": "Customize your account profile picture",
+ "Profile Picture Updated": "Profile Picture Updated",
+ "Your profile picture has been successfully updated.": "Your profile picture has been successfully updated.",
+ "Back to Settings": "Back to Settings",
+ "Upload profile image": "Upload profile image",
+ "You can change your profile image only after you setup your domain name.": "You can change your profile image only after you setup your domain name.",
+ "Customize your account by adding a profile image, which will be displayed on the apps you use.": "Customize your account by adding a profile image, which will be displayed on the apps you use.",
+ "Uploading image": "Uploading image",
+ "Setting image": "Setting image",
+ "Your new profile image is set correctly": "Your new profile image is set correctly",
+ "Update profile image": "Update profile image",
+ "Customize your account with a unique .vet domain name and profile image to enhance your identity across VeChain applications.": "Customize your account with a unique .vet domain name and profile image to enhance your identity across VeChain applications.",
+ "Your customizations are linked to your .vet domain name, making them portable across different applications.": "Your customizations are linked to your .vet domain name, making them portable across different applications.",
+ "To get started with customization, first secure your .vet domain name. Once you have a domain, you can add a profile image that will be visible wherever you use your account.": "To get started with customization, first secure your .vet domain name. Once you have a domain, you can add a profile image that will be visible wherever you use your account.",
+ "Changing your domain name will update also your profile image.": "Changing your domain name will update also your profile image.",
+ "Manage passkey login": "Manage passkey login",
+ "This is your main wallet, created by {{element}} and secured by Privy. This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions. Please be sure to keep it safe and backed up.": "This is your main wallet, created by {{element}} and secured by Privy. This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions. Please be sure to keep it safe and backed up.",
+ "This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions.": "This wallet is the owner of your smart account, which is used as your identity and as a gateway for your blockchain interactions.",
+ "Handle Passkey Login": "Handle Passkey Login"
}
diff --git a/packages/vechain-kit/src/utils/ipfs.ts b/packages/vechain-kit/src/utils/ipfs.ts
index 91f3016..f515c8b 100644
--- a/packages/vechain-kit/src/utils/ipfs.ts
+++ b/packages/vechain-kit/src/utils/ipfs.ts
@@ -1,3 +1,6 @@
+import { getConfig } from '@/config';
+import { NETWORK_TYPE } from '@/config/network';
+
/**
* Validate IPFS URI strings. An example of a valid IPFS URI is:
* - ipfs://QmfSTia1TJUiKQ2fyW9NTPzEKNdjMGzbUgrC3QPSTpkum6/406.json
@@ -22,3 +25,38 @@ export const validateIpfsUri = (uri: string): boolean => {
export function toIPFSURL(cid: string, fileName?: string): string {
return `ipfs://${cid}/${fileName ?? ''}`;
}
+
+/**
+ * Uploads a blob to IPFS.
+ * @param blob The Blob object to upload.
+ * @param filename A name for the file in the FormData payload.
+ * @param networkType The network type to use for the IPFS pinning service.
+ * @returns The IPFS hash of the uploaded blob.
+ */
+export async function uploadBlobToIPFS(
+ blob: Blob,
+ filename: string,
+ networkType: NETWORK_TYPE,
+): Promise {
+ try {
+ const form = new FormData();
+ form.append('file', blob, filename);
+ const response = await fetch(
+ getConfig(networkType).ipfsPinningService,
+ {
+ method: 'POST',
+ body: form,
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ return data.IpfsHash;
+ } catch (error) {
+ console.error('Error uploading blob:', error);
+ throw new Error('Failed to upload blob to IPFS');
+ }
+}
diff --git a/packages/vechain-kit/src/utils/uri.ts b/packages/vechain-kit/src/utils/uri.ts
index c73eea2..be5b318 100644
--- a/packages/vechain-kit/src/utils/uri.ts
+++ b/packages/vechain-kit/src/utils/uri.ts
@@ -14,7 +14,8 @@ export const convertUriToUrl = (uri: string, networkType: NETWORK_TYPE) => {
if (uri.startsWith('data:')) return uri;
const splitUri = uri?.split('://');
- if (splitUri.length !== 2) throw new Error(`Invalid URI ${uri}`);
+ if (splitUri.length !== 2) return;
+ // if (splitUri.length !== 2) throw new Error(`Invalid URI ${uri}`);
const protocol = splitUri?.[0]?.trim();
const uriWithoutProtocol = splitUri[1];
diff --git a/yarn.lock b/yarn.lock
index 1b87fab..4368d88 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6296,6 +6296,13 @@ brorand@^1.0.1, brorand@^1.1.0:
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==
+browser-image-compression@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-2.0.2.tgz#4d5ef8882e9e471d6d923715ceb9034499d14eaa"
+ integrity sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==
+ dependencies:
+ uzip "0.20201231.0"
+
browserify-aes@^1.0.4, browserify-aes@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
@@ -13087,6 +13094,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+uzip@0.20201231.0:
+ version "0.20201231.0"
+ resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14"
+ integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==
+
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@@ -13156,9 +13168,9 @@ viem@^2.1.1:
ws "8.17.1"
viem@^2.21.14:
- version "2.22.13"
- resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.13.tgz#518c3286864cfc25fd8f028eee5a07078cc99349"
- integrity sha512-MaQKY5DUQ5SnZJPMytp5nTgvRu7N3wzvBhY31/9VT4lxDZAcQolqYEK3EqP+cdAD8jl0YmGuoJlfW9D1crqlGg==
+ version "2.22.19"
+ resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.19.tgz#e27d4f9010711e2d55020aef051aa09341900fbf"
+ integrity sha512-aGR/NUHaboQ/HoS86wYfJWbXt6aewjhp2OCO2uczCrusgcwXO/qC0l36AcFVw2dkOPBEhIG5oXMEso97L+xHmA==
dependencies:
"@noble/curves" "1.8.1"
"@noble/hashes" "1.7.1"