Learn how to integrate Chipi Pay with your Next.js application using Clerk for authentication and secure wallet storage.
npx create-next-app@latest my-chipi-app
cd my-chipi-app
# Install Chipi Pay SDK
npm install @chipi-pay/chipi-sdk
# Install Clerk
npm install @clerk/nextjs
.env
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_PUBLIC_KEY=your_chipi_api_public_key
"use client";
import { ChipiProvider } from "@chipi-pay/chipi-sdk";
const API_PUBLIC_KEY = process.env.NEXT_PUBLIC_CHIPI_PUBLIC_KEY
if (!API_PUBLIC_KEY) throw new Error("API_PUBLIC_KEY is not set");
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ChipiProvider
config={{
apiPublicKey: API_PUBLIC_KEY,
}}
>
{children}
</ChipiProvider>
);
}
// 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>
)
}
// 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>
);
}
// 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);
}
// 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 = "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8";
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>
);
}