From 3514169b9c62f0b2932859c6e3a8126d42775d6b Mon Sep 17 00:00:00 2001 From: lamborghini21 Date: Sun, 28 Jun 2026 01:22:25 +0100 Subject: [PATCH] feat: add opt-in telemetry with Plausible and privacy page --- index.html | 5 ++- src/App.tsx | 6 +++- src/components/CkbReceive.tsx | 2 ++ src/components/CkbSend.tsx | 2 ++ src/components/HorizenReceive.tsx | 3 ++ src/components/HorizenSend.tsx | 2 ++ src/components/SolanaReceive.tsx | 3 ++ src/components/SolanaSend.tsx | 2 ++ src/components/StellarReceive.tsx | 3 ++ src/components/StellarSend.tsx | 2 ++ src/components/TelemetryBanner.tsx | 51 +++++++++++++++++++++++++++ src/components/WalletConnect.tsx | 11 +++--- src/lib/telemetry.ts | 35 +++++++++++++++++++ src/pages/Privacy.tsx | 55 ++++++++++++++++++++++++++++++ src/pages/Receive.tsx | 8 ++++- src/pages/Send.tsx | 8 ++++- 16 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 src/components/TelemetryBanner.tsx create mode 100644 src/lib/telemetry.ts create mode 100644 src/pages/Privacy.tsx diff --git a/index.html b/index.html index 166440e..677b3e4 100644 --- a/index.html +++ b/index.html @@ -40,7 +40,10 @@ content="Send and receive private payments on Horizen and Stellar using stealth addresses." /> - + + + + diff --git a/src/App.tsx b/src/App.tsx index 41f52e0..ee2015b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,25 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import { Header } from '@/components/Header'; import { AutoSign } from '@/components/AutoSign'; +import { TelemetryBanner } from '@/components/TelemetryBanner'; import Send from '@/pages/Send'; import Receive from '@/pages/Receive'; +import Privacy from '@/pages/Privacy'; export function App() { return (
+
} /> } /> + } /> } />
); -} +} \ No newline at end of file diff --git a/src/components/CkbReceive.tsx b/src/components/CkbReceive.tsx index 183571e..aef6954 100644 --- a/src/components/CkbReceive.tsx +++ b/src/components/CkbReceive.tsx @@ -11,6 +11,7 @@ import { } from '@wraith-protocol/sdk/chains/ckb'; import { useStealthKeys } from '@/context/StealthKeysContext'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; function CkbStealthRow({ match }: { match: MatchedStealthCell }) { const [showKey, setShowKey] = useState(false); @@ -129,6 +130,7 @@ export function CkbReceive() { ); setMatched(results); setHasScanned(true); + trackEvent('scan_triggered'); } catch (err) { setError(err instanceof Error ? err.message : 'Scan failed'); } finally { diff --git a/src/components/CkbSend.tsx b/src/components/CkbSend.tsx index ae5261c..da23000 100644 --- a/src/components/CkbSend.tsx +++ b/src/components/CkbSend.tsx @@ -6,6 +6,7 @@ import { getDeployment, } from '@wraith-protocol/sdk/chains/ckb'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; const STEALTH_LOCK_CODE_HASH = getDeployment('ckb').contracts.stealthLockCodeHash; @@ -71,6 +72,7 @@ export function CkbSend() { await tx.completeFeeBy(signer, 1000); const hash = await signer.sendTransaction(tx); + trackEvent('send_submitted'); setTxHash(hash); } catch (err) { setError(err instanceof Error ? err.message : 'Transaction failed'); diff --git a/src/components/HorizenReceive.tsx b/src/components/HorizenReceive.tsx index 3ac860a..ebb62de 100644 --- a/src/components/HorizenReceive.tsx +++ b/src/components/HorizenReceive.tsx @@ -21,6 +21,7 @@ import { } from '@wraith-protocol/sdk/chains/evm'; import type { HexString, MatchedAnnouncement } from '@wraith-protocol/sdk/chains/evm'; import { useStealthKeys } from '@/context/StealthKeysContext'; +import { trackEvent } from '@/lib/telemetry'; import { CopyButton } from '@/components/CopyButton'; import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer'; import { horizenTestnet } from '@/config'; @@ -87,6 +88,7 @@ function StealthRow({ }); setWithdrawHash(hash); + trackEvent('withdraw'); onWithdrawn(hash); } catch (err) { setError(err instanceof Error ? err.message : 'Withdraw failed'); @@ -278,6 +280,7 @@ export function HorizenReceive() { ); setMatched(results); setHasScanned(true); + trackEvent('scan_triggered'); } catch (err) { setError(err instanceof Error ? err.message : 'Scan failed'); } finally { diff --git a/src/components/HorizenSend.tsx b/src/components/HorizenSend.tsx index b05e81a..1732619 100644 --- a/src/components/HorizenSend.tsx +++ b/src/components/HorizenSend.tsx @@ -5,6 +5,7 @@ import type { HexString, BuildSendStealthResult } from '@wraith-protocol/sdk/cha import { horizenTxUrl, horizenAddrUrl } from '@/lib/explorer'; import { horizenTestnet } from '@/config'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; export function HorizenSend() { const { isConnected, address } = useAccount(); @@ -61,6 +62,7 @@ export function HorizenSend() { }); setStealthResult(result); + trackEvent('send_submitted'); sendTransaction({ to: result.transaction.to as `0x${string}`, diff --git a/src/components/SolanaReceive.tsx b/src/components/SolanaReceive.tsx index 8856fa2..552daa4 100644 --- a/src/components/SolanaReceive.tsx +++ b/src/components/SolanaReceive.tsx @@ -12,6 +12,7 @@ import { import type { MatchedAnnouncement } from '@wraith-protocol/sdk/chains/solana'; import { useStealthKeys } from '@/context/StealthKeysContext'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer'; import { SOLANA_NETWORK } from '@/config'; @@ -92,6 +93,7 @@ function SolanaStealthRow({ await connection.confirmTransaction(txId, 'confirmed'); setWithdrawHash(txId); + trackEvent('withdraw'); onWithdrawn(); } catch (err) { setError(err instanceof Error ? err.message : 'Withdraw failed'); @@ -245,6 +247,7 @@ export function SolanaReceive() { ); setMatched(results); setHasScanned(true); + trackEvent('scan_triggered'); } catch (err) { setError(err instanceof Error ? err.message : 'Scan failed'); } finally { diff --git a/src/components/SolanaSend.tsx b/src/components/SolanaSend.tsx index 2fea1bd..f19d781 100644 --- a/src/components/SolanaSend.tsx +++ b/src/components/SolanaSend.tsx @@ -11,6 +11,7 @@ import { buildSendSol, bytesToHex } from '@wraith-protocol/sdk/chains/solana'; import { solanaTxUrl, solanaAddrUrl } from '@/lib/explorer'; import { SOLANA_NETWORK } from '@/config'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; export function SolanaSend() { const { publicKey, connected, sendTransaction } = useWallet(); @@ -48,6 +49,7 @@ export function SolanaSend() { amount: lamports, senderPubkey: publicKey.toBase58(), }); + trackEvent('send_submitted'); setStealthResult({ stealthAddress: result.stealthAddress, ephemeralPubKey: result.ephemeralPubKey, diff --git a/src/components/StellarReceive.tsx b/src/components/StellarReceive.tsx index d279a52..e0d9455 100644 --- a/src/components/StellarReceive.tsx +++ b/src/components/StellarReceive.tsx @@ -22,6 +22,7 @@ import type { Announcement, MatchedAnnouncement } from '@wraith-protocol/sdk/cha import { useStealthKeys } from '@/context/StealthKeysContext'; import { useStellarWallet } from '@/context/StellarWalletContext'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer'; import { STELLAR_NETWORK } from '@/config'; @@ -227,6 +228,7 @@ function StellarStealthRow({ } setWithdrawHash(submitData.hash); + trackEvent('withdraw'); onWithdrawn(); } catch (err) { setError(err instanceof Error ? err.message : 'Withdraw failed'); @@ -501,6 +503,7 @@ export function StellarReceive() { ); setMatched(results); setHasScanned(true); + trackEvent('scan_triggered'); } catch (err) { setError(err instanceof Error ? err.message : 'Scan failed'); } finally { diff --git a/src/components/StellarSend.tsx b/src/components/StellarSend.tsx index 6626d1f..930b42b 100644 --- a/src/components/StellarSend.tsx +++ b/src/components/StellarSend.tsx @@ -18,6 +18,7 @@ import { useStellarWallet } from '@/context/StellarWalletContext'; import { stellarTxUrl, stellarAddrUrl } from '@/lib/explorer'; import { STELLAR_NETWORK } from '@/config'; import { CopyButton } from '@/components/CopyButton'; +import { trackEvent } from '@/lib/telemetry'; const ANNOUNCER_CONTRACT = 'CCJLJ2QRBJAAKIG6ELNQVXLLWMKKWVN5O2FKWUETHZGMPAD4MHK7WVWL'; @@ -55,6 +56,7 @@ export function StellarSend() { const decoded = decodeStealthMetaAddress(metaAddress); const result = generateStealthAddress(decoded.spendingPubKey, decoded.viewingPubKey); setStealthResult(result); + trackEvent('send_submitted'); const horizonUrl = STELLAR_NETWORK.horizonUrl; const networkPassphrase = STELLAR_NETWORK.networkPassphrase; diff --git a/src/components/TelemetryBanner.tsx b/src/components/TelemetryBanner.tsx new file mode 100644 index 0000000..3922d99 --- /dev/null +++ b/src/components/TelemetryBanner.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { getConsent, setConsent, type ConsentState } from '@/lib/telemetry'; + +export function TelemetryBanner() { + const [consent, setConsentState] = useState(null); + + useEffect(() => { + setConsentState(getConsent()); + }, []); + + if (consent !== null) return null; + + function handleAccept() { + setConsent('accepted'); + setConsentState('accepted'); + } + + function handleDecline() { + setConsent('declined'); + setConsentState('declined'); + } + + return ( +
+
+

+ We use cookieless analytics to improve this demo.{' '} + + Privacy page + + . No wallet addresses or amounts are ever collected. +

+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/WalletConnect.tsx b/src/components/WalletConnect.tsx index 807db1c..8b74aa5 100644 --- a/src/components/WalletConnect.tsx +++ b/src/components/WalletConnect.tsx @@ -5,6 +5,7 @@ import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { ccc } from '@ckb-ccc/connector-react'; import { useChain } from '@/context/ChainContext'; import { useStellarWallet } from '@/context/StellarWalletContext'; +import { trackEvent } from '@/lib/telemetry'; const btnBase = 'bg-transparent border border-outline-variant px-3 py-1.5 font-heading text-[10px] uppercase tracking-widest text-primary transition-colors hover:bg-surface-bright disabled:opacity-50 sm:px-4 sm:py-2 sm:text-xs h-8 sm:h-9'; @@ -24,7 +25,7 @@ function HorizenButton() { })} > {!connected ? ( - ) : ( @@ -51,7 +52,7 @@ function FreighterButton() { } return ( - ); @@ -68,8 +69,8 @@ function SolanaButton() { ); } - - return ; +return trackEvent('connect_wallet')} />; + } function CkbButton() { @@ -94,7 +95,7 @@ function CkbButton() { } return ( - ); diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts new file mode 100644 index 0000000..88538f1 --- /dev/null +++ b/src/lib/telemetry.ts @@ -0,0 +1,35 @@ +const STORAGE_KEY = 'wraith-telemetry-consent'; + +export type ConsentState = 'accepted' | 'declined' | null; + +export function getConsent(): ConsentState { + const val = localStorage.getItem(STORAGE_KEY); + if (val === 'accepted' || val === 'declined') return val; + return null; +} + +export function setConsent(state: 'accepted' | 'declined'): void { + localStorage.setItem(STORAGE_KEY, state); +} + +function isEnabled(): boolean { + return getConsent() === 'accepted'; +} + +export function trackPageView(path: string): void { + if (!isEnabled()) return; + if (typeof window.plausible === 'undefined') return; + window.plausible('pageview', { u: window.location.origin + path }); +} + +export function trackEvent(name: string): void { + if (!isEnabled()) return; + if (typeof window.plausible === 'undefined') return; + window.plausible(name); +} + +declare global { + interface Window { + plausible?: (event: string, options?: { u?: string }) => void; + } +} \ No newline at end of file diff --git a/src/pages/Privacy.tsx b/src/pages/Privacy.tsx new file mode 100644 index 0000000..d5ddd79 --- /dev/null +++ b/src/pages/Privacy.tsx @@ -0,0 +1,55 @@ +import { useEffect } from 'react'; +import { trackPageView } from '@/lib/telemetry'; + +export default function Privacy() { + useEffect(() => { + trackPageView('/privacy'); + }, []); + return ( +
+
+

Privacy Policy

+

Last updated: June 2026

+
+ +
+

What we collect

+

+ If you opt in to analytics, we collect the following — nothing more: +

+
    +
  • Page views (which page you visited)
  • +
  • Key flow events: connect wallet, send submitted, scan triggered, withdraw
  • +
+
+ +
+

What we never collect

+
    +
  • Wallet addresses
  • +
  • Transaction amounts
  • +
  • IP addresses
  • +
  • Cookies or cross-site tracking
  • +
  • Any personally identifiable information
  • +
+
+ +
+

Provider

+

+ We use Plausible Analytics — a + cookieless, privacy-respecting analytics tool that does not use cookies and is fully + compliant with GDPR, CCPA, and PECR. No data is sold or shared with third parties. +

+
+ +
+

Your choice

+

+ Analytics is strictly opt-in. You are asked once on your first visit. You can change + your choice at any time by clearing your browser's local storage for this site. +

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Receive.tsx b/src/pages/Receive.tsx index dd0cc35..8069e77 100644 --- a/src/pages/Receive.tsx +++ b/src/pages/Receive.tsx @@ -1,14 +1,20 @@ +import { useEffect } from 'react'; import { useChain } from '@/context/ChainContext'; import { HorizenReceive } from '@/components/HorizenReceive'; import { StellarReceive } from '@/components/StellarReceive'; import { SolanaReceive } from '@/components/SolanaReceive'; import { CkbReceive } from '@/components/CkbReceive'; +import { trackPageView } from '@/lib/telemetry'; export default function Receive() { const { chain } = useChain(); + useEffect(() => { + trackPageView('/receive'); + }, []); + if (chain === 'stellar') return ; if (chain === 'solana') return ; if (chain === 'ckb') return ; return ; -} +} \ No newline at end of file diff --git a/src/pages/Send.tsx b/src/pages/Send.tsx index 8b1d43e..3a5e88b 100644 --- a/src/pages/Send.tsx +++ b/src/pages/Send.tsx @@ -1,14 +1,20 @@ +import { useEffect } from 'react'; import { useChain } from '@/context/ChainContext'; import { HorizenSend } from '@/components/HorizenSend'; import { StellarSend } from '@/components/StellarSend'; import { SolanaSend } from '@/components/SolanaSend'; import { CkbSend } from '@/components/CkbSend'; +import { trackPageView } from '@/lib/telemetry'; export default function Send() { const { chain } = useChain(); + useEffect(() => { + trackPageView('/send'); + }, []); + if (chain === 'stellar') return ; if (chain === 'solana') return ; if (chain === 'ckb') return ; return ; -} +} \ No newline at end of file