Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nft avatar modal added #3536

Merged
merged 9 commits into from
Aug 17, 2023
Merged
23 changes: 20 additions & 3 deletions apps/web/src/components/Profile/NftGallery/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import type { NftGalleryItem } from 'src/store/nft-gallery';
import { useNftGalleryStore } from 'src/store/nft-gallery';
import { mainnet } from 'wagmi/chains';

const Picker: FC = () => {
type PickerProps = {
bhavya2611 marked this conversation as resolved.
Show resolved Hide resolved
onlyAllowOne?: boolean;
};

const Picker: FC<PickerProps> = ({ onlyAllowOne }) => {
const currentProfile = useAppStore((state) => state.currentProfile);
const gallery = useNftGalleryStore((state) => state.gallery);
const setGallery = useNftGalleryStore((state) => state.setGallery);
Expand Down Expand Up @@ -80,11 +84,24 @@ const Picker: FC = () => {
if (gallery.items.length === 50) {
return toast.error(t`Only 50 items allowed for gallery`);
}

const customId = `${item.chainId}_${item.contractAddress}_${item.tokenId}`;
const nft = {
itemId: customId,
...item
};

if (onlyAllowOne) {
setGallery({
...gallery,
name: '',
items: [nft],
toAdd: [],
toRemove: []
});
return;
}

const alreadySelectedIndex = gallery.items.findIndex(
(n) => n.itemId === customId
);
Expand Down Expand Up @@ -143,7 +160,7 @@ const Picker: FC = () => {
});

return (
<div className="grid grid-cols-1 gap-4 p-5 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 p-5 sm:grid-cols-3 md:grid-cols-4">
{nfts?.map((nft, index) => {
const id = `${nft.chainId}_${nft.contractAddress}_${nft.tokenId}`;
const isSelected = selectedItems.includes(id);
Expand All @@ -156,7 +173,7 @@ const Picker: FC = () => {
)}
>
{isSelected && (
<button className="bg-brand-500 absolute right-2 top-2 rounded-full">
<button className="bg-brand-500 absolute right-2 top-2 z-20 rounded-full">
<CheckIcon className="h-5 w-5 p-1 text-white" />
</button>
)}
Expand Down
197 changes: 197 additions & 0 deletions apps/web/src/components/Settings/Profile/NftAvatarModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import Picker from '@components/Profile/NftGallery/Picker';
import { PencilIcon } from '@heroicons/react/outline';
import { LensHub } from '@lenster/abis';
import { LENSHUB_PROXY } from '@lenster/data/constants';
import { Errors } from '@lenster/data/errors';
import { SETTINGS } from '@lenster/data/tracking';
import {
type UpdateProfileImageRequest,
useBroadcastMutation,
useCreateSetProfileImageUriTypedDataMutation,
useCreateSetProfileImageUriViaDispatcherMutation,
useNftChallengeLazyQuery
} from '@lenster/lens';
import getSignature from '@lenster/lib/getSignature';
import { Button, ErrorMessage, Spinner } from '@lenster/ui';
import errorToast from '@lib/errorToast';
import { Leafwatch } from '@lib/leafwatch';
import { t, Trans } from '@lingui/macro';
import type { FC } from 'react';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useAppStore } from 'src/store/app';
import { useGlobalModalStateStore } from 'src/store/modals';
import { useNftGalleryStore } from 'src/store/nft-gallery';
import { useNonceStore } from 'src/store/nonce';
import { useContractWrite, useSignMessage, useSignTypedData } from 'wagmi';

const NftAvatarModal: FC = () => {
const setShowNftAvatarModal = useGlobalModalStateStore(
(state) => state.setShowNftAvatarModal
);
const userSigNonce = useNonceStore((state) => state.userSigNonce);
const [isLoading, setIsLoading] = useState(false);
const currentProfile = useAppStore((state) => state.currentProfile);
const gallery = useNftGalleryStore((state) => state.gallery);

const { signMessageAsync } = useSignMessage();

const setUserSigNonce = useNonceStore((state) => state.setUserSigNonce);

const onCompleted = (__typename?: 'RelayError' | 'RelayerResult') => {
if (__typename === 'RelayError') {
return;
}

setIsLoading(false);
toast.success(t`Avatar updated successfully!`);
Leafwatch.track(SETTINGS.PROFILE.SET_NFT_PICTURE);
};

const onError = (error: any) => {
setIsLoading(false);
errorToast(error);
};

const [loadChallenge] = useNftChallengeLazyQuery();
const [broadcast] = useBroadcastMutation({
onCompleted: ({ broadcast }) => onCompleted(broadcast.__typename)
});
const { signTypedDataAsync } = useSignTypedData({ onError });

const { error, write } = useContractWrite({
address: LENSHUB_PROXY,
abi: LensHub,
functionName: 'setProfileImageURI',
onSuccess: () => onCompleted(),
onError
});

// Dispatcher
const canUseRelay = currentProfile?.dispatcher?.canUseRelay;
const isSponsored = currentProfile?.dispatcher?.sponsor;

const [createSetProfileImageURITypedData] =
useCreateSetProfileImageUriTypedDataMutation({
onCompleted: async ({ createSetProfileImageURITypedData }) => {
const { id, typedData } = createSetProfileImageURITypedData;
const signature = await signTypedDataAsync(getSignature(typedData));
setUserSigNonce(userSigNonce + 1);
const { data } = await broadcast({
variables: { request: { id, signature } }
});
if (data?.broadcast.__typename === 'RelayError') {
const { profileId, imageURI } = typedData.value;
return write?.({ args: [profileId, imageURI] });
}
},
onError
});

const [createSetProfileImageURIViaDispatcher] =
useCreateSetProfileImageUriViaDispatcherMutation({
onCompleted: ({ createSetProfileImageURIViaDispatcher }) =>
onCompleted(createSetProfileImageURIViaDispatcher.__typename),
onError
});

const createViaDispatcher = async (request: UpdateProfileImageRequest) => {
const { data } = await createSetProfileImageURIViaDispatcher({
variables: { request }
});
if (
data?.createSetProfileImageURIViaDispatcher?.__typename === 'RelayError'
) {
return await createSetProfileImageURITypedData({
variables: {
options: { overrideSigNonce: userSigNonce },
request
}
});
}
};

const setAvatar = async () => {
if (!currentProfile || gallery.items.length === 0) {
return toast.error(Errors.SignWallet);
}

try {
setIsLoading(true);
const { contractAddress, tokenId, chainId } = gallery.items[0];
const challengeRes = await loadChallenge({
variables: {
request: {
ethereumAddress: currentProfile?.ownedBy,
nfts: [
{
contractAddress,
tokenId,
chainId
}
]
}
}
});

const signature = await signMessageAsync({
message: challengeRes?.data?.nftOwnershipChallenge?.text as string
});

const request: UpdateProfileImageRequest = {
profileId: currentProfile?.id,
nftData: {
id: challengeRes?.data?.nftOwnershipChallenge?.id,
signature
}
};

setShowNftAvatarModal(false);

if (canUseRelay && isSponsored) {
return await createViaDispatcher(request);
}

return await createSetProfileImageURITypedData({
variables: {
options: { overrideSigNonce: userSigNonce },
request
}
});
} catch (error) {
onError(error);
}
};

return (
<div className="flex flex-col">
{error && (
<ErrorMessage
className="mb-3"
title={t`Transaction failed!`}
error={error}
/>
)}
<div className="mb-4 mr-1 flex h-[70vh] overflow-y-scroll">
{currentProfile && <Picker onlyAllowOne={true} />}
bhavya2611 marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div className="ml-auto flex items-center space-x-2 p-4">
<Button
onClick={setAvatar}
disabled={isLoading || gallery.items.length === 0}
icon={
isLoading ? (
<Spinner size="xs" />
) : (
<PencilIcon className="h-4 w-4" />
)
}
>
<Trans>Save</Trans>
</Button>
</div>
</div>
);
};

export default NftAvatarModal;
Loading
Loading