Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@wraith-protocol/sdk": "^1.4.5",
"bs58": "^6.0.0",
"buffer": "^6.0.3",
"driver.js": "1.3.1",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.5",
"qrcode.react": "^4.2.0",
Expand Down
414 changes: 192 additions & 222 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

43 changes: 25 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,40 @@ import { Routes, Route, Navigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { AutoSign } from '@/components/AutoSign';
import { TelemetryBanner } from '@/components/TelemetryBanner';
import { TourLauncher } from '@/components/TourLauncher';
import Send from '@/pages/Send';
import Receive from '@/pages/Receive';
import Privacy from '@/pages/Privacy';
import Settings from '@/pages/Settings';
import { HelpButton } from '@/components/HelpButton';
import Vault from '@/pages/Vault';
import Schedule from '@/pages/Schedule';
import StellarSplit from '@/pages/StellarSplit';
import { OnboardingProvider } from '@/context/OnboardingContext';

export function App() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<TelemetryBanner />
<AutoSign />
<main className="mx-auto w-full max-w-[720px] flex-1 px-4 pb-16 pt-8 sm:px-6 sm:pb-24 sm:pt-10">
<Routes>
<Route path="/send" element={<Send />} />
<Route path="/receive" element={<Receive />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/vault" element={<Vault />} />
<Route path="/schedule" element={<Schedule />} />
<Route path="/stellar/split" element={<StellarSplit />} />
<Route path="/pay" element={<Send />} />
<Route path="*" element={<Navigate to="/send" replace />} />
</Routes>
</main>
<HelpButton />
</div>
<OnboardingProvider>
<div className="flex min-h-screen flex-col">
<Header />
<TelemetryBanner />
<AutoSign />
<TourLauncher />
<main className="mx-auto w-full max-w-[720px] flex-1 px-4 pb-16 pt-8 sm:px-6 sm:pb-24 sm:pt-10">
<Routes>
<Route path="/send" element={<Send />} />
<Route path="/receive" element={<Receive />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/settings" element={<Settings />} />
<Route path="/vault" element={<Vault />} />
<Route path="/schedule" element={<Schedule />} />
<Route path="/stellar/split" element={<StellarSplit />} />
<Route path="/pay" element={<Send />} />
<Route path="*" element={<Navigate to="/send" replace />} />
</Routes>
</main>
<HelpButton />
</div>
</OnboardingProvider>
);
}
2 changes: 1 addition & 1 deletion src/components/ChainSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function ChainSwitcher({ disabledChains = [] }: { disabledChains?: Chain[
const { chain, setChain } = useChain();

return (
<div className="relative">
<div className="relative" data-tour="chain-switcher">
<select
value={chain}
onChange={(e) => setChain(e.target.value as Chain)}
Expand Down
1 change: 1 addition & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function Header() {
{ to: '/send', label: t('nav.send') },
{ to: '/receive', label: t('nav.receive') },
{ to: '/schedule', label: t('nav.schedule') },
{ to: '/settings', label: t('nav.settings') },
];

return (
Expand Down
4 changes: 3 additions & 1 deletion src/components/HorizenReceive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export function HorizenReceive() {
{!evmKeys && (
<div className="flex flex-col gap-4">
<button
data-tour="derive-keys"
onClick={deriveKeys}
disabled={isDerivingKeys}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
Expand All @@ -355,7 +356,7 @@ export function HorizenReceive() {

{evmKeys && evmMetaAddress && (
<>
<div className="border border-outline-variant bg-surface-container p-5">
<div className="border border-outline-variant bg-surface-container p-5" data-tour="meta-address">
<div className="mb-2 flex items-center justify-between">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
{t('common.yourStealthMetaAddress')}
Expand Down Expand Up @@ -413,6 +414,7 @@ export function HorizenReceive() {

<div className="flex items-center justify-between">
<button
data-tour="scan-payments"
onClick={scanPayments}
disabled={isScanning}
className="h-12 bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
Expand Down
4 changes: 3 additions & 1 deletion src/components/StellarReceiveView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function StellarReceiveView({
{!keysDerived && (
<div className="flex flex-col gap-4">
<button
data-tour="derive-keys"
onClick={onDeriveKeys}
disabled={isDerivingKeys}
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
Expand All @@ -134,7 +135,7 @@ export function StellarReceiveView({

{keysDerived && metaAddress && (
<>
<div className="border border-outline-variant bg-surface-container p-5">
<div className="border border-outline-variant bg-surface-container p-5" data-tour="meta-address">
<div className="mb-2 flex items-center justify-between">
<span className="font-mono text-[10px] uppercase tracking-widest text-outline">
Your Stealth Meta-Address
Expand Down Expand Up @@ -274,6 +275,7 @@ export function StellarReceiveView({

<div className="flex items-center justify-between">
<button
data-tour="scan-payments"
onClick={onScan}
disabled={isScanning}
className="h-12 w-full bg-primary px-6 font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30 sm:w-auto"
Expand Down
36 changes: 36 additions & 0 deletions src/components/TourLauncher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useOnboarding } from '@/context/OnboardingContext';

/**
* TourLauncher — mounts inside the app and fires the tour once on first visit.
* We wait until the user is on /receive so that the derive-keys and scan
* anchors actually exist in the DOM when the tour starts.
*
* Subsequent steps that point to /receive elements will fall back gracefully
* (driver.js skips a step if the element is not found).
*/
export function TourLauncher() {
const { startTour, hasSeenTour } = useOnboarding();
const fired = useRef(false);
const location = useLocation();

useEffect(() => {
// Only fire once per session, and only for first-time visitors
if (fired.current) return;
if (hasSeenTour()) return;

// Small delay to let the page paint before the overlay appears
const timer = setTimeout(() => {
if (!hasSeenTour() && !fired.current) {
fired.current = true;
startTour();
}
}, 800);

return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);

return null;
}
17 changes: 13 additions & 4 deletions src/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,17 @@ function CkbButton() {
export function WalletConnect() {
const { chain } = useChain();

if (chain === 'stellar') return <StellarButton />;
if (chain === 'solana') return <SolanaButton />;
if (chain === 'ckb') return <CkbButton />;
return <HorizenButton />;
return (
<div data-tour="wallet-connect">
{chain === 'stellar' ? (
<StellarButton />
) : chain === 'solana' ? (
<SolanaButton />
) : chain === 'ckb' ? (
<CkbButton />
) : (
<HorizenButton />
)}
</div>
);
}
152 changes: 152 additions & 0 deletions src/context/OnboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
createContext,
useContext,
useCallback,
useRef,
type ReactNode,
} from 'react';
import { driver } from 'driver.js';
import 'driver.js/dist/driver.css';

const STORAGE_KEY = 'wraith-tour-dismissed';

interface OnboardingContextValue {
startTour: () => void;
dismissTour: () => void;
hasSeenTour: () => boolean;
}

const OnboardingContext = createContext<OnboardingContextValue | null>(null);

function buildTourSteps() {
return [
{
// Step 1 – Welcome / overview (no element highlight)
popover: {
title: 'Welcome to Wraith Protocol',
description:
'This demo shows how to send and receive <strong>stealth payments</strong> — private transactions where every payment lands at a fresh, one-time address.\n\nThis tour takes under a minute. You can skip at any time.',
side: 'over' as const,
align: 'center' as const,
},
},
{
// Step 2 – Connect wallet (targets the WalletConnect button)
element: '[data-tour="wallet-connect"]',
popover: {
title: 'Step 1 · Connect your wallet',
description:
'Start by connecting the wallet for your chosen chain — Freighter for Stellar, or a browser wallet for EVM/Solana/CKB.\n\nUse the chain switcher on the left to pick your network first.',
side: 'bottom' as const,
align: 'end' as const,
},
},
{
// Step 3 – Derive keys / sign message (targets the Derive Keys button on Receive page)
element: '[data-tour="derive-keys"]',
popover: {
title: 'Step 2 · Derive your stealth keys',
description:
'Click <strong>Derive Keys</strong> to generate your stealth spending and viewing key pair. Your wallet will ask you to sign a message.\n\n<code style="font-family:\'JetBrains Mono\',monospace;font-size:11px;background:rgba(0,0,0,0.15);padding:2px 4px">SIGN THIS MESSAGE TO DERIVE YOUR STEALTH KEYS</code>\n\nThis is a <em>read-only signature</em> — no transaction is broadcast and no funds move. It derives your keys deterministically from your wallet, so you can always re-derive them.',
side: 'bottom' as const,
align: 'start' as const,
},
},
{
// Step 4 – Meta-address display
element: '[data-tour="meta-address"]',
popover: {
title: 'Step 3 · Your stealth meta-address',
description:
'This is your <strong>stealth meta-address</strong> — share it like a regular address so senders can generate one-time stealth addresses that only you can spend.\n\nYou can register it on-chain so others can look you up by wallet address, or share it manually.',
side: 'bottom' as const,
align: 'start' as const,
},
},
{
// Step 5 – Scan for payments
element: '[data-tour="scan-payments"]',
popover: {
title: 'Step 4 · Scan for payments',
description:
'Hit <strong>Scan for Payments</strong> to check the chain for any stealth transfers addressed to you.\n\nThe SDK scans on-chain announcements and matches them against your viewing key locally — your private key never leaves your browser.',
side: 'top' as const,
align: 'start' as const,
},
},
{
// Step 6 – Send flow summary
element: '[data-tour="chain-switcher"]',
popover: {
title: 'Sending stealth payments',
description:
'To send, switch to the <strong>Send</strong> tab. Paste the recipient\'s stealth meta-address, enter an amount, and confirm. The SDK generates a fresh one-time address per transfer — the recipient\'s real address is never exposed on-chain.',
side: 'bottom' as const,
align: 'start' as const,
},
},
{
// Step 7 – Done
popover: {
title: "You're all set",
description:
"That's the full flow: connect → derive → share meta-address → scan.\n\nYou can restart this tour at any time from the <strong>Settings</strong> page.",
side: 'over' as const,
align: 'center' as const,
},
},
];
}

export function OnboardingProvider({ children }: { children: ReactNode }) {
const driverRef = useRef<ReturnType<typeof driver> | null>(null);

const hasSeenTour = useCallback(() => {
return localStorage.getItem(STORAGE_KEY) === 'true';
}, []);

const dismissTour = useCallback(() => {
localStorage.setItem(STORAGE_KEY, 'true');
if (driverRef.current) {
driverRef.current.destroy();
driverRef.current = null;
}
}, []);

const startTour = useCallback(() => {
// Destroy any running instance before starting fresh
if (driverRef.current) {
driverRef.current.destroy();
}

const driverInstance = driver({
showProgress: true,
animate: true,
overlayOpacity: 0.6,
smoothScroll: true,
allowClose: true,
// Custom popover styles to match the Wraith design system
popoverClass: 'wraith-tour-popover',
steps: buildTourSteps(),
onDestroyed: () => {
localStorage.setItem(STORAGE_KEY, 'true');
driverRef.current = null;
},
});

driverRef.current = driverInstance;
driverInstance.drive();
}, []);

return (
<OnboardingContext.Provider value={{ startTour, dismissTour, hasSeenTour }}>
{children}
</OnboardingContext.Provider>
);
}

export function useOnboarding() {
const ctx = useContext(OnboardingContext);
if (!ctx) throw new Error('useOnboarding must be used within OnboardingProvider');
return ctx;
}
3 changes: 2 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"nav": {
"send": "Send",
"receive": "Receive",
"schedule": "Schedule"
"schedule": "Schedule",
"settings": "Settings"
},
"header": {
"menuLabel": "Menu",
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"nav": {
"send": "Enviar",
"receive": "Recibir",
"schedule": "Programar"
"schedule": "Programar",
"settings": "Configuración"
},
"header": {
"menuLabel": "Menú",
Expand Down
Loading
Loading