Next.js
Integrate XRPL-Connect into your Next.js application with the App Router.
Installation
npm install xrpl-connect xrplBasic Setup with App Router
Create a client component for wallet functionality:
// app/components/WalletConnector.tsx
'use client';
import { useEffect, useRef, useState } from 'react';
import { WalletManager,XamanAdapter,CrossmarkAdapter } from 'xrpl-connect';
import type { Account, WalletError } from 'xrpl-connect';
export default function WalletConnector() {
const connectorRef = useRef<HTMLElement>(null);
const [walletManager, setWalletManager] = useState<WalletManager | null>(null);
const [account, setAccount] = useState<Account | null>(null);
const [error, setError] = useState<WalletError | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const manager = new WalletManager({
adapters: [
new XamanAdapter({ apiKey: process.env.NEXT_PUBLIC_XAMAN_API_KEY || '' }),
new CrossmarkAdapter(),
],
network: 'testnet',
autoConnect: true,
});
manager.on('connect', (acc: Account) => {
setAccount(acc);
setError(null);
});
manager.on('disconnect', () => {
setAccount(null);
});
manager.on('error', (err: WalletError) => {
setError(err);
});
setWalletManager(manager);
setLoading(false);
if (connectorRef.current) {
connectorRef.current.setWalletManager(manager);
}
return () => {
manager.disconnect();
};
}, []);
const handleDisconnect = async () => {
if (walletManager) {
await walletManager.disconnect();
}
};
if (loading) {
return <div>Loading wallet...</div>;
}
return (
<div>
<xrpl-wallet-connector
ref={connectorRef}
primary-wallet="xaman"
/>
{error && (
<div style={{ color: 'red', marginTop: '20px' }}>
Error: {error.message}
</div>
)}
{account && (
<div style={{ marginTop: '20px', padding: '15px', background: '#f5f5f5', borderRadius: '8px' }}>
<h3>Connected Account</h3>
<p><strong>Address:</strong> {account.address}</p>
<p><strong>Network:</strong> {account.network.name}</p>
<button onClick={handleDisconnect}>Disconnect</button>
</div>
)}
</div>
);
}Environment Variables
Create a .env.local file with your API keys:
NEXT_PUBLIC_XAMAN_API_KEY=your_api_key_here
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_hereNote: Variables prefixed with
NEXT_PUBLIC_are exposed to the browser. Never expose secrets here.
Custom Hook
Create a reusable hook for wallet management:
// hooks/useWallet.ts
'use client';
import { useEffect, useRef, useState } from 'react';
import { WalletManager } from 'xrpl-connect';
import type { Account, WalletError, WalletAdapter } from 'xrpl-connect';
interface UseWalletOptions {
adapters: WalletAdapter[];
network?: 'mainnet' | 'testnet' | 'devnet';
}
export function useWallet({ adapters, network = 'testnet' }: UseWalletOptions) {
const connectorRef = useRef<HTMLElement>(null);
const [walletManager, setWalletManager] = useState<WalletManager | null>(null);
const [account, setAccount] = useState<Account | null>(null);
const [connected, setConnected] = useState(false);
const [error, setError] = useState<WalletError | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const manager = new WalletManager({
adapters,
network,
autoConnect: true,
});
manager.on('connect', (acc: Account) => {
setAccount(acc);
setConnected(true);
setError(null);
});
manager.on('disconnect', () => {
setAccount(null);
setConnected(false);
});
manager.on('error', (err: WalletError) => {
setError(err);
});
setWalletManager(manager);
setLoading(false);
if (connectorRef.current) {
connectorRef.current.setWalletManager(manager);
}
return () => {
manager.disconnect();
};
}, [adapters, network]);
const disconnect = async () => {
if (walletManager) {
await walletManager.disconnect();
}
};
return {
walletManager,
account,
connected,
error,
loading,
connectorRef,
disconnect,
};
}Usage:
// app/page.tsx
'use client';
import { useWallet } from '@/hooks/useWallet';
import { XamanAdapter } from 'xrpl-connect';
export default function Home() {
const { account, connected, connectorRef, disconnect } = useWallet({
adapters: [
new XamanAdapter({ apiKey: process.env.NEXT_PUBLIC_XAMAN_API_KEY || '' }),
],
});
return (
<main>
<xrpl-wallet-connector ref={connectorRef} />
{connected && <button onClick={disconnect}>Disconnect</button>}
</main>
);
}Context Provider
Create a provider for global wallet state:
// app/providers/WalletProvider.tsx
'use client';
import { createContext, useContext, ReactNode } from 'react';
import { useWallet } from '@/hooks/useWallet';
import { XamanAdapte,CrossmarkAdapter } from 'xrpl-connect';
import type { Account, WalletManager, WalletError } from 'xrpl-connect';
interface WalletContextType {
walletManager: WalletManager | null;
account: Account | null;
connected: boolean;
error: WalletError | null;
disconnect: () => Promise<void>;
connectorRef: React.RefObject<HTMLElement>;
}
const WalletContext = createContext<WalletContextType | undefined>(undefined);
export function WalletProvider({ children }: { children: ReactNode }) {
const wallet = useWallet({
adapters: [
new XamanAdapter({ apiKey: process.env.NEXT_PUBLIC_XAMAN_API_KEY || '' }),
new CrossmarkAdapter(),
],
});
return (
<WalletContext.Provider value={wallet}>
{children}
</WalletContext.Provider>
);
}
export function useProvidedWallet() {
const context = useContext(WalletContext);
if (!context) {
throw new Error('useProvidedWallet must be used within WalletProvider');
}
return context;
}Wrap your app with the provider:
// app/layout.tsx
import { WalletProvider } from './providers/WalletProvider';
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<WalletProvider>
{children}
</WalletProvider>
</body>
</html>
);
}Signing Transactions
Example transaction handler:
// app/components/PaymentForm.tsx
'use client';
import { useState } from 'react';
import { useProvidedWallet } from '@/app/providers/WalletProvider';
export default function PaymentForm() {
const { walletManager, connected } = useProvidedWallet();
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const handlePayment = async () => {
if (!walletManager?.connected) {
setError('Wallet not connected');
return;
}
setLoading(true);
setError(null);
try {
const txResult = await walletManager.signAndSubmit({
TransactionType: 'Payment',
Account: walletManager.account!.address,
Destination: 'rN7n7otQDd6FczFgLdlqtyMVrn3HMfXoQT',
Amount: '1000000',
});
setResult(txResult);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<button onClick={handlePayment} disabled={loading || !connected}>
{loading ? 'Sending...' : 'Send Payment'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
{result && <p style={{ color: 'green' }}>Success! Hash: {result.hash}</p>}
</div>
);
}API Routes for Backend Integration
Sign transactions on the backend for security:
// app/api/transactions/sign/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { transaction } = await request.json();
// Validate transaction on backend
if (!transaction.Account || !transaction.Destination) {
return NextResponse.json(
{ error: 'Invalid transaction' },
{ status: 400 }
);
}
// You could use xrpl-connect server-side here
// const result = await walletManager.signAndSubmit(transaction);
return NextResponse.json({ success: true });
} catch (error: any) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}Handling SSR Issues
The xrpl-wallet-connector web component is browser-only. Handle SSR with dynamic imports:
// app/components/ClientWalletConnector.tsx
import dynamic from 'next/dynamic';
const WalletConnector = dynamic(
() => import('./WalletConnector'),
{ ssr: false }
);
export default WalletConnector;Error Handling and Loading States
// app/components/WalletSection.tsx
'use client';
import { Suspense } from 'react';
import { useProvidedWallet } from '@/app/providers/WalletProvider';
function WalletContent() {
const { account, error, loading } = useProvidedWallet();
if (loading) {
return <div>Loading wallet...</div>;
}
if (error) {
return (
<div className="error">
Failed to connect: {error.message}
</div>
);
}
return (
<div>
{account ? (
<p>Connected: {account.address}</p>
) : (
<p>Click above to connect wallet</p>
)}
</div>
);
}
export default function WalletSection() {
return (
<Suspense fallback={<div>Loading...</div>}>
<WalletContent />
</Suspense>
);
}Middleware for Protected Routes
Protect routes that require a connected wallet:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// This runs on the server, so we can't directly check wallet connection
// Instead, use client-side guards in your components
return NextResponse.next();
}
export const config = {
matcher: ['/protected/:path*'],
};Protect routes client-side:
// app/protected/layout.tsx
'use client';
import { ReactNode } from 'react';
import { useProvidedWallet } from '@/app/providers/WalletProvider';
import { redirect } from 'next/navigation';
export default function ProtectedLayout({ children }: { children: ReactNode }) {
const { connected, loading } = useProvidedWallet();
if (loading) {
return <div>Loading...</div>;
}
if (!connected) {
redirect('/');
}
return <>{children}</>;
}Best Practices
Use
'use client'- Mark wallet components as client componentsEnvironment Variables - Keep API keys in
.env.localDynamic Imports - Use
dynamic()for browser-only componentsError Handling - Always catch and handle wallet errors
Loading States - Show loading indicators during initialization
Type Safety - Use TypeScript for type checking
Context Provider - Use Context for global wallet state
API Routes - Consider moving sensitive operations to API routes
Testing
Example test with Vitest:
import { render, screen } from '@testing-library/react';
import { WalletProvider } from '@/app/providers/WalletProvider';
import WalletConnector from '@/app/components/WalletConnector';
describe('WalletConnector', () => {
it('should render wallet connector', () => {
render(
<WalletProvider>
<WalletConnector />
</WalletProvider>
);
expect(screen.getByText(/Loading wallet/i)).toBeInTheDocument();
});
});