Building with Next.js

This guide will walk you through integrating Chipi Pay into your Next.js application with Clerk authentication and secure wallet storage. We’ll cover everything from installation to implementing secure transfer flows.

Prerequisites

  • Node.js 16 or later
  • Next.js 13 or later
  • Basic knowledge of React and Next.js
  • A Chipi Pay account
  • A Clerk account for authentication

Getting Started

To get started with Chipi Pay in your Next.js application, you’ll need to have a basic Next.js project set up. If you don’t have one yet, you can create it using:
npx create-next-app@latest my-chipi-app
cd my-chipi-app

Installation

First, install the required packages:
# Install Chipi Pay SDK
npm install @chipi-pay/chipi-sdk

# Install Clerk
npm install @clerk/nextjs

Configuration

  1. Create a .env.local file in your project root and add your API keys:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
CLERK_SECRET_KEY=your_clerk_secret_key
NEXT_PUBLIC_CHIPI_API_KEY=your_chipi_api_key
  1. Set up the Chipi Pay provider in your application:
// app/providers.tsx
"use client";

import { ChipiProvider } from "@chipi-pay/chipi-sdk";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ChipiProvider
      config={{
        apiPublicKey: process.env.NEXT_PUBLIC_CHIPI_API_KEY,
      }}
    >
      {children}
    </ChipiProvider>
  );
}
  1. Update your root layout to use the providers:
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
import { Providers } from './providers'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <ClerkProvider>
          <Providers>{children}</Providers>
        </ClerkProvider>
      </body>
    </html>
  )
}

Implementing Wallet Storage with Clerk

  1. Create a server action to save the wallet:
// app/actions.ts
"use server";

import { auth, clerkClient } from "@clerk/nextjs";

export async function saveWallet(walletData: any) {
  const { userId } = auth();
  
  if (!userId) {
    throw new Error("Not authenticated");
  }

  // Save wallet to Clerk's public metadata
  await clerkClient.users.updateUser(userId, {
    publicMetadata: {
      wallet: JSON.stringify(walletData)
    }
  });
}

export async function getWallet() {
  const { userId } = auth();
  
  if (!userId) {
    throw new Error("Not authenticated");
  }

  const user = await clerkClient.users.getUser(userId);
  const walletData = user.publicMetadata.wallet;
  
  if (!walletData) {
    return null;
  }

  return JSON.parse(walletData as string);
}
  1. Create a wallet creation component:
// app/components/CreateWallet.tsx
"use client";

import { useState } from "react";
import { useUser } from "@clerk/nextjs";
import { useCreateWallet } from "@chipi-pay/chipi-sdk";
import { saveWallet } from "../actions";

export default function CreateWallet() {
  const { user } = useUser();
  const { createWalletAsync } = useCreateWallet();
  const [encryptKey, setEncryptKey] = useState("");
  const [isProcessing, setIsProcessing] = useState(false);
  const [error, setError] = useState("");
  const [success, setSuccess] = useState("");

  const handleCreateWallet = async () => {
    if (!encryptKey) {
      setError("Please enter an encryption key");
      return;
    }

    try {
      setIsProcessing(true);
      setError("");
      setSuccess("");

      const response = await createWalletAsync({
        encryptKey: encryptKey,
        bearerToken: await user?.getToken({ template: "chipi-sdk" }),
      });

      console.log('createWalletResponse', response);
      
      // Save wallet to Clerk's public metadata
      await saveWallet(response.wallet);
      
      setSuccess("Wallet created successfully!");
      setEncryptKey("");
    } catch (error) {
      setError(error.message || "Failed to create wallet");
      console.error("Error creating wallet:", error);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <div className="space-y-4 p-4 border rounded-lg">
      <h2 className="text-xl font-semibold">Create Wallet</h2>
      
      {error && <div className="text-red-500 text-sm">{error}</div>}
      {success && <div className="text-green-500 text-sm">{success}</div>}
      
      <div className="space-y-2">
        <input
          type="password"
          placeholder="Enter encryption key"
          value={encryptKey}
          onChange={(e) => setEncryptKey(e.target.value)}
          className="w-full p-2 border rounded"
        />
        
        <button
          onClick={handleCreateWallet}
          disabled={isProcessing || !encryptKey}
          className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50 w-full"
        >
          {isProcessing ? "Creating..." : "Create Wallet"}
        </button>
      </div>
    </div>
  );
}

Implementing Transfer Flow

Here’s an example of implementing a transfer flow with Clerk authentication:
// app/components/Transfer.tsx
"use client";

import { useState } from "react";
import { useUser } from "@clerk/nextjs";
import { useTransfer } from "@chipi-pay/chipi-sdk";

const USDC_CONTRACT = "0xA0b86a33E6441b8c4C8C8C8C8C8C8C8C8C8C8C8C"; // Replace with actual USDC contract

export default function Transfer() {
  const { user } = useUser();
  const { transferAsync } = useTransfer();
  const [amount, setAmount] = useState("");
  const [recipientAddress, setRecipientAddress] = useState("");
  const [encryptKey, setEncryptKey] = useState("");
  const [isProcessing, setIsProcessing] = useState(false);
  const [error, setError] = useState("");
  const [success, setSuccess] = useState("");

  const handleTransfer = async () => {
    if (!amount || !recipientAddress || !encryptKey) {
      setError("Please fill in all fields");
      return;
    }

    try {
      setIsProcessing(true);
      setError("");
      setSuccess("");

      // Get stored wallet from Clerk's public metadata
      const storedWallet = user?.publicMetadata?.wallet;
      if (!storedWallet) {
        setError("No wallet found. Please create a wallet first.");
        return;
      }

      // Get the bearer token
      const token = await user?.getToken({ template: "chipi-sdk" });
      if (!token) {
        setError("No bearer token found");
        return;
      }

      const transferResponse = await transferAsync({
        encryptKey: encryptKey,
        bearerToken: token,
        wallet: JSON.parse(storedWallet as string),
        contractAddress: USDC_CONTRACT,
        recipient: recipientAddress,
        amount: amount,
        decimals: 6,
      });

      console.log("transfer response", transferResponse);
      setSuccess("Transfer completed successfully!");
      
      // Clear form
      setAmount("");
      setRecipientAddress("");
      setEncryptKey("");
    } catch (error) {
      setError(error.message || "Transfer failed");
      console.error("Transfer error:", error);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <div className="space-y-4 p-4 border rounded-lg">
      <h2 className="text-xl font-semibold">Transfer USDC</h2>
      
      {error && <div className="text-red-500 text-sm">{error}</div>}
      {success && <div className="text-green-500 text-sm">{success}</div>}
      
      <div className="space-y-2">
        <input
          type="text"
          placeholder="Recipient Address"
          value={recipientAddress}
          onChange={(e) => setRecipientAddress(e.target.value)}
          className="w-full p-2 border rounded"
        />
        
        <input
          type="number"
          placeholder="Amount"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          className="w-full p-2 border rounded"
        />
        
        <input
          type="password"
          placeholder="Encryption Key"
          value={encryptKey}
          onChange={(e) => setEncryptKey(e.target.value)}
          className="w-full p-2 border rounded"
        />
        
        <button
          onClick={handleTransfer}
          disabled={isProcessing || !amount || !recipientAddress || !encryptKey}
          className="bg-green-500 text-white px-4 py-2 rounded disabled:opacity-50 w-full"
        >
          {isProcessing ? "Processing..." : "Make Transfer"}
        </button>
      </div>
    </div>
  );
}

Main Page Implementation

// app/page.tsx
"use client";

import { useUser } from "@clerk/nextjs";
import CreateWallet from "./components/CreateWallet";
import Transfer from "./components/Transfer";

export default function Home() {
  const { user, isSignedIn } = useUser();

  if (!isSignedIn) {
    return (
      <div className="p-5">
        <h1 className="text-2xl font-bold mb-4">Chipi Pay Demo</h1>
        <p>Please sign in to continue.</p>
      </div>
    );
  }

  return (
    <div className="p-5 max-w-2xl mx-auto">
      <h1 className="text-2xl font-bold mb-6">Chipi Pay Demo</h1>
      <p className="mb-4">Welcome, {user?.firstName}!</p>
      
      <div className="space-y-6">
        <CreateWallet />
        <Transfer />
      </div>
    </div>
  );
}

Next Steps

  • Add error handling and loading states
  • Customize the UI to match your app’s design
  • Implement additional features like wallet balance checking
  • Add proper validation for addresses and amounts
Need help? Check out our Telegram Community for support and to connect with other developers.