mirror of
https://github.com/waku-org/waku-lab.git
synced 2025-03-03 08:50:30 +00:00
merge with master
This commit is contained in:
commit
db65143aca
@ -23,14 +23,6 @@
|
||||
browser: true
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
const path = window.location.pathname;
|
||||
if (path && !window.location.search.includes('?/')) {
|
||||
window.location.replace('/?/' + path + window.location.search + window.location.hash);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
20125
examples/buddybook/package-lock.json
generated
20125
examples/buddybook/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,7 @@
|
||||
"@types/node": "^22.7.6",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@waku/interfaces": "^0.0.29-5674b0e.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>BuddyBook</title>
|
||||
<script type="text/javascript">
|
||||
var pathSegmentsToKeep = 0;
|
||||
|
||||
var l = window.location;
|
||||
l.replace(
|
||||
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
|
||||
'/?/' +
|
||||
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
|
||||
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
||||
l.hash
|
||||
);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
@ -12,9 +12,12 @@ import { BlockPayload, getMessagesFromStore, subscribeToFilter } from './lib/wak
|
||||
import TelemetryOptIn from './components/TelemetryOptIn';
|
||||
import TelemetryPage from './components/TelemetryPage';
|
||||
import SignSharedChain from './components/Chain/SignSharedChain'
|
||||
import ConnectionStatus from '@/components/ConnectionStatus';
|
||||
|
||||
type Status = 'success' | 'in-progress' | 'error';
|
||||
|
||||
|
||||
|
||||
interface WakuStatus {
|
||||
filter: Status;
|
||||
store: Status;
|
||||
@ -29,6 +32,9 @@ function App() {
|
||||
filter: 'in-progress',
|
||||
store: 'in-progress',
|
||||
});
|
||||
|
||||
(global.window as any).waku = node;
|
||||
|
||||
const [telemetryOptIn, setTelemetryOptIn] = useState<boolean | null>(null);
|
||||
const [isLoadingChains, setIsLoadingChains] = useState(true);
|
||||
|
||||
@ -40,9 +46,22 @@ function App() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) return;
|
||||
if (isWakuLoading || !node || node.libp2p.getConnections().length === 0 || chainsData.length > 0 || isListening) {
|
||||
console.log("not starting message listening");
|
||||
console.log({
|
||||
isWakuLoading,
|
||||
node,
|
||||
connections: node?.libp2p.getConnections().length,
|
||||
chainsData,
|
||||
isListening
|
||||
})
|
||||
return;
|
||||
}
|
||||
setIsListening(true);
|
||||
console.log("connections", node.libp2p.getConnections().length)
|
||||
setTimeout(() => {
|
||||
startMessageListening();
|
||||
}, 2000);
|
||||
}, [node, isWakuLoading, wakuStatus])
|
||||
|
||||
const handleTelemetryOptIn = (optIn: boolean) => {
|
||||
@ -66,12 +85,23 @@ function App() {
|
||||
|
||||
const startMessageListening = async () => {
|
||||
console.log("Starting message listening")
|
||||
console.log("connections", node.libp2p.getConnections().length)
|
||||
|
||||
// Add timeout for store query
|
||||
const STORE_TIMEOUT = 30000; // 30 seconds
|
||||
const storeTimeout = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Store query timeout')), STORE_TIMEOUT);
|
||||
});
|
||||
|
||||
try {
|
||||
setWakuStatus(prev => ({ ...prev, store: 'in-progress' }));
|
||||
setIsLoadingChains(true);
|
||||
const messageGenerator = getMessagesFromStore(node as LightNode);
|
||||
|
||||
try {
|
||||
// Race between store query and timeout
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
for await (const message of messageGenerator) {
|
||||
setChainsData(prevChains => {
|
||||
const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID);
|
||||
@ -79,12 +109,14 @@ function App() {
|
||||
return [...prevChains, message];
|
||||
});
|
||||
}
|
||||
})(),
|
||||
storeTimeout
|
||||
]);
|
||||
setWakuStatus(prev => ({ ...prev, store: 'success' }));
|
||||
} catch (error) {
|
||||
console.error("Error processing message:", error);
|
||||
// Continue processing other messages
|
||||
setWakuStatus(prev => ({ ...prev, store: 'error' }));
|
||||
}
|
||||
|
||||
setWakuStatus(prev => ({ ...prev, store: 'success' }));
|
||||
} catch (error) {
|
||||
console.error("Error fetching messages from store:", error);
|
||||
setWakuStatus(prev => ({ ...prev, store: 'error' }));
|
||||
@ -92,11 +124,21 @@ function App() {
|
||||
setIsLoadingChains(false);
|
||||
}
|
||||
|
||||
// Add timeout for filter subscription
|
||||
const FILTER_TIMEOUT = 15000; // 15 seconds
|
||||
try {
|
||||
setWakuStatus(prev => ({ ...prev, filter: 'in-progress' }));
|
||||
await subscribeToFilter(node as LightNode, (message) => {
|
||||
handleChainUpdate(message); // Use the same function for both updates
|
||||
})
|
||||
const filterPromise = subscribeToFilter(node as LightNode, (message) => {
|
||||
handleChainUpdate(message);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
filterPromise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Filter subscription timeout')), FILTER_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
|
||||
setWakuStatus(prev => ({ ...prev, filter: 'success' }));
|
||||
} catch (error) {
|
||||
console.error("Error subscribing to filter:", error);
|
||||
@ -132,6 +174,9 @@ function App() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<Header wakuStatus={wakuStatus} />
|
||||
<div className="md:hidden">
|
||||
<ConnectionStatus filter={wakuStatus.filter} store={wakuStatus.store} />
|
||||
</div>
|
||||
<main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl">
|
||||
<Routes>
|
||||
<Route path="" element={<Home />} />
|
||||
|
@ -7,13 +7,13 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import QRCode from '@/components/QRCode';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useWalletPrompt } from '@/hooks/useWalletPrompt';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { fromLightPush, Telemetry } from '@/lib/telemetry';
|
||||
|
||||
interface SignChainProps {
|
||||
block: BlockPayload;
|
||||
chainsData: BlockPayload[]; // Add this prop
|
||||
chainsData: BlockPayload[];
|
||||
onSuccess: (newBlock: BlockPayload) => void;
|
||||
}
|
||||
|
||||
@ -22,42 +22,49 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
|
||||
const [isSigning, setIsSigning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [alreadySigned, setAlreadySigned] = useState(false);
|
||||
const [isWalletPrompt, setIsWalletPrompt] = useState(false);
|
||||
|
||||
const { address } = useAccount();
|
||||
const { data: ensName } = useEnsName({ address });
|
||||
const { node } = useWaku<LightNode>();
|
||||
const { ensureWalletConnected } = useWalletPrompt();
|
||||
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
// Check if the address has signed this block or any blocks in the chain
|
||||
const checkSignatures = (blockToCheck: BlockPayload): boolean => {
|
||||
// Check current block's signatures
|
||||
if (blockToCheck.signatures.some(
|
||||
sig => sig.address.toLowerCase() === address.toLowerCase()
|
||||
)) {
|
||||
const checkSignatures = (blockToCheck: BlockPayload, visitedBlocks: Set<string>): boolean => {
|
||||
if (visitedBlocks.has(blockToCheck.blockUUID)) return false;
|
||||
visitedBlocks.add(blockToCheck.blockUUID);
|
||||
|
||||
// Check current block signatures
|
||||
if (blockToCheck.signatures?.some(sig => sig?.address?.toLowerCase() === address?.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check parent blocks
|
||||
// Check parent block
|
||||
const parentBlock = chainsData.find(b => b.blockUUID === blockToCheck.parentBlockUUID);
|
||||
if (parentBlock && checkSignatures(parentBlock)) {
|
||||
if (parentBlock && checkSignatures(parentBlock, visitedBlocks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check child blocks
|
||||
const childBlocks = chainsData.filter(b => b.parentBlockUUID === blockToCheck.blockUUID);
|
||||
return childBlocks.some(childBlock => checkSignatures(childBlock));
|
||||
// Check immediate child blocks
|
||||
return chainsData
|
||||
.filter(b => b.parentBlockUUID === blockToCheck.blockUUID)
|
||||
.some(childBlock => checkSignatures(childBlock, visitedBlocks));
|
||||
};
|
||||
|
||||
const hasAlreadySigned = checkSignatures(block);
|
||||
setAlreadySigned(hasAlreadySigned);
|
||||
useEffect(() => {
|
||||
if (!address) return;
|
||||
|
||||
try {
|
||||
const visitedBlocks = new Set<string>();
|
||||
setAlreadySigned(checkSignatures(block, visitedBlocks));
|
||||
} catch (error) {
|
||||
console.error('Error in signature check:', error);
|
||||
setAlreadySigned(false);
|
||||
}
|
||||
}, [address, block, chainsData]);
|
||||
|
||||
const { signMessage } = useSignMessage({
|
||||
mutation: {
|
||||
onMutate() {
|
||||
// Reset any previous errors when starting a new signing attempt
|
||||
setError(null);
|
||||
setIsSigning(true);
|
||||
},
|
||||
@ -65,7 +72,6 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
|
||||
if (!address || !node) return;
|
||||
|
||||
try {
|
||||
// Double check signature before proceeding
|
||||
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
|
||||
setError('You have already signed this chain.');
|
||||
return;
|
||||
@ -121,36 +127,57 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
|
||||
const handleSign = async () => {
|
||||
try {
|
||||
if (!address) {
|
||||
// If not connected, try to connect first
|
||||
setIsWalletPrompt(true);
|
||||
const connected = await ensureWalletConnected();
|
||||
if (!connected) return;
|
||||
if (!connected) {
|
||||
setError('Please ensure your wallet is connected and the app is open.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already signed
|
||||
if (alreadySigned) {
|
||||
setError('You have already signed this chain.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the message
|
||||
const message = `Sign Block:
|
||||
Chain UUID: ${block.chainUUID}
|
||||
Block UUID: ${block.blockUUID}
|
||||
Title: ${block.title}
|
||||
Description: ${block.description}
|
||||
Timestamp: ${new Date().getTime()}
|
||||
Parent Block UUID: ${block.parentBlockUUID}
|
||||
Signed by: ${ensName || address}`;
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
if (isMobile && typeof window.ethereum === 'undefined') {
|
||||
setError('Please ensure your wallet app is installed and open before signing.');
|
||||
window.location.href = 'metamask:///';
|
||||
return;
|
||||
}
|
||||
|
||||
const message = [
|
||||
'Sign Block:',
|
||||
`Chain UUID: ${block.chainUUID}`,
|
||||
`Block UUID: ${block.blockUUID}`,
|
||||
`Title: ${block.title}`,
|
||||
`Description: ${block.description}`,
|
||||
`Timestamp: ${new Date().getTime()}`,
|
||||
`Parent Block UUID: ${block.parentBlockUUID}`,
|
||||
`Signed by: ${ensName || address}`
|
||||
].join('\n');
|
||||
|
||||
// Trigger signing
|
||||
signMessage({ message });
|
||||
} catch (error) {
|
||||
console.error('Error in sign flow:', error);
|
||||
setError('Failed to initiate signing. Please try again.');
|
||||
setError('Failed to initiate signing. Please ensure your wallet app is open and try again.');
|
||||
setIsSigning(false);
|
||||
} finally {
|
||||
setIsWalletPrompt(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonText = () => {
|
||||
if (isSigning) return 'Signing...';
|
||||
if (isWalletPrompt) return 'Connecting...';
|
||||
if (alreadySigned) return 'Already Signed';
|
||||
if (!address) return 'Connect Wallet';
|
||||
return 'Sign';
|
||||
};
|
||||
|
||||
const showLoadingSpinner = isSigning || isWalletPrompt;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
|
||||
@ -174,22 +201,27 @@ Signed by: ${ensName || address}`;
|
||||
</div>
|
||||
<QRCode text={`${window.location.origin}/sign/${block.chainUUID}/${block.blockUUID}`} />
|
||||
</div>
|
||||
{(error || isWalletPrompt) && (
|
||||
<div className="space-y-2">
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{isWalletPrompt && (
|
||||
<div className="rounded-md bg-blue-50 p-4">
|
||||
<p className="text-sm text-blue-700">Attempting to connect to your wallet...</p>
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
If your wallet doesn't open automatically, please open it manually to approve the connection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSign} disabled={isSigning || alreadySigned}>
|
||||
{isSigning ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing...
|
||||
</>
|
||||
) : alreadySigned ? (
|
||||
'Already Signed'
|
||||
) : !address ? (
|
||||
'Connect Wallet'
|
||||
) : (
|
||||
'Sign'
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSign}
|
||||
disabled={isSigning || alreadySigned || isWalletPrompt}
|
||||
>
|
||||
{showLoadingSpinner && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
@ -4,7 +4,7 @@ import { type BlockPayload } from '@/lib/waku';
|
||||
import SignChain from '@/components/Chain/SignChain';
|
||||
import { useEnsName } from 'wagmi';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from "@/components/ui/dialog";
|
||||
import QRCode from '@/components/QRCode';
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
@ -64,22 +64,23 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Share</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="flex flex-col gap-4">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share this Book</DialogTitle>
|
||||
<DialogDescription>
|
||||
Share this book with others to collect their signatures.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<QRCode text={shareUrl} width={200} height={200} />
|
||||
<p className="text-sm text-center break-all">{shareUrl}</p>
|
||||
<Button
|
||||
onClick={() => navigator.clipboard.writeText(shareUrl)}
|
||||
variant="outline"
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<QRCode
|
||||
text={shareUrl}
|
||||
width={180}
|
||||
height={180}
|
||||
showCopyButton="text"
|
||||
title={block.title}
|
||||
description={`Sign this chain: ${block.title}`}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
54
examples/buddybook/src/components/ConnectionStatus.tsx
Normal file
54
examples/buddybook/src/components/ConnectionStatus.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Loader2, CheckCircle2, XCircle } from "lucide-react";
|
||||
|
||||
type Status = 'success' | 'error' | 'in-progress';
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
filter: Status;
|
||||
store: Status;
|
||||
}
|
||||
|
||||
const StatusIndicator: React.FC<{ status: Status; label: string }> = ({ status, label }) => {
|
||||
const getStatusIcon = () => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||
case 'error':
|
||||
return <XCircle className="h-4 w-4 text-red-500" />;
|
||||
case 'in-progress':
|
||||
return <Loader2 className="h-4 w-4 animate-spin text-yellow-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'bg-green-500/10 text-green-500';
|
||||
case 'error':
|
||||
return 'bg-red-500/10 text-red-500';
|
||||
case 'in-progress':
|
||||
return 'bg-yellow-500/10 text-yellow-500';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-2 px-2 py-1 rounded-full ${getStatusColor()}`}>
|
||||
{getStatusIcon()}
|
||||
<span className="text-xs font-medium">{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ filter, store }) => {
|
||||
return (
|
||||
<Card className="fixed bottom-4 left-4 right-4 md:static md:bottom-auto md:left-auto p-2 bg-background/80 backdrop-blur-sm border shadow-lg z-50 md:z-auto">
|
||||
<div className="flex flex-row justify-around md:justify-start md:gap-4">
|
||||
<StatusIndicator status={filter} label="Filter" />
|
||||
<StatusIndicator status={store} label="Store" />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatus;
|
@ -82,30 +82,32 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{!isWakuLoading && !wakuError && (
|
||||
<>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-muted-foreground">Filter:</span>
|
||||
<span className="hidden md:inline text-muted-foreground">Filter:</span>
|
||||
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-muted-foreground">Store:</span>
|
||||
<span className="hidden md:inline text-muted-foreground">Store:</span>
|
||||
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="hidden md:inline text-muted-foreground">Peers:</span>
|
||||
{isWakuLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : wakuError ? (
|
||||
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full bg-red-500`} />
|
||||
) : (
|
||||
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isWakuLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : wakuError ? (
|
||||
<span className="text-xs text-red-500">Error</span>
|
||||
) : (
|
||||
<div className={`w-2 h-2 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
|
||||
)}
|
||||
|
||||
{isConnected ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs md:text-sm text-muted-foreground truncate max-w-[80px] md:max-w-[120px]">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
@ -7,11 +7,37 @@ interface QRCodeProps {
|
||||
text: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
showCopyButton?: 'icon' | 'text' | 'both';
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
|
||||
const QRCode: React.FC<QRCodeProps> = ({
|
||||
text,
|
||||
width = 256,
|
||||
height = 256,
|
||||
showCopyButton = 'both',
|
||||
title,
|
||||
description
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [qrSize, setQrSize] = useState(Math.min(width, height));
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 640);
|
||||
setQrSize(
|
||||
window.innerWidth < 640
|
||||
? Math.min(window.innerWidth - 80, 200)
|
||||
: Math.min(width, height)
|
||||
);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, [width, height]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@ -19,13 +45,36 @@ const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: title || 'Share Chain',
|
||||
text: description || 'Sign this chain',
|
||||
url: text
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
console.error('Error sharing:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleCopy();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex flex-col items-center w-full space-y-4">
|
||||
<div className="flex justify-center w-full">
|
||||
<QRCodeSVG
|
||||
value={text}
|
||||
size={isMobile ? Math.min(width * 0.8, window.innerWidth - 64) : Math.min(width, height)}
|
||||
size={qrSize}
|
||||
className="max-w-full"
|
||||
/>
|
||||
<div className="flex items-center space-x-2 w-full max-w-[300px]">
|
||||
</div>
|
||||
|
||||
{showCopyButton !== 'text' && (
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
@ -41,6 +90,21 @@ const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
|
||||
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCopyButton === 'text' && (
|
||||
<Button
|
||||
onClick={isMobile && 'share' in navigator ? handleShare : handleCopy}
|
||||
variant="secondary"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isMobile && 'share' in navigator
|
||||
? 'Share'
|
||||
: copied
|
||||
? 'Copied!'
|
||||
: 'Copy Link'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@ -36,9 +35,12 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200",
|
||||
"rounded-lg",
|
||||
"max-h-[85vh] overflow-y-auto",
|
||||
"fixed z-50 border bg-background shadow-lg duration-200",
|
||||
"p-4 md:p-6",
|
||||
"w-full md:w-[calc(100%-2rem)] md:max-w-lg",
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
|
||||
"bottom-0 rounded-t-lg md:rounded-lg",
|
||||
"md:left-[50%] md:top-[50%] md:translate-x-[-50%] md:translate-y-[-50%]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -7,16 +7,33 @@ export function useWalletPrompt() {
|
||||
const ensureWalletConnected = async () => {
|
||||
if (!isConnected) {
|
||||
try {
|
||||
// Find the first available connector (usually injected/metamask)
|
||||
const connector = connectors[0]
|
||||
if (connector) {
|
||||
await connect({ connector })
|
||||
// Check if we're on iOS
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
|
||||
// If on iOS, try to open MetaMask first
|
||||
if (isIOS) {
|
||||
// Attempt to open MetaMask
|
||||
window.location.href = 'metamask:///'
|
||||
|
||||
// Give a small delay before attempting connection
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
await connect({ connector })
|
||||
|
||||
// Wait a brief moment for the connection to be established
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error connecting wallet:', error)
|
||||
// If connection fails, try to open MetaMask directly
|
||||
if (typeof window.ethereum === 'undefined') {
|
||||
// Redirect to MetaMask download page if not installed
|
||||
window.open('https://metamask.io/download/', '_blank')
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export type BlockPayload = {
|
||||
parentBlockUUID: string | null;
|
||||
}
|
||||
|
||||
export const contentTopic = "/buddybook-dogfood/1/chain/proto";
|
||||
const contentTopic = "/buddybook-devcon/1/chain/proto";
|
||||
|
||||
export const encoder = createEncoder({
|
||||
contentTopic: contentTopic,
|
||||
|
@ -9,7 +9,7 @@ import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { LightNodeProvider } from "@waku/react";
|
||||
import { config } from './lib/walletConnect.ts'
|
||||
import { WAKU_NODE_OPTIONS } from './lib/waku-config.ts'
|
||||
import { WAKU_NODE_OPTIONS } from './lib/waku.ts'
|
||||
|
||||
// Polyfills
|
||||
if (typeof global === 'undefined') {
|
||||
|
Loading…
x
Reference in New Issue
Block a user