feat(channels): implement WhatsApp QR login proxy (#29)
This commit is contained in:
committed by
GitHub
Unverified
parent
0cf4ad3a8c
commit
7a9fd7fc0f
@@ -369,6 +369,10 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
||||
setConfigValues({});
|
||||
setChannelName('');
|
||||
setIsExistingConfig(false);
|
||||
setChannelName('');
|
||||
setIsExistingConfig(false);
|
||||
// Ensure we clean up any pending QR session if switching away
|
||||
window.electron.ipcRenderer.invoke('channel:cancelWhatsAppQr').catch(() => { });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -404,6 +408,47 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
||||
return () => { cancelled = true; };
|
||||
}, [selectedType]);
|
||||
|
||||
// Listen for WhatsApp QR events
|
||||
useEffect(() => {
|
||||
if (selectedType !== 'whatsapp') return;
|
||||
|
||||
const onQr = (data: { qr: string; raw: string }) => {
|
||||
setQrCode(`data:image/png;base64,${data.qr}`);
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
toast.success('WhatsApp connected successfully!');
|
||||
// Register the channel locally so it shows up immediately
|
||||
addChannel({
|
||||
type: 'whatsapp',
|
||||
name: channelName || 'WhatsApp',
|
||||
}).then(() => {
|
||||
// Restart gateway to pick up the new session
|
||||
window.electron.ipcRenderer.invoke('gateway:restart').catch(console.error);
|
||||
onChannelAdded();
|
||||
});
|
||||
};
|
||||
|
||||
const onError = (err: string) => {
|
||||
console.error('WhatsApp Login Error:', err);
|
||||
toast.error(`WhatsApp Login Failed: ${err}`);
|
||||
setQrCode(null);
|
||||
setConnecting(false);
|
||||
};
|
||||
|
||||
const removeQrListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-qr', onQr);
|
||||
const removeSuccessListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-success', onSuccess);
|
||||
const removeErrorListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-error', onError);
|
||||
|
||||
return () => {
|
||||
if (typeof removeQrListener === 'function') removeQrListener();
|
||||
if (typeof removeSuccessListener === 'function') removeSuccessListener();
|
||||
if (typeof removeErrorListener === 'function') removeErrorListener();
|
||||
// Cancel when unmounting or switching types
|
||||
window.electron.ipcRenderer.invoke('channel:cancelWhatsAppQr').catch(() => { });
|
||||
};
|
||||
}, [selectedType, addChannel, channelName, onChannelAdded]);
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (!selectedType) return;
|
||||
|
||||
@@ -457,10 +502,9 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
||||
try {
|
||||
// For QR-based channels, request QR code
|
||||
if (meta.connectionType === 'qr') {
|
||||
// Simulate QR code generation (in real implementation, call Gateway)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
setQrCode('placeholder-qr');
|
||||
setConnecting(false);
|
||||
const accountId = channelName.trim() || 'default';
|
||||
await window.electron.ipcRenderer.invoke('channel:requestWhatsAppQr', accountId);
|
||||
// The QR code will be set via event listener
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -625,23 +669,24 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
||||
) : qrCode ? (
|
||||
// QR Code display
|
||||
<div className="text-center space-y-4">
|
||||
<div className="bg-white p-4 rounded-lg inline-block">
|
||||
<div className="w-48 h-48 bg-gray-100 flex items-center justify-center">
|
||||
<QrCode className="h-32 w-32 text-gray-400" />
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg inline-block shadow-sm border">
|
||||
{qrCode.startsWith('data:image') ? (
|
||||
<img src={qrCode} alt="Scan QR Code" className="w-64 h-64 object-contain" />
|
||||
) : (
|
||||
<div className="w-64 h-64 bg-gray-100 flex items-center justify-center">
|
||||
<QrCode className="h-32 w-32 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Scan this QR code with {meta?.name} to connect
|
||||
</p>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button variant="outline" onClick={() => setQrCode(null)}>
|
||||
Generate New Code
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
toast.success('Channel connected successfully');
|
||||
onChannelAdded();
|
||||
<Button variant="outline" onClick={() => {
|
||||
setQrCode(null);
|
||||
handleConnect(); // Retry
|
||||
}}>
|
||||
I've Scanned It
|
||||
Refresh Code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,10 @@ export function Dashboard() {
|
||||
const { channels, fetchChannels } = useChannelsStore();
|
||||
const { skills, fetchSkills } = useSkillsStore();
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
|
||||
|
||||
const isGatewayRunning = gatewayStatus.state === 'running';
|
||||
const [uptime, setUptime] = useState(0);
|
||||
|
||||
|
||||
// Fetch data only when gateway is running
|
||||
useEffect(() => {
|
||||
if (isGatewayRunning) {
|
||||
@@ -39,11 +39,11 @@ export function Dashboard() {
|
||||
fetchSkills();
|
||||
}
|
||||
}, [fetchChannels, fetchSkills, isGatewayRunning]);
|
||||
|
||||
|
||||
// Calculate statistics safely
|
||||
const connectedChannels = Array.isArray(channels) ? channels.filter((c) => c.status === 'connected').length : 0;
|
||||
const enabledSkills = Array.isArray(skills) ? skills.filter((s) => s.enabled).length : 0;
|
||||
|
||||
|
||||
// Update uptime periodically
|
||||
useEffect(() => {
|
||||
const updateUptime = () => {
|
||||
@@ -53,13 +53,13 @@ export function Dashboard() {
|
||||
setUptime(0);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Update immediately
|
||||
updateUptime();
|
||||
|
||||
|
||||
// Update every second
|
||||
const interval = setInterval(updateUptime, 1000);
|
||||
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [gatewayStatus.connectedAt]);
|
||||
|
||||
@@ -79,7 +79,7 @@ export function Dashboard() {
|
||||
console.error('Error opening Dev Console:', err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Status Cards */}
|
||||
@@ -101,7 +101,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Channels */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
@@ -115,7 +115,7 @@ export function Dashboard() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Skills */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
@@ -129,7 +129,7 @@ export function Dashboard() {
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Uptime */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
@@ -146,7 +146,7 @@ export function Dashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -192,7 +192,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{/* Connected Channels */}
|
||||
@@ -221,7 +221,6 @@ export function Dashboard() {
|
||||
{channel.type === 'whatsapp' && '📱'}
|
||||
{channel.type === 'telegram' && '✈️'}
|
||||
{channel.type === 'discord' && '🎮'}
|
||||
{channel.type === 'slack' && '💼'}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium">{channel.name}</p>
|
||||
@@ -237,7 +236,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Enabled Skills */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -284,7 +283,7 @@ function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
} else if (hours > 0) {
|
||||
|
||||
Reference in New Issue
Block a user