Skip to content

Best Practices ​

Follow these best practices to build high-quality Mini Apps that provide excellent user experiences. Use the recommended code snippets from this MiniPay developer documentation for wallet connection, transactions, and balances; they are kept up to date with the wallet behavior.

Wallet connection ​

Run inside MiniPay and check the provider ​

Your app expects to run inside MiniPay where window.ethereum is injected. Check for the provider and show a clear message (or throw) if it's missing:

tsx
if (typeof window.ethereum === "undefined") {
  return <div>This app must be opened from MiniPay.</div>;
}
// Or use getEthereumProvider() that throws — see Project setup / wallet-connection docs.

Optionally detect MiniPay: window.ethereum?.isMiniPay === true.

Always auto-connect ​

✅ Do: Auto-connect on page load

tsx
useEffect(() => {
  if (connectors.length > 0) {
    connect({ connector: connectors[0] });
  }
}, [connectors, connect]);

❌ Don't: Show a connect button

tsx
// Never do this in Mini Apps
<button onClick={() => connect()}>Connect Wallet</button>

No message signing for access ​

Do not prompt users to sign a message to access your site or to authenticate. MiniPay connects automatically; users should not need to sign an arbitrary message to use your Mini App.

Handle connection states ​

Always handle connection states gracefully (Wagmi v3: use useConnection()):

tsx
import { useConnection } from "wagmi";

const { isConnected, isConnecting, address } = useConnection();

if (isConnecting) {
  return <div>Connecting to MiniPay...</div>;
}

if (!isConnected || !address) {
  return <div>Please open this app from MiniPay to connect to your wallet.</div>;
}

Error Handling ​

User-Friendly Error Messages ​

Prefer error codes (from the JSON-RPC / provider error) or standard error names over message text; provider messages can change. Use a generic message when codes don't identify the error.

Provide clear, actionable error messages:

tsx
function handleTransactionError(error: Error & { code?: number }) {
  // Prefer code or name; avoid matching on message text (provider messages can change).
  if (error.code === -32604 || error.name === "UserRejectedRequestError") {
    return "Transaction was cancelled.";
  }
  return "Transaction failed. Please try again.";
}

Log Errors for Debugging ​

Log errors for debugging while showing user-friendly messages:

tsx
try {
  await sendTransaction({ ... });
} catch (error) {
  console.error("Transaction error:", error); // For debugging
  showUserMessage("Transaction failed. Please try again."); // For users
}

Low balance handling ​

If the user's balance is too low to complete an action (e.g. send, pay a network fee), redirect them to MiniPay's Add Cash flow so they can top up. Use the official deeplink for Add Cash — do not hardcode the URL, as deeplinks may change. See Deeplinks for the current Add Cash URL and parameters (e.g. optional token list).

Transaction UX ​

Show Loading States ​

Always show loading states during transactions:

tsx
const { isPending, sendTransaction } = useSendTransaction();
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash });

<button disabled={isPending || isConfirming}>
  {isPending ? "Preparing..." : isConfirming ? "Confirming..." : "Send"}
</button>;

Provide Transaction Feedback ​

Give users clear feedback at each stage:

tsx
{
  isPending && <div>Preparing transaction...</div>;
}
{
  isConfirming && <div>Waiting for confirmation...</div>;
}
{
  isSuccess && <div>Transaction confirmed!</div>;
}
{
  isError && <div>Transaction failed. Please try again.</div>;
}

Display Transaction Hash ​

Show transaction hash so users can track it:

tsx
{
  hash && (
    <div>
      <p>Transaction submitted</p>
      <a href={`https://celoscan.io/tx/${hash}`}>
        View on CeloScan
      </a>
    </div>
  );
}

Security ​

Validate User Input ​

Use a schema library like Zod to validate and parse user input:

tsx
import { z } from "zod";
import { isAddress, type Address } from "viem";

const sendFormSchema = z.object({
  address: z
    .string()
    .transform((v) => v as Address)
    .refine((v) => isAddress(v), { message: "Invalid destination address" }),
  amount: z.coerce.number().positive("Amount must be greater than 0"),
});

// Parse and validate; throws ZodError if invalid
const { address, amount } = sendFormSchema.parse({ address: userAddress, amount: userAmount });

Never Store Private Keys ​

❌ Never: Store private keys or sensitive data ✅ Do: Rely on MiniPay for wallet management

Verify Contract Addresses ​

Always verify contract addresses before interacting:

tsx
const KNOWN_CONTRACTS = {
  USDC: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
  // ...
};

function useTokenAddress(symbol: string) {
  const address = KNOWN_CONTRACTS[symbol];
  if (!address) {
    throw new Error(`Unknown token: ${symbol}`);
  }
  return address;
}

Performance ​

Optimize Contract Reads ​

Use enabled to prevent unnecessary contract reads:

tsx
const { data } = useReadContract({
  address: contractAddress,
  abi: contractAbi,
  functionName: "getValue",
  query: {
    enabled: !!address && !!contractAddress, // Only fetch when ready
  },
});

Batch Contract Calls ​

Batch multiple reads into a single call:

tsx
// ✅ Good: Single call
const { data } = useReadContracts({
  contracts: [
    { address, abi, functionName: "balance" },
    { address, abi, functionName: "decimals" },
    { address, abi, functionName: "symbol" },
  ],
});

// ❌ Bad: Multiple separate calls
const balance = useReadContract({ ... });
const decimals = useReadContract({ ... });
const symbol = useReadContract({ ... });

Cache Contract Data ​

Use React Query's caching to avoid redundant calls:

tsx
const { data } = useReadContract({
  address: contractAddress,
  abi: contractAbi,
  functionName: "getValue",
  query: {
    staleTime: 30000, // Cache for 30 seconds
  },
});

User Experience ​

Mobile-First Design ​

Design for mobile devices first:

  • ✅ Touch-friendly buttons (min 44x44px)
  • ✅ Readable text sizes
  • ✅ Adequate spacing
  • ✅ Responsive layouts

Loading States ​

Show loading states for all async operations:

tsx
const { data, isLoading } = useReadContract({ ... });

if (isLoading) {
  return <Spinner />;
}

Empty States ​

Handle empty states gracefully:

tsx
if (!data || data.length === 0) {
  return <div>No items found</div>;
}

Error Boundaries ​

Use error boundaries to catch and handle errors:

tsx
class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    console.error("Error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong. Please refresh.</div>;
    }
    return this.props.children;
  }
}

Code Organization ​

Separate Concerns ​

Organize code into logical modules:

src/
  hooks/
    useWallet.ts
    useBalance.ts
  components/
    WalletStatus.tsx
    TransactionButton.tsx
  lib/
    contracts.ts
    tokens.ts

Reusable Hooks ​

Create reusable hooks for common patterns:

tsx
// hooks/useTokenBalance.ts
export function useTokenBalance(tokenAddress: Address) {
  const { address } = useAccount();
  return useReadContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: "balanceOf",
    args: [address!],
    query: { enabled: !!address },
  });
}

Type Safety ​

Use TypeScript for type safety:

tsx
type TokenSymbol = "USDC" | "USDm" | "USDT";

function useTokenAddress(symbol: TokenSymbol): Address {
  // TypeScript ensures only valid symbols are used
}

Testing ​

Test Wallet Connection ​

Test that wallet connection works:

tsx
// Test auto-connect
expect(connectors.length).toBeGreaterThan(0);
expect(isConnected).toBe(true);

Test Error Handling ​

Test error scenarios:

  • Insufficient funds
  • Network errors
  • User rejection

Test on Both Networks ​

Test on both mainnet and testnet:

  • ✅ Celo Mainnet (Chain ID: 42220)
  • ✅ Celo Sepolia Testnet (Chain ID: 11142220)

Common Pitfalls ​

❌ Not Auto-Connecting ​

Always auto-connect. Never show a connect button.

❌ Ignoring Errors ​

Always handle errors gracefully with user-friendly messages.

❌ Not Showing Loading States ​

Always show loading states during async operations.

❌ Hardcoding Addresses ​

Use environment variables or configuration for contract addresses.

❌ Not Validating Input ​

Always validate user inputs before using them.

Next Steps ​