[Dashboard] Add NFT approval step to Marketplace create listing & auction (#4311)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the `CreateListingButton` and `CreateListingsForm` components to use `mutation` instead of `mutate` for transaction handling.

### Detailed summary
- Refactored `useSendAndConfirmTransaction()` to `useSendAndConfirmTransaction()`
- Renamed `mutate` to `mutation` in `CreateListingButton`
- Updated `useMutationResult` import in `list-form.tsx`
- Replaced `mutate` with `mutation` in `CreateListingsForm`
- Added error handling and approval logic in `CreateListingsForm`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
kien-ngo
2024-08-29 18:11:43 +00:00
parent 68ee714e22
commit 3aaffe1a70
2 changed files with 70 additions and 33 deletions
@@ -27,7 +27,8 @@ import type { WalletNFT } from "lib/wallet/nfts/types";
import { useMemo } from "react";
import { useForm } from "react-hook-form";
import { FiInfo } from "react-icons/fi";
import type { UseMutateFunction } from "react-query-v5";
import type { UseMutationResult } from "react-query-v5";
import { toast } from "sonner";
import {
NATIVE_TOKEN_ADDRESS,
type PreparedTransaction,
@@ -38,6 +39,14 @@ import {
} from "thirdweb";
import type { TransactionReceipt } from "thirdweb/dist/types/transaction/types";
import { decimals } from "thirdweb/extensions/erc20";
import {
isApprovedForAll as isApprovedForAll721,
setApprovalForAll as setApprovalForAll721,
} from "thirdweb/extensions/erc721";
import {
isApprovedForAll as isApprovedForAll1155,
setApprovalForAll as setApprovalForAll1155,
} from "thirdweb/extensions/erc1155";
import { createAuction, createListing } from "thirdweb/extensions/marketplace";
import { useActiveAccount } from "thirdweb/react";
import {
@@ -55,8 +64,6 @@ interface ListForm
extends Omit<NewDirectListing, "type">,
Omit<NewAuctionListing, "type"> {
selected?: WalletNFT;
contractAddress: string;
tokenId: string;
listingType: "direct" | "auction";
listingDurationInSeconds: string;
quantity: string;
@@ -66,12 +73,7 @@ type CreateListingsFormProps = {
contract: ThirdwebContract;
formId: string;
type?: "direct-listings" | "english-auctions";
mutate: UseMutateFunction<
TransactionReceipt,
Error,
PreparedTransaction,
unknown
>;
mutation: UseMutationResult<TransactionReceipt, Error, PreparedTransaction>;
};
const auctionTimes = [
@@ -88,7 +90,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
contract,
formId,
type,
mutate,
mutation,
}) => {
const trackEvent = useTrack();
const network = useEVMContractInfo()?.chain;
@@ -104,8 +106,6 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
const form = useForm<ListForm>({
defaultValues: {
selected: undefined,
contractAddress: "",
tokenId: "",
currencyContractAddress: NATIVE_TOKEN_ADDRESS,
quantity: "1",
buyoutPricePerToken: "0",
@@ -117,21 +117,23 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
},
});
const address = useActiveAccount()?.address;
const account = useActiveAccount();
const selectedContract = getContract({
address: form.watch("contractAddress"),
chain: contract.chain,
client: thirdwebClient,
});
const selectedContract = form.watch("selected.contractAddress")
? getContract({
address: form.watch("selected.contractAddress"),
chain: contract.chain,
client: thirdwebClient,
})
: undefined;
const { data: ownedNFTs, isLoading: isOwnedNFTsLoading } =
useDashboardOwnedNFTs({
contract: selectedContract,
owner: address,
owner: account?.address,
// Only run this hook as the last resort if this chain is not supported by the API services we are using
disabled:
!form.watch("contractAddress") ||
!selectedContract ||
isSupportedChain ||
isWalletNFTsLoading ||
(walletNFTs?.result || []).length > 0,
@@ -150,13 +152,13 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
return {
...nft,
supply: "1",
contractAddress: form.watch("contractAddress"),
contractAddress: form.watch("selected.contractAddress"),
tokenId: nft.id.toString(),
};
}
return {
...nft,
contractAddress: form.watch("contractAddress"),
contractAddress: form.watch("selected.contractAddress"),
tokenId: nft.id.toString(),
};
}) as WalletNFT[];
@@ -179,9 +181,42 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
as="form"
id={formId}
onSubmit={form.handleSubmit(async (formData) => {
if (!formData.selected) {
if (!formData.selected || !selectedContract) {
return;
}
if (!account) {
return toast.error("No account detected");
}
const isNftApproved =
formData.selected.type === "ERC1155"
? isApprovedForAll1155
: isApprovedForAll721;
const isApproved = await isNftApproved({
contract: selectedContract,
operator: contract.address,
owner: account.address,
});
if (!isApproved) {
const setNftApproval =
formData.selected.type === "ERC1155"
? setApprovalForAll1155
: setApprovalForAll721;
const approveTx = setNftApproval({
contract: selectedContract,
operator: contract.address,
approved: true,
});
try {
await mutation.mutateAsync(approveTx);
} catch {
return toast.error("Failed to approve NFT for marketplace");
}
}
if (formData.listingType === "direct") {
// Hard code to 100 years for now
const endTimestamp = new Date(
@@ -197,7 +232,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
pricePerToken: String(formData.buyoutPricePerToken),
endTimestamp,
});
mutate(transaction, {
mutation.mutate(transaction, {
onSuccess: () => {
onSuccess();
modalContext.onClose();
@@ -246,7 +281,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
minimumBidAmountWei * BigInt(formData.quantity),
buyoutBidAmountWei: buyoutBidAmountWei * BigInt(formData.quantity),
});
mutate(transaction, {
mutation.mutate(transaction, {
onSuccess: () => {
onSuccess();
trackEvent({
@@ -305,18 +340,20 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
contract address of the NFT you want to list.
</Text>
</Stack>
<FormControl isInvalid={!!form.formState.errors.contractAddress}>
<FormControl
isInvalid={!!form.formState.errors.selected?.contractAddress}
>
<Heading as={FormLabel} size="label.lg">
Contract address
</Heading>
<SolidityInput
solidityType="address"
formContext={form}
{...form.register("contractAddress")}
{...form.register("selected.contractAddress")}
placeholder=""
/>
<FormErrorMessage>
{form.formState.errors.contractAddress?.message}
{form.formState.errors.selected?.contractAddress?.message}
</FormErrorMessage>
<FormHelperText>
This will display all the NFTs you own from this contract.
@@ -327,7 +364,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
{isWalletNFTsLoading ||
(isOwnedNFTsLoading &&
!isSupportedChain &&
form.watch("contractAddress")) ? (
form.watch("selected.contractAddress")) ? (
<Center height="60px">
<Spinner />
</Center>
@@ -23,7 +23,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
}) => {
const address = useActiveAccount()?.address;
const { isOpen, onOpen, onClose } = useDisclosure();
const { mutate, isPending } = useSendAndConfirmTransaction();
const mutation = useSendAndConfirmTransaction();
return (
<ListerOnly contract={contract}>
@@ -38,7 +38,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
children: (
<>
<Button
isDisabled={isPending}
isDisabled={mutation.isPending}
variant="outline"
mr={3}
onClick={onClose}
@@ -46,7 +46,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
Cancel
</Button>
<TransactionButton
isLoading={isPending}
isLoading={mutation.isPending}
transactionCount={2}
form={LIST_FORM_ID}
type="submit"
@@ -62,7 +62,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
contract={contract}
formId={LIST_FORM_ID}
type={type}
mutate={mutate}
mutation={mutation}
/>
</Drawer>
<Button