Skip to content
Closed
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
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,47 @@ jobs:

- name: Format check
run: bun run format:check

test:
name: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.4

- name: Install dependencies
run: bun install

- name: Run unit tests
run: bun run test -- --coverage

- name: Run referral-specific tests
run: bun run test -- --reporter=verbose src/lib/referral src/components/Referral

referral-e2e:
name: referral-e2e
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.4

- name: Install dependencies
run: bun install

- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium

- name: Build app
run: bun run build

- name: Run referral E2E tests
run: bunx playwright test referral/
env:
NEXT_PUBLIC_STELLAR_NETWORK: testnet
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ public/assets/ analemma marks, wordmark, favicon

Implemented from the _Heliobond Design System_ handoff bundle exported from
Claude Design. The reference bundle lives under `.design-handoff/` (gitignored).


## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
152 changes: 152 additions & 0 deletions docs/REFERRAL_PROGRAM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Heliobond Referral Program

> Earn USDC rewards by sharing Heliobond with your friends. Both you and your
> referred friend earn rewards when they make their first green bond investment.

## Overview

The Heliobond referral program is built directly into the application and
integrated with the Stellar network. Every user gets a unique referral code
generated from their wallet address — no database required, fully deterministic.

## How It Works

1. **Get your link** — Connect your Stellar wallet and visit the Referral page
(`/referral`). Your unique referral link is generated automatically.

2. **Share** — Send your link to friends via Twitter/X, Telegram, email, or
copy it to your clipboard.

3. **They invest** — When a referred user signs up and makes their first deposit
(minimum $10 USDC), the referral is recorded on-chain.

4. **Both earn** — You and your friend each receive **$5 USDC** directly to
your Stellar wallets via the Heliobond vault smart contract.

## Reward Structure

| Action | Reward |
|---|---|
| Successful referral (first deposit ≥ $10) | **$5 USDC** each |
| Maximum referrals | **Unlimited** |
| Reward source | Heliobond vault contract |
| Network | Stellar (testnet or mainnet) |

## Technical Details

### Referral Code Generation

Referral codes are 8-character alphanumeric strings generated deterministically
from the user's Stellar wallet address (G...). The algorithm:

```typescript
// src/lib/referral.ts
function generateReferralCode(address: string): string
```

- Uses a hash of the wallet address
- Excludes ambiguous characters (I, O, 0, 1)
- Same address always produces the same code
- No database or server-side storage needed

### Share Links

Share links are pre-configured for four platforms:

| Platform | Format |
|---|---|
| Twitter/X | `https://twitter.com/intent/tweet?text=...` |
| Telegram | `https://t.me/share/url?url=...` |
| Email | `mailto:?subject=...&body=...` |
| Clipboard | Plain URL copied to clipboard |

### On-Chain Integration

When `NEXT_PUBLIC_VAULT_CONTRACT_ID` is configured:

- Referral stats are fetched from the Stellar Horizon API
- Reward payments are tracked via on-chain transaction history
- The Heliobond vault contract handles reward distribution

In **demo mode** (no contract ID), mock data is returned for development and
testing.

## Environment Variables

| Variable | Default | Description |
|---|---|---|
| `NEXT_PUBLIC_REFERRAL_REWARD_USDC` | `5` | USDC reward per successful referral |
| `NEXT_PUBLIC_APP_URL` | `https://heliobond.vercel.app` | Base URL for share links |
| `NEXT_PUBLIC_VAULT_CONTRACT_ID` | _(none)_ | Soroban contract ID for on-chain mode |
| `NEXT_PUBLIC_STELLAR_NETWORK` | `testnet` | Stellar network (`public`, `testnet`, `futurenet`) |

## Components

### `ReferralDashboard`

The main dashboard component showing:
- Referral stats (total referrals, rewards earned, pending)
- Share link with copy-to-clipboard
- Social sharing buttons
- Referral history table
- "How It Works" guide

```tsx
import { ReferralDashboard } from '@/components/ReferralDashboard'

<ReferralDashboard walletAddress="GABC..." />
```

### `ReferralShareLink`

A standalone share link component with copy button and social sharing:

```tsx
import { ReferralShareLink } from '@/components/ReferralShareLink'

<ReferralShareLink referralCode="ABCDEFGH" />
```

### Library Functions

| Function | Description |
|---|---|
| `generateReferralCode(address)` | Generate a referral code from a wallet address |
| `buildReferralLink(code)` | Build the full referral URL |
| `extractReferralCode(url)` | Extract referral code from a URL |
| `isValidReferralCode(code)` | Validate referral code format |
| `generateShareLinks(code)` | Generate share links for all platforms |
| `fetchReferralStats(address)` | Fetch referral stats (mock or on-chain) |
| `fetchReferralHistory(address)` | Fetch referral history records |
| `getReferralReward()` | Get the configured reward amount |

## Testing

```bash
# Run all referral tests
bun run test -- src/lib/referral src/components/Referral

# Run with coverage
bun run test -- --coverage src/lib/referral src/components/Referral

# Run E2E tests
bunx playwright test referral/
```

## Security Considerations

- Referral codes are derived from public wallet addresses — no private keys
involved
- Share links use standard web intents (Twitter, Telegram) — no API tokens
needed
- Rewards are distributed via audited Soroban smart contracts
- All on-chain interactions require wallet signature confirmation
- Referral codes cannot be used to reverse-engineer wallet addresses

## Future Enhancements

- [ ] Real-time referral notifications via Stellar WebSocket
- [ ] Referral leaderboard
- [ ] Multi-tier rewards (more for power referrers)
- [ ] Referral analytics dashboard
- [ ] Email invitation system with tracking
31 changes: 31 additions & 0 deletions src/app/referral/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import { Card } from '@/components/Card'
import Link from 'next/link'

export default function ReferralError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="referral-page">
<Card>
<div className="referral-page__error">
<h2>Something went wrong</h2>
<p>
We couldn&apos;t load the referral program right now. Please try again.
</p>
<div className="referral-error__actions">
<button onClick={reset} className="referral-error__retry">
Try again
</button>
<Link href="/">Go home</Link>
</div>
</div>
</Card>
</div>
)
}
113 changes: 113 additions & 0 deletions src/app/referral/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Referral page — the main entry point for the referral program.
*
* This page shows:
* - A referral dashboard with stats, share link, and history (when wallet connected)
* - A call-to-action to connect wallet (when not connected)
* - Referral code from URL query param (for referred users landing here)
*/

'use client'

import { useSearchParams } from 'next/navigation'
import { Suspense, useState, useEffect } from 'react'
import { ReferralDashboard } from '@/components/ReferralDashboard'
import { Card } from '@/components/Card'
import { Button } from '@/components/Button'

function ReferralPageContent() {
const searchParams = useSearchParams()
const refCode = searchParams.get('ref')
const [walletAddress, setWalletAddress] = useState<string>('')

useEffect(() => {
// Try to get the wallet address from localStorage or a wallet provider
// For now, this is a placeholder — in production this would connect to
// the WalletProvider context.
const stored = typeof window !== 'undefined'
? window.localStorage.getItem('heliobond_wallet_address')
: null
if (stored) setWalletAddress(stored)
}, [])

return (
<div className="referral-page">
{/* Hero section */}
<section className="referral-page__hero">
<h1>Referral Program</h1>
<p className="referral-page__hero-sub">
Earn USDC rewards by sharing Heliobond with your friends.
</p>
{refCode && (
<Card className="referral-page__ref-banner">
<p>
🎉 You were referred by a friend! Sign up and make your first
deposit to earn your reward.
</p>
<p className="referral-page__ref-code">
Referral code: <strong>{refCode}</strong>
</p>
</Card>
)}
</section>

{/* Dashboard */}
<section className="referral-page__dashboard">
{walletAddress ? (
<ReferralDashboard walletAddress={walletAddress} />
) : (
<Card>
<div className="referral-page__connect-prompt">
<h2>Start Earning Rewards</h2>
<p>
Connect your Stellar wallet to get your unique referral link and
start earning USDC for every friend who joins.
</p>
<Button disabled reason="Wallet connection coming soon">
Connect Wallet
</Button>
</div>
</Card>
)}
</section>

{/* Rewards info */}
<section className="referral-page__info">
<Card>
<h2>Reward Details</h2>
<ul>
<li>
<strong>$5 USDC</strong> reward per successful referral
</li>
<li>
Referred user must make a minimum deposit of <strong>$10</strong>
</li>
<li>
Rewards are paid directly to your Stellar wallet via the Heliobond
vault contract
</li>
<li>
No limit on the number of referrals — earn as much as you share!
</li>
</ul>
</Card>
</section>
</div>
)
}

export default function ReferralPage() {
return (
<Suspense
fallback={
<div className="referral-page" aria-busy="true">
<Card>
<p>Loading referral program...</p>
</Card>
</div>
}
>
<ReferralPageContent />
</Suspense>
)
}
Loading
Loading