Skip to content

Commit ab51601

Browse files
committed
Merge main into fix/daniel007-ai-issues-362-363-365-367, resolve conflicts in StreamCard.tsx and WalletContext.tsx
2 parents 2ec3f76 + 6d0c66b commit ab51601

27 files changed

Lines changed: 2450 additions & 94 deletions

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ jobs:
2323
- 'src/app/layout.tsx'
2424
- 'src/app/globals.css'
2525
- 'src/app/stream/new/**'
26+
- 'src/app/stream/[id]/**'
2627
- 'src/lib/theme.tsx'
2728
- 'e2e/visual-regression.spec.ts'
29+
- 'e2e/analytics-visual-regression.spec.ts'
2830
2931
lint:
3032
runs-on: ubuntu-latest
@@ -82,7 +84,7 @@ jobs:
8284
- name: Install Playwright browsers
8385
run: npx playwright install --with-deps chromium
8486
- name: Visual regression test
85-
run: npx playwright test e2e/visual-regression.spec.ts --project=chromium
87+
run: npx playwright test e2e/visual-regression.spec.ts e2e/analytics-visual-regression.spec.ts --project=chromium
8688
env:
8789
NEXT_PUBLIC_STELLAR_NETWORK: testnet
8890
NEXT_PUBLIC_CONTRACT_ID: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM

CONTRIBUTING.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,85 @@ Before submitting your PR, ensure:
250250
- [ ] Translation keys are added/updated if UI text changed
251251
- [ ] If the create-stream form changed, the dark-mode visual baseline was reviewed and updated intentionally
252252

253+
## How to Add a New SAC Token to the Token Selector Dropdown
254+
255+
When you need to support a new Stellar Asset Contract (SAC) token in the app's
256+
create-stream form, you must update the token registry and several surrounding
257+
files. This section lists every touchpoint so nothing gets missed.
258+
259+
### Files That Must Be Updated
260+
261+
| # | File | What to change |
262+
|---|------|----------------|
263+
| 1 | `src/app/stream/new/page.tsx` | Add the token to the `SUPPORTED_TOKENS` array |
264+
| 2 | `src/app/settings/page.tsx` | Add a `<option>` for the default-token dropdown |
265+
| 3 | `src/lib/sorostream.ts` (optional) | Add the token to `MOCK_CONTRACT_STATE.whitelistedTokens` for admin-area testing |
266+
| 4 | `src/lib/streamTemplates.ts` (optional) | Add stream templates that use the new token |
267+
| 5 | `src/locales/en.json` (optional) | If the token name appears in user-facing text, add a translation key |
268+
269+
### Worked Example — Adding `EURC`
270+
271+
#### Step 1 – Register the token
272+
273+
Open `src/app/stream/new/page.tsx` and locate the `SUPPORTED_TOKENS` array
274+
(around line 35). Append a new entry:
275+
276+
```ts
277+
const SUPPORTED_TOKENS = [
278+
{ symbol: "USDC", name: "USD Coin", address: "CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU" },
279+
{ symbol: "XLM", name: "Stellar Lumens", address: "native" },
280+
{ symbol: "AQUA", name: "Aquarius", address: "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA" },
281+
{ symbol: "yXLM", name: "Yield XLM", address: "GARDNV3Q7YGT4AKSDF25LT32YSCCW4EV22Y2TV3I2PU2MMXJTEDL5T55" },
282+
// 👇 Add the new token here
283+
{ symbol: "EURC", name: "Euro Coin", address: "CCW67SZVBUIC7FNJPZXNBGRAV3HMPF4Y7XYE44CUMVHWKRSOGF4RDOIV" },
284+
] as const;
285+
```
286+
287+
Each entry requires:
288+
- **`symbol`** — ticker shown in the dropdown and throughout the UI (e.g. `EURC`)
289+
- **`name`** — human-readable token name (e.g. `Euro Coin`)
290+
- **`address`** — Stellar contract address of the SAC token (use `"native"` for XLM)
291+
292+
#### Step 2 – Expose in Settings
293+
294+
Open `src/app/settings/page.tsx`, find the `<select>` for the default token
295+
(around line 275), and add a new `<option>`:
296+
297+
```tsx
298+
<option value="EURC">EURC (Euro Coin)</option>
299+
```
300+
301+
This lets users set the new token as their default for the create-stream form.
302+
303+
#### Step 3 – Add to token whitelist (Admin area)
304+
305+
If the token should appear in the admin panel's whitelist for testing, open
306+
`src/lib/sorostream.ts`, find `MOCK_CONTRACT_STATE.whitelistedTokens`, and add
307+
the symbol:
308+
309+
```ts
310+
whitelistedTokens: ["USDC", "XLM", "AQUA", "EURC"],
311+
```
312+
313+
#### Step 4 – Verify
314+
315+
1. Run `npm run build` — the build must pass.
316+
2. Run `npm run lint` — no new lint violations.
317+
3. Start the dev server (`npm run dev`), open `/stream/new`, and confirm the
318+
new token appears in the dropdown.
319+
4. Create a stream with the new token and verify the stream detail page shows
320+
the correct token symbol.
321+
322+
### Important Notes
323+
324+
- **No icon asset needed** — the dropdown uses text-only option labels; icons
325+
are not rendered for individual tokens.
326+
- **The token address should be the Stellar Asset Contract address**, not the
327+
classic asset issuer.
328+
- On testnet, make sure the token has been deployed before adding it.
329+
- After adding a token here, run the Playwright E2E tests to verify the
330+
create-stream flow still works: `npm run test:e2e`.
331+
253332
## Getting Help
254333

255334
- Check existing issues for similar problems

components/DelegatesSection.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
5+
const STORAGE_KEY = "sorostream-delegates";
6+
7+
interface Delegate {
8+
address: string;
9+
addedAt: string;
10+
/** Stream IDs this delegate can manage (empty = all streams) */
11+
streamIds: string[];
12+
}
13+
14+
function loadDelegates(): Delegate[] {
15+
if (typeof window === "undefined") return [];
16+
try {
17+
const raw = localStorage.getItem(STORAGE_KEY);
18+
return raw ? (JSON.parse(raw) as Delegate[]) : [];
19+
} catch {
20+
return [];
21+
}
22+
}
23+
24+
function saveDelegates(list: Delegate[]): void {
25+
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
26+
}
27+
28+
function truncateAddr(addr: string): string {
29+
if (addr.length <= 12) return addr;
30+
return `${addr.slice(0, 6)}${addr.slice(-4)}`;
31+
}
32+
33+
export default function DelegatesSection() {
34+
const [delegates, setDelegates] = useState<Delegate[]>([]);
35+
const [input, setInput] = useState("");
36+
const [inputError, setInputError] = useState("");
37+
const [adding, setAdding] = useState(false);
38+
39+
useEffect(() => {
40+
setDelegates(loadDelegates());
41+
}, []);
42+
43+
function validate(addr: string): string {
44+
if (!addr.trim()) return "Address is required.";
45+
if (!/^G[A-Z2-7]{55}$/.test(addr.trim()))
46+
return "Must be a valid Stellar public key (starts with G, 56 chars).";
47+
if (delegates.some((d) => d.address === addr.trim()))
48+
return "This address is already a delegate.";
49+
return "";
50+
}
51+
52+
async function handleAdd() {
53+
const err = validate(input);
54+
if (err) { setInputError(err); return; }
55+
setAdding(true);
56+
// Simulate SDK call
57+
await new Promise((r) => setTimeout(r, 500));
58+
const next: Delegate[] = [
59+
...delegates,
60+
{ address: input.trim(), addedAt: new Date().toISOString(), streamIds: [] },
61+
];
62+
saveDelegates(next);
63+
setDelegates(next);
64+
setInput("");
65+
setInputError("");
66+
setAdding(false);
67+
}
68+
69+
function handleRevoke(address: string) {
70+
const next = delegates.filter((d) => d.address !== address);
71+
saveDelegates(next);
72+
setDelegates(next);
73+
}
74+
75+
return (
76+
<div className="bg-gray-800 rounded-xl p-6 space-y-4 mb-8">
77+
<div>
78+
<h2 className="text-lg font-semibold">Delegation Management</h2>
79+
<p className="text-gray-400 text-sm mt-1">
80+
Grant other addresses the ability to manage your streams on your behalf.
81+
</p>
82+
</div>
83+
84+
{/* Add delegate */}
85+
<div className="space-y-2">
86+
<label htmlFor="delegate-address" className="text-gray-200 text-sm font-medium block">
87+
Delegate Address
88+
</label>
89+
<div className="flex gap-2">
90+
<input
91+
id="delegate-address"
92+
type="text"
93+
value={input}
94+
onChange={(e) => { setInput(e.target.value); setInputError(""); }}
95+
onKeyDown={(e) => { if (e.key === "Enter") void handleAdd(); }}
96+
placeholder="G… (Stellar public key)"
97+
className="flex-1 bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500"
98+
aria-invalid={!!inputError}
99+
aria-describedby={inputError ? "delegate-input-error" : undefined}
100+
/>
101+
<button
102+
onClick={() => void handleAdd()}
103+
disabled={adding}
104+
className="bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-green-700 disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500"
105+
>
106+
{adding ? "Adding…" : "Add Delegate"}
107+
</button>
108+
</div>
109+
{inputError && (
110+
<p id="delegate-input-error" className="text-red-400 text-xs">{inputError}</p>
111+
)}
112+
</div>
113+
114+
{/* Current delegates */}
115+
{delegates.length === 0 ? (
116+
<p className="text-gray-500 text-sm text-center py-4">No delegates added yet.</p>
117+
) : (
118+
<ul className="space-y-2">
119+
{delegates.map((d) => (
120+
<li
121+
key={d.address}
122+
className="flex items-center gap-3 bg-gray-700/50 rounded-lg px-4 py-3"
123+
>
124+
<div className="flex-1 min-w-0">
125+
<p className="text-white text-sm font-mono truncate" title={d.address}>
126+
{truncateAddr(d.address)}
127+
</p>
128+
<p className="text-gray-500 text-xs mt-0.5">
129+
{d.streamIds.length > 0
130+
? `Can manage streams: ${d.streamIds.join(", ")}`
131+
: "Can manage all streams"}
132+
</p>
133+
</div>
134+
<button
135+
onClick={() => handleRevoke(d.address)}
136+
className="text-red-400 hover:text-red-300 text-sm px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 rounded"
137+
aria-label={`Revoke delegate ${d.address}`}
138+
>
139+
Revoke
140+
</button>
141+
</li>
142+
))}
143+
</ul>
144+
)}
145+
</div>
146+
);
147+
}

0 commit comments

Comments
 (0)