Skip to content

Commit 391bc94

Browse files
feat: add xBull wallet support as fallback connector (#817)
- Add useXBull hook (window.xBullSDK, no npm dep required) - Typed error codes: NO_XBULL, NOT_CONNECTED, USER_REJECTED, SIGNING_FAILED - connect(), disconnect(), signPayload() matching useFreighter shape - Rewrite useWallet as unified multi-wallet hook - Persists active wallet choice in localStorage - Exposes availableWallets (only installed wallets listed) - Delegates to Freighter or xBull based on user choice - Export isFreighterInstalled from useFreighter (was private) - Add WalletSelector component replacing FreighterConnectButton - Connect button for each installed wallet - Install prompt link for each missing wallet - Inline error banner with install link for not-installed errors - compact prop for header slot usage - Update App.tsx to use useWallet + WalletSelector - handleRelease / handleRefund use wallet.signPayload - Remove corrupted vite.config.js build artifact (real config is .ts) - 26 new tests: 16 for useXBull, 10 for WalletSelector (all passing)
1 parent 39b844c commit 391bc94

9 files changed

Lines changed: 1390 additions & 469 deletions

File tree

‎frontend/src/App.tsx‎

Lines changed: 162 additions & 192 deletions
Large diffs are not rendered by default.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, within } from '@testing-library/react';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4+
import WalletSelector from './WalletSelector';
5+
import type { WalletState } from '../hooks/useWallet';
6+
7+
// ---------------------------------------------------------------------------
8+
// Mock window globals for wallet detection
9+
// ---------------------------------------------------------------------------
10+
function installFreighter() {
11+
(window as any).freighter = {
12+
isConnected: vi.fn().mockResolvedValue({ isConnected: false }),
13+
getPublicKey: vi.fn(),
14+
signMessage: vi.fn(),
15+
getNetwork: vi.fn(),
16+
setNetwork: vi.fn(),
17+
};
18+
}
19+
20+
function uninstallFreighter() {
21+
delete (window as any).freighter;
22+
}
23+
24+
function installXBull() {
25+
(window as any).xBullSDK = {
26+
getAddress: vi.fn().mockResolvedValue({ address: 'GBULLTEST' }),
27+
signMessage: vi.fn(),
28+
};
29+
}
30+
31+
function uninstallXBull() {
32+
delete (window as any).xBullSDK;
33+
}
34+
35+
// ---------------------------------------------------------------------------
36+
// Helper: build a minimal WalletState mock
37+
// ---------------------------------------------------------------------------
38+
function makeWallet(overrides: Partial<WalletState> = {}): WalletState {
39+
return {
40+
activeWallet: null,
41+
availableWallets: [],
42+
publicKey: null,
43+
isConnected: false,
44+
connecting: false,
45+
errorMessage: null,
46+
setActiveWallet: vi.fn(),
47+
connect: vi.fn().mockResolvedValue(undefined),
48+
disconnect: vi.fn(),
49+
signPayload: vi.fn(),
50+
...overrides,
51+
};
52+
}
53+
54+
// ---------------------------------------------------------------------------
55+
// Tests
56+
// ---------------------------------------------------------------------------
57+
describe('WalletSelector – no wallets installed', () => {
58+
beforeEach(() => {
59+
uninstallFreighter();
60+
uninstallXBull();
61+
});
62+
63+
it('shows install prompts for both wallets', () => {
64+
const wallet = makeWallet();
65+
render(<WalletSelector wallet={wallet} />);
66+
67+
expect(screen.getByRole('link', { name: /install freighter/i })).toBeInTheDocument();
68+
expect(screen.getByRole('link', { name: /install xbull/i })).toBeInTheDocument();
69+
});
70+
71+
it('install prompt for xBull points to xbull.app', () => {
72+
const wallet = makeWallet();
73+
render(<WalletSelector wallet={wallet} />);
74+
const link = screen.getByRole('link', { name: /install xbull/i });
75+
expect(link).toHaveAttribute('href', 'https://xbull.app');
76+
});
77+
78+
it('install prompt for Freighter points to freighter.app', () => {
79+
const wallet = makeWallet();
80+
render(<WalletSelector wallet={wallet} />);
81+
const link = screen.getByRole('link', { name: /install freighter/i });
82+
expect(link).toHaveAttribute('href', 'https://freighter.app');
83+
});
84+
});
85+
86+
describe('WalletSelector – both wallets installed, not connected', () => {
87+
beforeEach(() => {
88+
installFreighter();
89+
installXBull();
90+
});
91+
92+
afterEach(() => {
93+
uninstallFreighter();
94+
uninstallXBull();
95+
});
96+
97+
it('shows connect buttons for both wallets', () => {
98+
const wallet = makeWallet({ availableWallets: ['freighter', 'xbull'] });
99+
render(<WalletSelector wallet={wallet} />);
100+
101+
expect(screen.getByRole('button', { name: /connect freighter/i })).toBeInTheDocument();
102+
expect(screen.getByRole('button', { name: /connect xbull/i })).toBeInTheDocument();
103+
});
104+
105+
it("clicking Connect xBull calls setActiveWallet('xbull') and connect()", async () => {
106+
const wallet = makeWallet({ availableWallets: ['freighter', 'xbull'] });
107+
render(<WalletSelector wallet={wallet} />);
108+
109+
const btn = screen.getByRole('button', { name: /connect xbull/i });
110+
fireEvent.click(btn);
111+
112+
expect(wallet.setActiveWallet).toHaveBeenCalledWith('xbull');
113+
// connect is async, wait a tick
114+
await Promise.resolve();
115+
expect(wallet.connect).toHaveBeenCalledOnce();
116+
});
117+
});
118+
119+
describe('WalletSelector – xBull connected', () => {
120+
beforeEach(() => {
121+
installXBull();
122+
uninstallFreighter();
123+
});
124+
125+
afterEach(() => {
126+
uninstallXBull();
127+
});
128+
129+
it('shows truncated public key and disconnect button when xBull is connected', () => {
130+
const wallet = makeWallet({
131+
activeWallet: 'xbull',
132+
availableWallets: ['xbull'],
133+
isConnected: true,
134+
publicKey: 'GBULLPUBLICKEY1234567890ABCDEFGHIJK',
135+
});
136+
render(<WalletSelector wallet={wallet} />);
137+
138+
// shortAddress slices 6 from start and 4 from end:
139+
// "GBULLP...HIJK"
140+
expect(screen.getByText('GBULLP...HIJK')).toBeInTheDocument();
141+
expect(screen.getByRole('button', { name: /disconnect xbull/i })).toBeInTheDocument();
142+
});
143+
144+
it('clicking disconnect calls wallet.disconnect()', () => {
145+
const wallet = makeWallet({
146+
activeWallet: 'xbull',
147+
availableWallets: ['xbull'],
148+
isConnected: true,
149+
publicKey: 'GBULLPUBLICKEY1234567890ABCDEFGHIJK',
150+
});
151+
render(<WalletSelector wallet={wallet} />);
152+
153+
const btn = screen.getByRole('button', { name: /disconnect xbull/i });
154+
fireEvent.click(btn);
155+
expect(wallet.disconnect).toHaveBeenCalledOnce();
156+
});
157+
});
158+
159+
describe('WalletSelector – error banner', () => {
160+
beforeEach(() => {
161+
uninstallFreighter();
162+
uninstallXBull();
163+
});
164+
165+
it('shows error message when errorMessage is set and compact=false', () => {
166+
const wallet = makeWallet({
167+
errorMessage: 'xBull wallet is not installed. Please install the xBull browser extension.',
168+
});
169+
render(<WalletSelector wallet={wallet} compact={false} />);
170+
171+
expect(screen.getByRole('alert')).toBeInTheDocument();
172+
expect(screen.getByRole('alert')).toHaveTextContent(/xBull wallet is not installed/i);
173+
});
174+
175+
it('includes install link in error banner for xBull not installed', () => {
176+
const wallet = makeWallet({
177+
errorMessage: 'xBull wallet is not installed. Please install the xBull browser extension.',
178+
});
179+
render(<WalletSelector wallet={wallet} compact={false} />);
180+
// There are two links with this label: the install prompt button AND the error banner link.
181+
// We verify the error banner link specifically (the one inside role="alert").
182+
const alert = screen.getByRole('alert');
183+
const link = within(alert).getByRole('link', { name: /install xbull wallet extension/i });
184+
expect(link).toHaveAttribute('href', 'https://xbull.app');
185+
});
186+
187+
it('hides error banner in compact mode', () => {
188+
const wallet = makeWallet({
189+
errorMessage: 'xBull wallet is not installed.',
190+
});
191+
render(<WalletSelector wallet={wallet} compact />);
192+
expect(screen.queryByRole('alert')).toBeNull();
193+
});
194+
});

0 commit comments

Comments
 (0)