Skip to content

Commit cbe3cd2

Browse files
Anubhav SinghAnubhav Singh
authored andcommitted
feat: add Lobstr wallet, dynamic portfolio comparison, OG meta tags, and embed widget params
1 parent 310e8df commit cbe3cd2

13 files changed

Lines changed: 583 additions & 28 deletions

frontend/package-lock.json

Lines changed: 71 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"react": "^18.2.0",
3232
"react-dom": "^18.2.0",
3333
"react-i18next": "^13.5.0",
34+
"react-router-dom": "^7.18.2",
3435
"recharts": "^2.8.0",
3536
"zod": "^4.3.6"
3637
},
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2+
import { render, screen, cleanup, fireEvent, waitFor } from '@testing-library/react'
3+
import { WalletPicker } from './WalletPicker'
4+
5+
const mockWalletManager = vi.hoisted(() => ({
6+
connect: vi.fn(),
7+
disconnect: vi.fn(),
8+
getWalletType: vi.fn(),
9+
getPublicKey: vi.fn(),
10+
}))
11+
12+
vi.mock('../utils/walletManager', () => ({
13+
walletManager: mockWalletManager,
14+
}))
15+
16+
vi.mock('../lib/wallet', () => ({
17+
SUPPORTED_WALLETS: [
18+
{ name: 'Freighter', type: 'freighter', installUrl: 'https://www.freighter.app/' },
19+
{ name: 'Rabet', type: 'rabet', installUrl: 'https://rabet.io/' },
20+
{ name: 'xBull', type: 'xbull', installUrl: 'https://xbull.app/' },
21+
{ name: 'LOBSTR', type: 'lobstr', installUrl: 'https://lobstr.co/' },
22+
{ name: 'WalletConnect', type: 'walletconnect', installUrl: 'https://walletconnect.com/' },
23+
],
24+
getLastUsedWallet: vi.fn(() => null),
25+
setLastUsedWallet: vi.fn(),
26+
clearLastUsedWallet: vi.fn(),
27+
}))
28+
29+
describe('WalletPicker', () => {
30+
let mockOnConnect: (publicKey: string, walletType: string) => void
31+
let mockOnError: (error: string) => void
32+
33+
beforeEach(() => {
34+
cleanup()
35+
vi.restoreAllMocks()
36+
37+
mockOnConnect = vi.fn()
38+
mockOnError = vi.fn()
39+
40+
mockWalletManager.getWalletType.mockReturnValue(null)
41+
})
42+
43+
afterEach(() => {
44+
cleanup()
45+
})
46+
47+
it('renders all supported wallet options', () => {
48+
render(<WalletPicker onConnect={mockOnConnect} onError={mockOnError} />)
49+
50+
expect(screen.getByText('Freighter')).toBeTruthy()
51+
expect(screen.getByText('Rabet')).toBeTruthy()
52+
expect(screen.getByText('xBull')).toBeTruthy()
53+
expect(screen.getByText('LOBSTR')).toBeTruthy()
54+
expect(screen.getByText('WalletConnect')).toBeTruthy()
55+
})
56+
57+
it('detects Lobstr as installed when window.lobstr is available', () => {
58+
window.lobstr = {} as any
59+
render(<WalletPicker onConnect={mockOnConnect} onError={mockOnError} />)
60+
expect(screen.getByText('Installed')).toBeTruthy()
61+
delete (window as any).lobstr
62+
})
63+
64+
it('calls walletManager.connect with lobstr when Lobstr is clicked', async () => {
65+
window.lobstr = {} as any
66+
const testPublicKey = 'GAlobstrtest1234567890abcdef1234567890abcdef1234567890abcdef'
67+
mockWalletManager.connect.mockResolvedValue(testPublicKey)
68+
69+
render(<WalletPicker onConnect={mockOnConnect} onError={mockOnError} />)
70+
71+
const lobstrButton = screen.getByText('LOBSTR').closest('button')
72+
expect(lobstrButton).toBeTruthy()
73+
74+
fireEvent.click(lobstrButton!)
75+
76+
await waitFor(() => {
77+
expect(mockWalletManager.connect).toHaveBeenCalledWith('lobstr')
78+
expect(mockOnConnect).toHaveBeenCalledWith(testPublicKey, 'LOBSTR')
79+
expect(mockOnError).not.toHaveBeenCalled()
80+
})
81+
82+
delete (window as any).lobstr
83+
})
84+
})

frontend/src/components/WalletPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const WalletPicker: React.FC<WalletPickerProps> = ({ onConnect, onError,
2727
case 'freighter': return !!window.freighter
2828
case 'rabet': return !!window.rabet
2929
case 'xbull': return !!window.xBull
30-
case 'lobstr': return false // LOBSTR is usually mobile or extension
30+
case 'lobstr': return !!window.lobstr
3131
case 'walletconnect': return false // Requires QR flow
3232
default: return false
3333
}

frontend/src/pages/Compare.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const Compare: React.FC<PortfolioCompareProps> = ({ onNavigate, publicKey }) =>
3838
const togglePortfolio = (portfolioId: string) => {
3939
if (selectedPortfolioIds.includes(portfolioId)) {
4040
setSelectedPortfolioIds(prev => prev.filter(id => id !== portfolioId))
41-
} else if (selectedPortfolioIds.length < 3) {
41+
} else if (selectedPortfolioIds.length < 5) {
4242
setSelectedPortfolioIds(prev => [...prev, portfolioId])
4343
}
4444
}
@@ -101,7 +101,7 @@ const Compare: React.FC<PortfolioCompareProps> = ({ onNavigate, publicKey }) =>
101101
</div>
102102
</div>
103103
<div className="text-sm text-gray-600 dark:text-gray-400">
104-
{selectedPortfolioIds.length}/3 {t('compare.selectPortfolios')}
104+
{selectedPortfolioIds.length}/5 {t('compare.selectPortfolios')}
105105
</div>
106106
</div>
107107

@@ -142,7 +142,7 @@ const Compare: React.FC<PortfolioCompareProps> = ({ onNavigate, publicKey }) =>
142142
<div className="space-y-6">
143143
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
144144
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{t('compare.allocation')}</h2>
145-
<div className={`grid gap-6 ${selectedPortfolioIds.length === 2 ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1 md:grid-cols-3'}`}>
145+
<div className={`grid gap-6 ${selectedPortfolioIds.length <= 2 ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'}`}>
146146
{selectedPortfolios.map((portfolio, index) => {
147147
const allocationData = getAllocationData(portfolio)
148148
return (

0 commit comments

Comments
 (0)