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
5 changes: 5 additions & 0 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { EmployeeDashboard } from "./EmployeeDashboard";
import { StreamStatusCard } from "./StreamStatusCard";
import { BatchCreateStreams } from "./BatchCreateStreams";
import { ErrorBoundary } from "./ErrorBoundary";
import { OnboardingWizard, shouldShowOnboarding } from "./OnboardingWizard";

const STROOP = 10_000_000n; // 1 XLM in stroops

Expand Down Expand Up @@ -109,6 +110,7 @@ type AppView = "demo" | "dashboard" | "employee" | "batch";
export default function App() {
const [dark, toggleDark] = useDarkMode();
const [view, setView] = useState<AppView>("demo");
const [showOnboarding, setShowOnboarding] = useState(() => shouldShowOnboarding());
const { publicKey, streams, claimableAmounts, error, loading, connect, loadStream, createStream, withdraw } =
usePayStream();
const history = useTransactionHistory();
Expand Down Expand Up @@ -269,6 +271,9 @@ export default function App() {

return (
<div className="app-root" id="main-content">
{showOnboarding && (
<OnboardingWizard onComplete={() => setShowOnboarding(false)} />
)}
{/* ── Header ── */}
<header className="app-header" role="banner">
<h1>💸 PayStream Demo</h1>
Expand Down
136 changes: 136 additions & 0 deletions demo/src/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from "react";

const STEPS = [
{ label: "Connect Wallet", icon: "🔗" },
{ label: "Fund Account", icon: "💰" },
{ label: "Configure Stream", icon: "⚙️" },
{ label: "Confirm", icon: "✅" },
];

const STEP_CONTENT: React.FC<{ step: number }>[] = [
() => (
<div className="wizard-step-body">
<h3>Step 1: Connect Your Wallet</h3>
<p>Install <a href="https://freighter.app" target="_blank" rel="noreferrer">Freighter</a> and connect it to PayStream. Your wallet is your identity on the Stellar network — no sign-up required.</p>
<ul className="wizard-checklist">
<li>Install the Freighter browser extension</li>
<li>Create or import a Stellar account</li>
<li>Click <strong>Connect Freighter</strong> on the main page</li>
</ul>
</div>
),
() => (
<div className="wizard-step-body">
<h3>Step 2: Fund Your Account</h3>
<p>Your account needs tokens to create a stream. On testnet you can use Friendbot to get free XLM.</p>
<ul className="wizard-checklist">
<li>Ensure your account has enough XLM for the deposit</li>
<li>On testnet: use <a href="https://laboratory.stellar.org/#account-creator?network=test" target="_blank" rel="noreferrer">Stellar Friendbot</a></li>
<li>On mainnet: fund via an exchange or wallet transfer</li>
</ul>
</div>
),
() => (
<div className="wizard-step-body">
<h3>Step 3: Configure Your Stream</h3>
<p>Set up the stream parameters for your employee.</p>
<ul className="wizard-checklist">
<li>Enter the employee's Stellar public key</li>
<li>Choose the token (e.g., USDC)</li>
<li>Set the deposit amount and rate per second</li>
<li>Optionally set a stop time</li>
</ul>
</div>
),
() => (
<div className="wizard-step-body">
<h3>Step 4: Confirm &amp; Launch</h3>
<p>Review your stream details and submit the transaction. Once confirmed on-chain, your employee starts earning immediately.</p>
<ul className="wizard-checklist">
<li>Double-check the employee address</li>
<li>Verify the deposit and rate</li>
<li>Approve the transaction in Freighter</li>
<li>Your stream is live! 🎉</li>
</ul>
</div>
),
];

const STORAGE_KEY = "paystream_onboarding_done";

interface OnboardingWizardProps {
onComplete: () => void;
}

export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
const [step, setStep] = useState(0);

const isLast = step === STEPS.length - 1;

const handleNext = () => {
if (isLast) {
localStorage.setItem(STORAGE_KEY, "1");
onComplete();
} else {
setStep((s) => s + 1);
}
};

const handleSkip = () => {
localStorage.setItem(STORAGE_KEY, "1");
onComplete();
};

const StepBody = STEP_CONTENT[step];

return (
<div className="wizard-overlay" role="dialog" aria-modal="true" aria-label="Onboarding wizard">
<div className="wizard-card card">
{/* Progress indicator */}
<div className="wizard-progress" role="list" aria-label="Wizard steps">
{STEPS.map((s, i) => (
<div
key={i}
className={`wizard-step-dot${i === step ? " wizard-step-dot--active" : i < step ? " wizard-step-dot--done" : ""}`}
role="listitem"
aria-current={i === step ? "step" : undefined}
aria-label={`${s.label}${i < step ? " (completed)" : ""}`}
>
<span className="wizard-dot-icon" aria-hidden="true">
{i < step ? "✓" : s.icon}
</span>
<span className="wizard-dot-label">{s.label}</span>
</div>
))}
</div>

{/* Step content */}
<StepBody step={step} />

{/* Navigation */}
<div className="wizard-nav">
<button
className="btn btn-secondary"
onClick={() => setStep((s) => s - 1)}
disabled={step === 0}
aria-label="Go to previous step"
>
← Back
</button>
<button className="btn btn-secondary wizard-skip" onClick={handleSkip}>
Skip
</button>
<button className="btn" onClick={handleNext} aria-label={isLast ? "Finish onboarding" : "Go to next step"}>
{isLast ? "Get Started →" : "Next →"}
</button>
</div>
</div>
</div>
);
}

/** Returns true if the user has not yet completed onboarding. */
export function shouldShowOnboarding(): boolean {
return !localStorage.getItem(STORAGE_KEY);
}
91 changes: 91 additions & 0 deletions demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1364,3 +1364,94 @@ code {
background: var(--input-bg);
color: var(--text);
}

/* ── Onboarding Wizard (#232) ── */
.wizard-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.wizard-card {
width: min(520px, 95vw);
padding: 28px;
display: flex;
flex-direction: column;
gap: 20px;
}

.wizard-progress {
display: flex;
justify-content: space-between;
gap: 8px;
}

.wizard-step-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex: 1;
opacity: 0.45;
}

.wizard-step-dot--active,
.wizard-step-dot--done {
opacity: 1;
}

.wizard-dot-icon {
font-size: 20px;
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-card);
}

.wizard-step-dot--active .wizard-dot-icon {
border-color: var(--btn-bg);
background: var(--btn-bg);
color: var(--btn-text);
}

.wizard-step-dot--done .wizard-dot-icon {
border-color: var(--status-active);
color: var(--status-active);
}

.wizard-dot-label {
font-size: 11px;
color: var(--text-muted);
text-align: center;
}

.wizard-step-body h3 {
margin: 0 0 8px;
}

.wizard-checklist {
padding-left: 20px;
margin: 8px 0 0;
color: var(--text-muted);
font-size: 14px;
line-height: 1.7;
}

.wizard-nav {
display: flex;
gap: 8px;
justify-content: flex-end;
align-items: center;
}

.wizard-skip {
margin-right: auto;
}