Skip to content

Commit

Permalink
fix: rewrite mint button using useEffect (#1541)
Browse files Browse the repository at this point in the history
  • Loading branch information
alessey authored Oct 30, 2024
1 parent 8dcbb1e commit ed910fb
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 32 deletions.
4 changes: 2 additions & 2 deletions playground/nextjs-app-router/lib/nft/buildMintTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async function getMintData({
: [{ collection: contractAddress, quantity }];

const response = await fetch(
'https://api-base.reservoir.tools/execute/mint/v1',
'https://api-base.reservoir.tools/execute/mint/v1?skipBalanceCheck=true',
{
method: 'POST',
headers: {
Expand All @@ -32,7 +32,7 @@ async function getMintData({

const data = await response.json();

if (data.error) {
if (data.message) {
return Promise.reject(data.message);
}

Expand Down
2 changes: 1 addition & 1 deletion playground/nextjs-app-router/onchainkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/onchainkit",
"version": "0.35.1",
"version": "0.35.2",
"type": "module",
"repository": "https://github.com/coinbase/onchainkit.git",
"license": "MIT",
Expand Down
75 changes: 57 additions & 18 deletions src/nft/components/mint/NFTMintButton.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { base } from 'viem/chains';
Expand Down Expand Up @@ -38,12 +38,9 @@ vi.mock('../../../transaction', async (importOriginal) => {
{text}
</button>
),
Transaction: ({ onStatus, children, calls, capabilities }) => (
Transaction: ({ onStatus, children, capabilities }) => (
<>
<div>
<button type="button" onClick={calls}>
Calls
</button>
<button
type="button"
onClick={() => onStatus({ statusName: 'transactionPending' })}
Expand Down Expand Up @@ -130,22 +127,23 @@ describe('NFTMintButton', () => {
});
});

it('should render the mint button with default label', () => {
const { getByText } = render(
it('should render the mint button with default label', async () => {
const { findByText } = render(
<TestProviders>
<NFTMintButton />
</TestProviders>,
);
expect(getByText('Mint')).toBeInTheDocument();

expect(await findByText('Mint')).toBeInTheDocument();
});

it('should render the mint button with custom label', () => {
const { getByText } = render(
it('should render the mint button with custom label', async () => {
const { findByText } = render(
<TestProviders>
<NFTMintButton label="Custom Mint" />
</TestProviders>,
);
expect(getByText('Custom Mint')).toBeInTheDocument();
expect(await findByText('Custom Mint')).toBeInTheDocument();
});

it('should call updateLifecycleStatus with transactionPending status when transactionStatus is transactionPending', () => {
Expand Down Expand Up @@ -229,17 +227,17 @@ describe('NFTMintButton', () => {
expect(container.firstChild).toBeNull();
});

it('should disble button if disabled props is true', () => {
const { getByText } = render(
it('should disble button if disabled props is true', async () => {
const { findByText } = render(
<TestProviders>
<NFTMintButton disabled={true} />
</TestProviders>,
);
expect(getByText('Mint')).toBeDisabled();
expect(await findByText('Mint')).toBeDisabled();
});

it('should show minting not available when isEligibleToMint is false', () => {
(useNFTContext as Mock).mockReturnValueOnce({
(useNFTContext as Mock).mockReturnValue({
contractAddress: '0x123',
tokenId: '1',
quantity: 1,
Expand All @@ -256,7 +254,7 @@ describe('NFTMintButton', () => {
expect(getByText('Minting not available')).toBeInTheDocument();
});

it('calls buildMintTransaction when button is clicked', async () => {
it('calls buildMintTransaction when quantity changes', async () => {
const buildMintTransactionMock = vi.fn().mockResolvedValue([]);
(useNFTContext as Mock).mockReturnValueOnce({
contractAddress: '0x123',
Expand All @@ -266,18 +264,59 @@ describe('NFTMintButton', () => {
quantity: 1,
});

const { getByText } = render(
const { rerender } = render(
<TestProviders>
<NFTMintButton />
</TestProviders>,
);
expect(buildMintTransactionMock).toHaveBeenCalledWith({
contractAddress: '0x123',
tokenId: '1',
takerAddress: '0xabc',
quantity: 1,
});

(useNFTContext as Mock).mockReturnValueOnce({
contractAddress: '0x123',
tokenId: '1',
isEligibleToMint: true,
buildMintTransaction: buildMintTransactionMock,
quantity: 2,
});

rerender(
<TestProviders>
<NFTMintButton />
</TestProviders>,
);
fireEvent.click(getByText('Calls'));

expect(buildMintTransactionMock).toHaveBeenCalledWith({
contractAddress: '0x123',
tokenId: '1',
takerAddress: '0xabc',
quantity: 2,
});
});

it('calls updateLifecycleStatus on buildMintTransaction error', async () => {
const buildMintTransactionMock = vi.fn().mockRejectedValue('error');
(useNFTContext as Mock).mockReturnValueOnce({
contractAddress: '0x123',
tokenId: '1',
isEligibleToMint: true,
buildMintTransaction: buildMintTransactionMock,
quantity: 1,
});

await render(
<TestProviders>
<NFTMintButton />
</TestProviders>,
);

expect(mockUpdateLifecycleStatus).toHaveBeenCalledWith({
statusName: 'error',
statusData: expect.objectContaining({ message: 'error' }),
});
});
});
73 changes: 63 additions & 10 deletions src/nft/components/mint/NFTMintButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAccount, useChainId } from 'wagmi';
import { Spinner } from '../../../internal/components/Spinner';
import { cn } from '../../../styles/theme';
import {
Transaction,
Expand All @@ -11,6 +12,7 @@ import {
TransactionStatusAction,
TransactionStatusLabel,
} from '../../../transaction';
import type { Call } from '../../../transaction/types';
import { ConnectWallet } from '../../../wallet';
import { useNFTLifecycleContext } from '../NFTLifecycleProvider';
import { useNFTContext } from '../NFTProvider';
Expand Down Expand Up @@ -42,6 +44,56 @@ export function NFTMintButton({
isSponsored,
} = useNFTContext();
const { updateLifecycleStatus } = useNFTLifecycleContext();
const [callData, setCallData] = useState<Call[]>([]);
const [mintError, setMintError] = useState<string | null>(null);

const handleTransactionError = useCallback(
(error: string) => {
updateLifecycleStatus({
statusName: 'error',
statusData: {
error: 'Error building mint transaction',
code: 'NmNBc01', // NFT module NFTMintButton component 01 error
message: error,
},
});
setMintError(error);
},
[updateLifecycleStatus],
);

const fetchTransactions = useCallback(async () => {
if (address && buildMintTransaction) {
try {
setCallData([]);
setMintError(null);
const mintTransaction = await buildMintTransaction({
contractAddress,
tokenId,
takerAddress: address,
quantity,
});
setCallData(mintTransaction);
} catch (error) {
handleTransactionError(error as string);
}
} else {
setCallData([]);
}
}, [
address,
contractAddress,
tokenId,
quantity,
buildMintTransaction,
handleTransactionError,
]);

useEffect(() => {
// need to fetch calls on quantity change instead of onClick to avoid smart wallet
// popups getting blocked by safari
fetchTransactions();
}, [fetchTransactions]);

const handleOnStatus = useCallback(
(transactionStatus: TransactionLifecycleStatus) => {
Expand All @@ -65,8 +117,16 @@ export function NFTMintButton({
return 'Minting not available';
}

if (mintError) {
return mintError;
}

if (callData.length === 0) {
return <Spinner />;
}

return label;
}, [isEligibleToMint, label]);
}, [callData, isEligibleToMint, label, mintError]);

if (!buildMintTransaction) {
return null;
Expand All @@ -85,14 +145,7 @@ export function NFTMintButton({
<Transaction
isSponsored={isSponsored}
chainId={chainId}
calls={() =>
buildMintTransaction({
contractAddress,
tokenId,
takerAddress: address,
quantity,
})
}
calls={callData}
onStatus={handleOnStatus}
>
<TransactionButton
Expand Down
2 changes: 1 addition & 1 deletion src/transaction/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export type IsSpinnerDisplayedProps = {
export type TransactionButtonReact = {
className?: string; // An optional CSS class name for styling the button component.
disabled?: boolean; // A optional prop to disable the submit button
text?: string; // An optional text to be displayed in the button component.
text?: ReactNode; // An optional text to be displayed in the button component.
errorOverride?: TransactionButtonOverride; // Optional overrides for text and onClick handler in error state (default is resubmit txn)
successOverride?: TransactionButtonOverride; // Optional overrides for text and onClick handler in success state (default is view txn on block explorer)
pendingOverride?: Pick<TransactionButtonOverride, 'text'>; // Optional overrides for text in pending state (default is loading spinner)
Expand Down

0 comments on commit ed910fb

Please sign in to comment.