mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 19:27:56 +00:00
feat: add WalletConnect v2 integration for mobile wallet connection
- Add @walletconnect/sign-client for dApp-side WC v2 support - Create walletconnect-service.ts with session management and Signer adapter - Create WalletConnectModal.tsx with QR code display - Update PezkuwiContext with connectWalletConnect(), session restore, walletSource tracking - Update WalletContext signing router: mobile → WalletConnect → extension - Update WalletModal with "Connect with pezWallet (Mobile)" button - Move WalletConnect projectId to env variable - Fix vite build: disable assetsInclude for JSON (crypto-browserify compat)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Smartphone, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
|
||||
interface WalletConnectModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type ConnectionState = 'generating' | 'waiting' | 'connected' | 'error';
|
||||
|
||||
export const WalletConnectModal: React.FC<WalletConnectModalProps> = ({ isOpen, onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { connectWalletConnect, selectedAccount, wcPeerName } = usePezkuwi();
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string>('');
|
||||
const [connectionState, setConnectionState] = useState<ConnectionState>('generating');
|
||||
const [errorMsg, setErrorMsg] = useState<string>('');
|
||||
|
||||
const startConnection = useCallback(async () => {
|
||||
setConnectionState('generating');
|
||||
setErrorMsg('');
|
||||
|
||||
try {
|
||||
const uri = await connectWalletConnect();
|
||||
|
||||
// Generate QR code as data URL
|
||||
const dataUrl = await QRCode.toDataURL(uri, {
|
||||
width: 300,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#ffffff',
|
||||
},
|
||||
});
|
||||
|
||||
setQrDataUrl(dataUrl);
|
||||
setConnectionState('waiting');
|
||||
} catch (err) {
|
||||
setConnectionState('error');
|
||||
setErrorMsg(err instanceof Error ? err.message : 'Connection failed');
|
||||
}
|
||||
}, [connectWalletConnect]);
|
||||
|
||||
// Start connection when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
startConnection();
|
||||
}
|
||||
|
||||
return () => {
|
||||
setQrDataUrl('');
|
||||
setConnectionState('generating');
|
||||
};
|
||||
}, [isOpen, startConnection]);
|
||||
|
||||
// Listen for successful connection
|
||||
useEffect(() => {
|
||||
const handleConnected = () => {
|
||||
setConnectionState('connected');
|
||||
// Auto-close after brief success display
|
||||
setTimeout(() => onClose(), 1500);
|
||||
};
|
||||
|
||||
window.addEventListener('walletconnect_connected', handleConnected);
|
||||
return () => window.removeEventListener('walletconnect_connected', handleConnected);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Smartphone className="h-5 w-5 text-purple-400" />
|
||||
WalletConnect
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('walletModal.wcScanQR', 'Scan with pezWallet to connect')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col items-center gap-4 py-4">
|
||||
{/* Generating state */}
|
||||
{connectionState === 'generating' && (
|
||||
<div className="flex flex-col items-center gap-3 py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-400" />
|
||||
<p className="text-sm text-gray-400">
|
||||
{t('walletModal.wcGenerating', 'Generating QR code...')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* QR Code display - waiting for scan */}
|
||||
{connectionState === 'waiting' && qrDataUrl && (
|
||||
<>
|
||||
<div className="bg-white rounded-xl p-3">
|
||||
<img
|
||||
src={qrDataUrl}
|
||||
alt="WalletConnect QR Code"
|
||||
className="w-[280px] h-[280px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t('walletModal.wcWaiting', 'Waiting for wallet to connect...')}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center max-w-[280px]">
|
||||
{t('walletModal.wcInstructions', 'Open pezWallet app → Settings → WalletConnect → Scan QR code')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Connected state */}
|
||||
{connectionState === 'connected' && (
|
||||
<div className="flex flex-col items-center gap-3 py-8">
|
||||
<CheckCircle className="h-12 w-12 text-green-500" />
|
||||
<p className="text-lg font-medium text-green-400">
|
||||
{t('walletModal.wcConnected', 'Connected!')}
|
||||
</p>
|
||||
{wcPeerName && (
|
||||
<p className="text-sm text-gray-400">{wcPeerName}</p>
|
||||
)}
|
||||
{selectedAccount && (
|
||||
<code className="text-xs text-gray-500 font-mono">
|
||||
{selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-6)}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{connectionState === 'error' && (
|
||||
<div className="flex flex-col items-center gap-3 py-8">
|
||||
<XCircle className="h-12 w-12 text-red-500" />
|
||||
<p className="text-sm text-red-400">{errorMsg}</p>
|
||||
<Button
|
||||
onClick={startConnection}
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
>
|
||||
{t('walletModal.wcRetry', 'Try Again')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield } from 'lucide-react';
|
||||
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield, Smartphone } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { formatAddress } from '@pezkuwi/lib/wallet';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
import { WalletConnectModal } from './WalletConnectModal';
|
||||
|
||||
interface WalletModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,11 +29,14 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
api,
|
||||
isApiReady,
|
||||
peopleApi,
|
||||
walletSource,
|
||||
wcPeerName,
|
||||
error
|
||||
} = usePezkuwi();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showWCModal, setShowWCModal] = useState(false);
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
@@ -241,8 +245,15 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">{t('walletModal.source')}</div>
|
||||
<div className="text-sm text-gray-300">
|
||||
{selectedAccount.meta.source || 'pezkuwi'}
|
||||
<div className="text-sm text-gray-300 flex items-center gap-2">
|
||||
{walletSource === 'walletconnect' ? (
|
||||
<>
|
||||
<Smartphone className="h-3 w-3 text-purple-400" />
|
||||
WalletConnect{wcPeerName ? ` (${wcPeerName})` : ''}
|
||||
</>
|
||||
) : (
|
||||
selectedAccount.meta.source || 'pezkuwi'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,7 +332,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// No accounts, show connect button
|
||||
// No accounts, show connect buttons
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
@@ -331,6 +342,26 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
{t('walletModal.connectPezkuwi')}
|
||||
</Button>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t border-gray-700" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-gray-500">
|
||||
{t('walletModal.or', 'or')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowWCModal(true)}
|
||||
variant="outline"
|
||||
className="w-full border-purple-500/30 hover:border-purple-500/60 hover:bg-purple-500/10"
|
||||
>
|
||||
<Smartphone className="mr-2 h-4 w-4" />
|
||||
{t('walletModal.connectWC', 'Connect with pezWallet (Mobile)')}
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-gray-400 text-center">
|
||||
{t('walletModal.noWallet')}{' '}
|
||||
<a
|
||||
@@ -347,6 +378,16 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
{/* WalletConnect QR Modal */}
|
||||
<WalletConnectModal
|
||||
isOpen={showWCModal}
|
||||
onClose={() => {
|
||||
setShowWCModal(false);
|
||||
// If connected via WC, close the main modal too
|
||||
if (selectedAccount) onClose();
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user