React mint panel
This single-mint example reads a collection, checks the connected wallet and submits through the SDK. It uses the first active phase; add a phase selector for collections with several active phases. Your application supplies a connected wallet address and the base64 signer.
Install
npm install @solanadeads/gravemintThe client
Create one client for the application. Use an origin-bound gm_pub_ key in the browser, or a gm_test_ key for test-network collections. Keep gm_live_ server keys out of public build variables.
// lib/gravemint.ts
import { GraveMintClient } from '@solanadeads/gravemint';
export const gm = new GraveMintClient({
apiKey: import.meta.env.VITE_GRAVEMINT_KEY,
});The panel
import { useEffect, useRef, useState } from 'react';
import { MintOutcomeUnknownError, type PriceDisplay, type TransactionSigner, type V1Collection, type V1Eligibility } from '@solanadeads/gravemint';
import { gm } from './lib/gravemint';
function priceLabel(price: PriceDisplay) {
switch (price.kind) {
case 'amount': return price.isFree ? 'Free' : `${price.approximate ? '~' : ''}${price.amount} ${price.currency}`;
case 'range': return `${price.min}–${price.max} ${price.currency}`;
case 'hidden': return 'Price revealed when you qualify';
case 'unknown': return 'See on GraveMint';
}
}
type Props = {
identifier: string;
walletAddress?: string;
signer?: TransactionSigner;
// Persist these IDs outside the component so navigation does not lose the record.
onUnknownOutcome: (sessionIds: string[]) => void;
};
export function MintPanel({ identifier, walletAddress, signer, onUnknownOutcome }: Props) {
const [snapshot, setSnapshot] = useState<{
identifier: string;
walletAddress?: string;
drop: V1Collection;
verdict: V1Eligibility | null;
} | null>(null);
const [readError, setReadError] = useState('');
const [status, setStatus] = useState('');
const [busy, setBusy] = useState(false);
const [uncertain, setUncertain] = useState(false);
const [refresh, setRefresh] = useState(0);
const submitting = useRef(false);
useEffect(() => {
let cancelled = false;
async function load() {
try {
const drop = await gm.v1.collection(identifier);
const phase = drop.phases.active[0];
const verdict = phase && walletAddress && drop.capabilities.supportedBySurface
? await gm.v1.eligibility(drop.collection.id, phase.id, walletAddress)
: null;
if (!cancelled) {
setSnapshot({ identifier, walletAddress, drop, verdict });
setReadError('');
}
} catch (error) {
if (!cancelled) setReadError(error instanceof Error ? error.message : 'Could not load the collection.');
}
}
void load();
return () => { cancelled = true; };
}, [identifier, walletAddress, refresh]);
// Refresh phase state and wallet eligibility periodically.
useEffect(() => {
const timer = setInterval(() => setRefresh(value => value + 1), 30_000);
return () => clearInterval(timer);
}, []);
const current = snapshot?.identifier === identifier && snapshot.walletAddress === walletAddress
? snapshot : null;
const drop = current?.drop;
const verdict = current?.verdict;
const phase = drop?.phases.active[0];
const qualifies = !!verdict && (!verdict.gated || verdict.meetsRequirements === true);
const canMint = !!(drop?.capabilities.supportedBySurface && phase && walletAddress && signer
&& qualifies && verdict?.phase.hasStarted && !verdict.phase.hasEnded
&& !readError && !busy && !uncertain);
async function mint() {
if (!canMint || !drop || !phase || !walletAddress || !signer || submitting.current) return;
submitting.current = true;
setBusy(true);
setStatus('Preparing your mint. Approve the transaction in your wallet when prompted.');
try {
const result = await gm.mint.mint({
collectionId: drop.collection.id, phaseId: phase.id, walletAddress, quantity: 1, signer,
});
setStatus(result.success ? 'Mint complete.' : result.message ?? 'Check the mint results.');
setRefresh(value => value + 1);
} catch (error) {
if (error instanceof MintOutcomeUnknownError) {
setUncertain(true);
setStatus('The mint may still complete. Check your wallet before starting another mint.');
onUnknownOutcome(error.sessionIds);
} else {
setStatus(error instanceof Error ? error.message : 'Could not complete the mint.');
}
} finally {
submitting.current = false;
setBusy(false);
}
}
if (!drop) return <p role="status">{readError || 'Loading…'}</p>;
if (!drop.capabilities.supportedBySurface) {
return drop.capabilities.mintUrl
? <a href={drop.capabilities.mintUrl}>Mint on GraveMint</a>
: <p>This collection is not available through this integration.</p>;
}
return (
<section>
<h2>{drop.collection.name}</h2>
<p>{drop.stats.mintedCount} / {drop.stats.totalSupply} minted</p>
<p>{phase ? priceLabel(phase.priceDisplay) : 'No active phase.'}</p>
{!walletAddress && <p>Connect a wallet to continue.</p>}
{walletAddress && verdict && !qualifies && <p>{verdict.message ?? 'This wallet does not meet the phase requirements.'}</p>}
{readError && <p role="alert">{readError}</p>}
<button onClick={mint} disabled={!canMint}>{busy ? 'Minting…' : 'Mint'}</button>
<p role="status">{status}</p>
</section>
);
}Adapting the panel
Persist unknown-outcome session IDs in your application and keep minting disabled until the outcome is resolved, including after a reload. The callback provides the IDs; durable storage and reconciliation are application responsibilities.
For multiple phases, add a selector and recheck eligibility when it changes. For quantities above one, handle each batch result, including pending transactions. To show a final cost breakdown or a reduced prepared quantity before signing, use the manual prepare/sign/execute flow.
Add wallet-mint counts and remaining allocations with walletMints if the UI displays them. Anchor countdowns to serverTime and refresh the collection at a transition. Preparation checks current eligibility and supply again.
Next
- The read model: response fields and display behavior.
- Wallets and signing: adapters, batches and recovery.
- Troubleshooting: errors and next steps.
