From baa5145ff1b95341681349d353eeddd281e2fd94 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Mon, 11 Nov 2024 16:05:14 +0700 Subject: [PATCH 1/5] chore: connection status --- examples/buddybook/src/App.tsx | 4 ++ .../src/components/ConnectionStatus.tsx | 53 +++++++++++++++++++ .../buddybook/src/components/ui/dialog.tsx | 12 +++-- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 examples/buddybook/src/components/ConnectionStatus.tsx diff --git a/examples/buddybook/src/App.tsx b/examples/buddybook/src/App.tsx index 34f8f5d..30b0c29 100644 --- a/examples/buddybook/src/App.tsx +++ b/examples/buddybook/src/App.tsx @@ -12,6 +12,7 @@ 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'; @@ -132,6 +133,9 @@ function App() { return (
+
+ +
} /> diff --git a/examples/buddybook/src/components/ConnectionStatus.tsx b/examples/buddybook/src/components/ConnectionStatus.tsx new file mode 100644 index 0000000..dd1ab32 --- /dev/null +++ b/examples/buddybook/src/components/ConnectionStatus.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Card } from "@/components/ui/card"; +import { Loader2, CheckCircle2, XCircle } from "lucide-react"; +import type { Status } from '@/App'; + +interface ConnectionStatusProps { + filter: Status; + store: Status; +} + +const StatusIndicator: React.FC<{ status: Status; label: string }> = ({ status, label }) => { + const getStatusIcon = () => { + switch (status) { + case 'success': + return ; + case 'error': + return ; + case 'in-progress': + return ; + } + }; + + 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 ( +
+ {getStatusIcon()} + {label} +
+ ); +}; + +const ConnectionStatus: React.FC = ({ filter, store }) => { + return ( + +
+ + +
+
+ ); +}; + +export default ConnectionStatus; \ No newline at end of file diff --git a/examples/buddybook/src/components/ui/dialog.tsx b/examples/buddybook/src/components/ui/dialog.tsx index f2ca1ce..0e36c68 100644 --- a/examples/buddybook/src/components/ui/dialog.tsx +++ b/examples/buddybook/src/components/ui/dialog.tsx @@ -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,14 @@ const DialogContent = React.forwardRef< Date: Mon, 11 Nov 2024 16:39:46 +0700 Subject: [PATCH 2/5] chore: improvements --- examples/buddybook/package-lock.json | 8 +++ examples/buddybook/package.json | 1 + examples/buddybook/src/App.tsx | 5 +- .../src/components/Chain/SignChain.tsx | 53 ++++++++++++++++--- .../src/components/ConnectionStatus.tsx | 3 +- examples/buddybook/src/components/Header.tsx | 24 +++++---- .../buddybook/src/hooks/useWalletPrompt.ts | 25 +++++++-- 7 files changed, 95 insertions(+), 24 deletions(-) diff --git a/examples/buddybook/package-lock.json b/examples/buddybook/package-lock.json index cbba8de..c291ab9 100644 --- a/examples/buddybook/package-lock.json +++ b/examples/buddybook/package-lock.json @@ -43,6 +43,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", "autoprefixer": "^10.4.20", "copy-webpack-plugin": "^11.0.0", @@ -6610,6 +6611,13 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", diff --git a/examples/buddybook/package.json b/examples/buddybook/package.json index eb60566..4d1ecff 100644 --- a/examples/buddybook/package.json +++ b/examples/buddybook/package.json @@ -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", "autoprefixer": "^10.4.20", "copy-webpack-plugin": "^11.0.0", diff --git a/examples/buddybook/src/App.tsx b/examples/buddybook/src/App.tsx index 30b0c29..3a46233 100644 --- a/examples/buddybook/src/App.tsx +++ b/examples/buddybook/src/App.tsx @@ -80,12 +80,13 @@ function App() { return [...prevChains, message]; }); } + setWakuStatus(prev => ({ ...prev, store: 'success' })); } catch (error) { console.error("Error processing message:", error); + // Update store status to error when query fails + setWakuStatus(prev => ({ ...prev, store: 'error' })); // Continue processing other messages } - - setWakuStatus(prev => ({ ...prev, store: 'success' })); } catch (error) { console.error("Error fetching messages from store:", error); setWakuStatus(prev => ({ ...prev, store: 'error' })); diff --git a/examples/buddybook/src/components/Chain/SignChain.tsx b/examples/buddybook/src/components/Chain/SignChain.tsx index 0d6e6f2..9547ea6 100644 --- a/examples/buddybook/src/components/Chain/SignChain.tsx +++ b/examples/buddybook/src/components/Chain/SignChain.tsx @@ -7,9 +7,8 @@ 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'; interface SignChainProps { block: BlockPayload; chainsData: BlockPayload[]; // Add this prop @@ -25,6 +24,7 @@ const SignChain: React.FC = ({ block, chainsData, onSuccess }) = const { data: ensName } = useEnsName({ address }); const { node } = useWaku(); const { ensureWalletConnected } = useWalletPrompt(); + const [isWalletPrompt, setIsWalletPrompt] = useState(false); useEffect(() => { if (address) { @@ -109,8 +109,13 @@ const SignChain: React.FC = ({ block, chainsData, onSuccess }) = try { if (!address) { // If not connected, try to connect first + setIsWalletPrompt(true); const connected = await ensureWalletConnected(); - if (!connected) return; + setIsWalletPrompt(false); + if (!connected) { + setError('Please ensure your wallet is connected and the app is open.'); + return; + } } // Check if already signed @@ -119,6 +124,13 @@ const SignChain: React.FC = ({ block, chainsData, onSuccess }) = return; } + 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; + } + // Prepare the message const message = `Sign Block: Chain UUID: ${block.chainUUID} @@ -133,8 +145,10 @@ Signed by: ${ensName || address}`; 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); } }; @@ -161,15 +175,42 @@ Signed by: ${ensName || address}`;
- {error &&

{error}

} + {(error || isWalletPrompt) && ( +
+ {error &&

{error}

} + {isWalletPrompt && ( +
+

+ Attempting to connect to your wallet... +

+

+ If your wallet doesn't open automatically, please open it manually to approve the connection. +

+
+ )} + {/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) && ( +

+ Tip: If your wallet doesn't open automatically, minimize this app and open your wallet manually before trying again. +

+ )} +
+ )} - - - + + Sign Chain {alreadySigned @@ -167,57 +180,39 @@ Signed by: ${ensName || address}`; : 'Review the block details and sign to add your signature to the chain.'} -
-
-

Block Details

-

{block.title}

-

{block.description}

+
+
+
+

Block Details

+

{block.title}

+

{block.description}

+
+
+ +
- + {(error || isWalletPrompt) && ( +
+ {error &&

{error}

} + {isWalletPrompt && ( +
+

Attempting to connect to your wallet...

+

+ If your wallet doesn't open automatically, please open it manually to approve the connection. +

+
+ )} +
+ )}
- {(error || isWalletPrompt) && ( -
- {error &&

{error}

} - {isWalletPrompt && ( -
-

- Attempting to connect to your wallet... -

-

- If your wallet doesn't open automatically, please open it manually to approve the connection. -

-
- )} - {/iPhone|iPad|iPod|Android/i.test(navigator.userAgent) && ( -

- Tip: If your wallet doesn't open automatically, minimize this app and open your wallet manually before trying again. -

- )} -
- )} - + diff --git a/examples/buddybook/src/components/Chain/View/ChainList.tsx b/examples/buddybook/src/components/Chain/View/ChainList.tsx index 2a0369b..2b9d6aa 100644 --- a/examples/buddybook/src/components/Chain/View/ChainList.tsx +++ b/examples/buddybook/src/components/Chain/View/ChainList.tsx @@ -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 = ({ chainsData, onChainUpdate, isLoad - + - Share this Chain + Share Chain Share this chain with others to collect their signatures. -
- -

{shareUrl}

- + +
+
diff --git a/examples/buddybook/src/components/QRCode.tsx b/examples/buddybook/src/components/QRCode.tsx index 889f6d5..98c3974 100644 --- a/examples/buddybook/src/components/QRCode.tsx +++ b/examples/buddybook/src/components/QRCode.tsx @@ -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 = ({ text, width = 256, height = 256 }) => { +const QRCode: React.FC = ({ + 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,28 +45,66 @@ const QRCode: React.FC = ({ 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 ( -
- -
- +
+ -
+ + {showCopyButton !== 'text' && ( +
+ + +
+ )} + + {showCopyButton === 'text' && ( + + )}
); }; diff --git a/examples/buddybook/src/components/ui/dialog.tsx b/examples/buddybook/src/components/ui/dialog.tsx index 0e36c68..0946c1b 100644 --- a/examples/buddybook/src/components/ui/dialog.tsx +++ b/examples/buddybook/src/components/ui/dialog.tsx @@ -35,14 +35,12 @@ const DialogContent = React.forwardRef< Date: Wed, 13 Nov 2024 12:20:46 +0700 Subject: [PATCH 4/5] chore(buddybook): improvements --- examples/buddybook/src/App.tsx | 66 +++++++++++++++---- .../src/components/Chain/SignChain.tsx | 48 +++++++------- examples/buddybook/src/lib/waku.ts | 39 ++++++----- 3 files changed, 96 insertions(+), 57 deletions(-) diff --git a/examples/buddybook/src/App.tsx b/examples/buddybook/src/App.tsx index 6392a5a..34d283a 100644 --- a/examples/buddybook/src/App.tsx +++ b/examples/buddybook/src/App.tsx @@ -16,6 +16,8 @@ import ConnectionStatus from '@/components/ConnectionStatus'; type Status = 'success' | 'in-progress' | 'error'; + + interface WakuStatus { filter: Status; store: Status; @@ -30,6 +32,9 @@ function App() { filter: 'in-progress', store: 'in-progress', }); + + (global.window as any).waku = node; + const [telemetryOptIn, setTelemetryOptIn] = useState(null); const [isLoadingChains, setIsLoadingChains] = useState(true); @@ -41,10 +46,22 @@ function App() { }, []); useEffect(() => { - if (isWakuLoading || !node || node.libp2p.getConnections().length <= 1 || 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) - startMessageListening(); + setTimeout(() => { + startMessageListening(); + }, 2000); }, [node, isWakuLoading, wakuStatus]) const handleTelemetryOptIn = (optIn: boolean) => { @@ -69,25 +86,36 @@ 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 { - for await (const message of messageGenerator) { - setChainsData(prevChains => { - const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID); - if (blockExists) return prevChains; - return [...prevChains, message]; - }); - } + // 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); + if (blockExists) return prevChains; + return [...prevChains, message]; + }); + } + })(), + storeTimeout + ]); setWakuStatus(prev => ({ ...prev, store: 'success' })); } catch (error) { console.error("Error processing message:", error); - // Update store status to error when query fails setWakuStatus(prev => ({ ...prev, store: 'error' })); - // Continue processing other messages } } catch (error) { console.error("Error fetching messages from store:", error); @@ -96,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); diff --git a/examples/buddybook/src/components/Chain/SignChain.tsx b/examples/buddybook/src/components/Chain/SignChain.tsx index 359e8fb..315485b 100644 --- a/examples/buddybook/src/components/Chain/SignChain.tsx +++ b/examples/buddybook/src/components/Chain/SignChain.tsx @@ -171,8 +171,8 @@ const SignChain: React.FC = ({ block, chainsData, onSuccess }) = {alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Chain'} - - + + Sign Chain {alreadySigned @@ -180,32 +180,28 @@ const SignChain: React.FC = ({ block, chainsData, onSuccess }) = : 'Review the block details and sign to add your signature to the chain.'} -
-
-
-

Block Details

-

{block.title}

-

{block.description}

-
-
- -
+
+
+

Block Details

+

{block.title}

+

{block.description}

- {(error || isWalletPrompt) && ( -
- {error &&

{error}

} - {isWalletPrompt && ( -
-

Attempting to connect to your wallet...

-

- If your wallet doesn't open automatically, please open it manually to approve the connection. -

-
- )} -
- )} +
- + {(error || isWalletPrompt) && ( +
+ {error &&

{error}

} + {isWalletPrompt && ( +
+

Attempting to connect to your wallet...

+

+ If your wallet doesn't open automatically, please open it manually to approve the connection. +

+
+ )} +
+ )} +