Skip to content
IN THIS GUIDE

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 ​

bash
npm install @solanadeads/gravemint

The 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.

ts
// lib/gravemint.ts
import { GraveMintClient } from '@solanadeads/gravemint';

export const gm = new GraveMintClient({
  apiKey: import.meta.env.VITE_GRAVEMINT_KEY,
});

The panel ​

tsx
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 ​