Web client for TricklePay token streams on Stellar.
Connect a Freighter wallet to see your incoming and outgoing streams, watch a recipient's balance accrue in real time, and create, withdraw from, or cancel a stream — all signed in the wallet and confirmed on-chain.
It reads stream data from tricklepay-backend and writes to the stream contract directly over Soroban RPC.
git clone https://github.com/TricklePay/tricklepay-frontend.git
cd tricklepay-frontend
cp .env.example .env.local
npm install
npm run devOpen http://localhost:3000 and connect your Freighter wallet on testnet.
See Running locally for detailed setup instructions, Configuration for environment variables, and Wallet Requirement for the Freighter wallet walkthrough.
- Quickstart
- Features
- Stack
- Browser Support
- Wallet Requirement
- Running locally
- Configuration
- Troubleshooting
- Glossary of Streaming Terms
- Frequently Asked Questions
- Styling Approach
- Project structure
- API contract
- Related repositories
- License
- Wallet connect via Freighter, with silent session restore and a network-mismatch warning.
- Dashboard splitting streams into incoming (you receive) and outgoing (you send).
- Live balances — a streaming stream's withdrawable amount is recomputed every second client-side, using the same linear vesting math as the contract, so it climbs in real time without polling.
- Create a stream with a recipient, token, amount, time window, and optional cliff.
- Withdraw as the recipient and cancel as the sender, each built, signed, submitted, and confirmed on-chain with step-by-step transaction progress stages (Preparing -> Sign -> Submit -> Confirm).
- Duplicate submission protection — in-flight invocation guards in contract utilities and components prevent double-submitting active transactions.
- Transaction timeout recovery — explicit recovery UI allows re-checking confirmation by transaction hash if network confirmation times out without re-submitting.
- Maximum withdraw amount hint — accessible hint and quick "Max" action fill the withdrawal input directly with the live vested balance.
- Keyboard focus visibility — every interactive element gets a consistent, high-contrast focus ring when navigated to via keyboard (
:focus-visibleinapp/globals.css), independent of any per-component focus styling. - Reduced-motion support — visitors with the OS-level "reduce motion" preference enabled get all animations and transitions (skeleton shimmer, spinners, progress pulses, hover/focus transitions) collapsed to a single frame, app-wide (
prefers-reduced-motioninapp/globals.css). - Light theme toggle — a header button flips the whole app between dark (default) and light, persisted in
localStorageand applied before first paint to avoid a flash of the wrong theme. Seecomponents/theme-toggle.tsx,components/theme-provider.tsx, andlib/theme.ts. - Branded loading indicator — indeterminate loading states (route transitions, wallet connect) use
components/brand-spinner.tsx, three bouncing indigo dots echoing the trickle-drop mark inapp/icon.svg, instead of a generic spinner. Content-shaped loading (stream lists, stream detail) keeps the existing skeletons incomponents/skeleton.tsx.
- Next.js 15 (App Router) + TypeScript
- Tailwind CSS v4
- @stellar/stellar-sdk for the write path
TricklePay is designed for modern desktop browsers with the Freighter wallet extension installed:
- Google Chrome (version 100+)
- Brave Browser
- Mozilla Firefox (version 100+)
- Microsoft Edge (Chromium-based)
Mobile browsers and browsers without extension support can view public stream information but require a supported desktop browser to sign and execute transactions.
TricklePay requires the Freighter browser extension to sign and submit transactions.
- Visit freighter.app and install the extension for your browser.
- Create a new wallet or import an existing Stellar keypair.
- Open the extension and switch the network to Testnet (via the network dropdown in the extension's top bar).
If your Freighter wallet is on a different network than what the app expects (e.g. the wallet is on PUBLIC while the app is configured for testnet), the client displays a warning banner and disables stream creation and management actions. Switch the wallet's network to match the app's NEXT_PUBLIC_NETWORK setting.
A funded account is required to create a stream. Fund your testnet account using the Stellar Laboratory or the Friendbot faucet.
- Node.js:
v20.0.0or higher (Node 20+ LTS). Next.js 15, React 19, and Tailwind CSS v4 build tooling require Node 20+ runtime features and module resolution; older Node versions will encounter errors during package installation or compilation. - npm:
v10.0.0or higher. - Freighter Extension: Installed in your browser (see Wallet Requirement).
- Backend API: A running instance of
tricklepay-backend.
cp .env.example .env.local # set NEXT_PUBLIC_CONTRACT_ID and the API URL
npm install
npm run devOpen http://localhost:3000.
Configuration comes from NEXT_PUBLIC_* variables; see .env.example.
| Variable | Description | Required | Default |
|---|---|---|---|
NEXT_PUBLIC_CONTRACT_ID |
Deployed stream contract ID (starts with C). |
Yes | (None) |
NEXT_PUBLIC_API_URL |
Base URL of the backend read API. | No | http://localhost:3000 |
NEXT_PUBLIC_NETWORK |
Stellar network (testnet or mainnet). |
No | testnet |
NEXT_PUBLIC_RPC_URL |
Soroban RPC endpoint for submitting transactions. | No | https://soroban-testnet.stellar.org |
To switch the application to interact with a different stream contract:
- Update
NEXT_PUBLIC_CONTRACT_IDin.env.local(or your environment variable configuration) with the new contract address. - Rebuild the application (
npm run build) or restart the development server so that the change is compiled into the JavaScript bundle.
Important
Rebuild Required: Next.js inlines NEXT_PUBLIC_* environment variables directly into the client-side JavaScript bundle at build/compile time. Because the contract ID is fixed at build time, simply modifying .env.local and restarting a running production instance without rebuilding will not update the active contract ID, causing the app to continue targeting the old contract.
Common local setup issues and their resolutions:
- What you see: A warning banner appears stating "Wallet Network Mismatch: Connected to PUBLIC, but app expects testnet", and stream creation/management buttons are disabled.
- Fix: Open the Freighter extension in your browser, click the network selector at the top, and switch to Testnet (or the network specified by
NEXT_PUBLIC_NETWORK).
- What you see: Dashboard stream lists fail to load, showing error messages or infinite loading skeletons, and the browser console logs
ERR_CONNECTION_REFUSEDorFailed to fetchrequests againstNEXT_PUBLIC_API_URL. - Fix: Start the
tricklepay-backendservice locally, or check.env.localto verifyNEXT_PUBLIC_API_URLpoints to the correct backend host.
- What you see: Transactions continue targeting a previously configured contract address even after updating
NEXT_PUBLIC_CONTRACT_IDin.env.local, or transaction submissions fail withHostError/ invalid contract invocation errors. - Fix: Because
NEXT_PUBLIC_*values are inlined at build time, stop the application server and runnpm run build(or restartnpm run dev) to recompile the client bundle with the updated contract ID.
TricklePay uses precise terminology across the interface to represent continuous token streaming schedules and balances. Each definition below directly matches how the value is computed across the smart contract, backend indexer, and frontend client:
- Vested (
vested): The cumulative total of tokens that have unlocked along the stream's linear schedule fromstartTimeup to the current timestamp (now).- Computation: Evaluated in base units (stroops) matching
lib/vesting.tsand the Soroban contract:- If
now < cliffTimeornow < startTime:0. - If
now >= endTime:totalAmount. - Otherwise:
(totalAmount * (now - startTime)) / (endTime - startTime).
- If
- Computation: Evaluated in base units (stroops) matching
- Withdrawn (
withdrawn): The cumulative total of tokens that the recipient has already transferred out of the stream contract through on-chainwithdraworwithdraw_amounttransactions. - Withdrawable (
withdrawable): The portion of the vested balance that the recipient is eligible to claim immediately.- Computation:
max(0, vested - withdrawn).
- Computation:
- Locked (
locked): The unvested portion of the total stream amount that remains locked in the contract awaiting future release.- Computation:
totalAmount - vested. For active streams (streamingorpending), this represents tokens awaiting future vesting. When a stream iscancelled, the unvested balance is returned to the sender.
- Computation:
- Cliff (
cliffTime): An optional initial milestone timestamp before which zero tokens vest (vested = 0). Oncenow >= cliffTime, the stream immediately unlocks the full linear allocation accrued sincestartTime. When no cliff is configured,cliffTimeequalsstartTime. - Progress (
progress): The proportion of the total stream allocation that has vested so far, expressed in basis points (0to10000, where100 bps = 1%).- Computation:
(vested / totalAmount) * 10000(precomputed by the backend indexer and rendered as a percentagevalue / 100viacomponents/progress-bar.tsx).
- Computation:
- Status (
status): The discrete lifecycle phase of a stream:pending: The current time is beforestartTime(now < startTime).streaming: The stream is actively vesting in real time (startTime <= now < endTimeand not cancelled).completed: The stream has reached or passedendTime(now >= endTime), and 100% of tokens have fully vested.cancelled: The sender stopped the stream beforeendTime; unvested funds were returned to the sender while remaining vested tokens remain available for the recipient to withdraw.
Active streams calculate live balance accrual entirely client-side using useAccrual (hooks/use-accrual.ts), which evaluates the contract's integer linear vesting math once every second. This enables real-time balance climbing without polling the backend API or Soroban RPC endpoints. See Live balances and the Vested computation.
No. TricklePay is completely non-custodial. All tokens committed to a stream are held directly by the deployed Soroban stream smart contract on the Stellar network. All state-modifying actions (creating a stream, withdrawing tokens, cancelling a stream) are assembled locally and signed exclusively within your browser by the Freighter extension. The backend API is read-only and never handles private keys or transaction signing. See Wallet Requirement and On-chain contract surface.
When a sender cancels an active stream, vesting stops immediately at that exact second. All tokens vested up to that point remain available for the recipient to withdraw at any time, while all remaining unvested (locked) tokens are immediately returned to the sender's account in the same transaction. See Glossary: Locked & Status and Stream detail balance display.
This warning appears when your connected Freighter wallet is targeting a different Stellar network (such as PUBLIC / mainnet) than what the application expects (such as testnet specified by NEXT_PUBLIC_NETWORK). Switch the network selector in your Freighter extension to match the app configuration. See Network Mismatch and Troubleshooting: Wallet Network Mismatch.
Next.js inlines all NEXT_PUBLIC_* configuration variables directly into the static client JavaScript bundle at build time. Modifying .env.local without running npm run build (or restarting npm run dev) will cause the client bundle to continue targeting old contract addresses or RPC endpoints. See Configuration: Switching Contracts and Troubleshooting: Stale Build.
The client enforces a single in-flight transaction lock in lib/contract.ts to prevent duplicate concurrent submissions. If network confirmation exceeds the polling threshold (30 seconds), the client raises a TransactionTimeoutError preserving the submitted transaction hash, allowing users to safely re-check confirmation status without risking a duplicate transaction. See Features and Transaction lifecycle.
TricklePay employs utility-first styling powered by Tailwind CSS v4 (@import "tailwindcss"; in app/globals.css). Styling conventions focus on shared design tokens, an inverted neutral palette for instant theming, accessible contrast standards, and responsive layout guidelines.
- Neutral Ramp (
neutral-*): The primary monochrome scale used across the application for backgrounds, cards, borders, input controls, and typography.- Dark theme (default): Backgrounds use
bg-neutral-950andbg-neutral-900, borders useborder-neutral-800andborder-neutral-700, while text hierarchy spanstext-neutral-100(headings/primary values),text-neutral-300/text-neutral-400(body/secondary text), andtext-neutral-500(muted labels/hints).
- Dark theme (default): Backgrounds use
- Brand Accent (
indigo-*):var(--color-indigo-500)serves as the primary brand accent (matching the trickle-drop mark inapp/icon.svg). It is applied to interactive keyboard focus rings (:focus-visible) and branded loading spinners (components/brand-spinner.tsx), ensuring a compliant contrast ratio of at least 3:1 across both dark and light modes. - Semantic Status Colours: Standardized status tokens maintain clear visual indicators across both light and dark themes:
- Streaming (Active): Green (
text-green-300,bg-green-950/40,border-green-700/50). - Completed: Blue (
text-blue-300,bg-blue-950/40,border-blue-700/50). - Cancelled / Destructive / Error: Red (
text-red-300,text-red-400,bg-red-950/20–40,border-red-700/50–border-red-900/50). - Pending / Locked / Notice: Amber and Neutral (
text-amber-400/80,bg-neutral-800,border-neutral-700).
- Streaming (Active): Green (
- Spacing Grid: UI spacing follows a consistent 4px rhythm using Tailwind spacing utilities (
gap-2= 8px,gap-3= 12px,gap-4= 16px,p-4= 16px,p-6= 24px,py-10= 40px). - Container Constraints: Content widths are bounded for readability: forms and stream detail views use
mx-auto max-w-2xl(672px), while dashboard listings expand across responsive grid structures (max-w-4xl,max-w-6xl). - Touch Targets & Accessibility: Interactive controls (buttons, inputs, links) maintain a minimum touch target size of 44px x 44px. Keyboard navigation is globally highlighted via
:focus-visiblewith high-contrast outlines (outline: 2px solid var(--color-indigo-500)). - Typography & Numbers: Standard system sans-serif typography is paired with
font-monofor cryptographic addresses, contract IDs, and token amounts. Numeric figures that change frequently utilizetabular-numsto prevent layout jitter during live balance ticks. - Motion & Animations: Transitions and animations respect the OS-level
prefers-reduced-motion: reducepreference by collapsing all durations to 0.01ms globally inapp/globals.css.
- Inverted Neutral Token Architecture: Dark mode is the default theme (
:root { color-scheme: dark; }). Rather than scattering redundantdark:*variant classes across every element, light mode is activated by adding a.lightclass to the root<html>element (:root.light). Under:root.light, CSS custom properties for the neutral palette (--color-neutral-50through--color-neutral-950) are inverted inapp/globals.css(e.g.,neutral-950maps to#fafafaandneutral-100maps to#171717). All components automatically adapt without per-component overrides. - Zero-Flash Theme Bootstrapping: User theme selection is stored in
localStorageundertrickle-themeand evaluated synchronously before first paint via an inline script inapp/layout.tsx(using pure resolution logic inlib/theme.ts), preventing any flash of unstyled theme (FOUC). React components subscribe to theme changes viaThemeProvider(components/theme-provider.tsx) andThemeToggle(components/theme-toggle.tsx).
The codebase follows a modular directory organization separating route declarations, presentation, reactive hooks, domain logic, and testing:
| Directory | What Belongs Here | Example |
|---|---|---|
app/ |
Next.js App Router route segments, layouts, page entrypoints, route error boundaries, loading skeletons, and global stylesheet definitions. Files here handle routing, URL params, and top-level page composition. | app/streams/[id]/page.tsx |
components/ |
Reusable React UI components, interactive widgets, form controls, status badges, and React Context providers. Code here focuses on rendering presentation, user interaction, and accessibility attributes. | components/stream-card.tsx |
hooks/ |
Custom React hooks that encapsulate stateful side effects, reactive timers, browser event listeners, and context consumption without rendering JSX markup directly. | hooks/use-accrual.ts |
lib/ |
Pure, framework-agnostic domain logic, Soroban SDK transaction builders, REST API client functions, validation rules, formatting helpers, and configuration constants. Code here avoids React hooks/JSX so it can be tested directly in headless unit tests. | lib/vesting.ts |
types/ |
TypeScript type declarations, interfaces, and data models representing backend API schemas, contract payloads, and shared domain entities. | types/stream.ts |
e2e/ |
Playwright end-to-end user journey tests, synthetic wallet listeners, mock chain fixtures, and visual regression smoke tests. | e2e/cancel-confirm.spec.ts |
docs/ |
In-depth technical specifications, protocol integration guides, API contracts, and architectural documentation. | docs/api-contract.md |
app/
layout.tsx root layout: theme bootstrap script, providers, header
loading.tsx route-level branded loading fallback
page.tsx dashboard: incoming and outgoing streams
create/page.tsx create-stream form
streams/[id]/page.tsx stream detail with live balance and actions
components/
header.tsx brand, nav, theme toggle, wallet button
theme-provider.tsx app-wide light/dark theme state via context
theme-toggle.tsx header button that flips the theme
brand-spinner.tsx branded loading indicator (bouncing dots)
wallet-provider.tsx app-wide Freighter connection state via context
wallet-button.tsx connect / address / network state
stream-card.tsx stream summary card
stream-list.tsx grid of cards with an empty state
create-form.tsx new-stream form
stream-actions.tsx withdraw and cancel buttons
transaction-progress.tsx transaction progress stage indicator
hooks/
use-accrual.ts per-second vested/withdrawable recomputation
lib/
config.ts client configuration
api.ts backend API client
contract.ts build, sign, submit, confirm contract calls with stage tracking
vesting.ts linear vesting math, mirroring the contract
format.ts amount and address formatting
theme.ts pure light/dark theme resolution logic
types/
stream.ts API response types
| Directory | Purpose |
|---|---|
app/ |
Next.js App Router pages, layouts, and route-level loading states |
components/ |
Reusable React UI components and context providers |
hooks/ |
Custom React hooks (e.g. live balance accrual) |
lib/ |
Core domain logic, SDK helpers, and configuration |
types/ |
TypeScript type definitions for API responses |
e2e/ |
Playwright end-to-end and visual regression tests |
docs/ |
Architecture and API contract documentation |
The frontend integrates with two independent backends — the read-only
tricklepay-backend REST API (lib/api.ts) and the Soroban stream contract,
invoked directly (lib/contract.ts). Request/response shapes, amount and
time encoding, the write transaction lifecycle, and the on-chain error-code
mapping are all documented in docs/api-contract.md.
- tricklepay-contracts — the Soroban streaming contract.
- tricklepay-backend — indexer and read API this client consumes.
- tricklepay-docs — architecture, security model, and contributor guides.
MIT. See LICENSE.