Skip to content

Commit e33a41a

Browse files
authored
Merge pull request #147 from jabir-dev788/FIX-vero-guardian-dashboard
Add dev-only tool to mock wallet publicKey (reduces reconnect friction)
2 parents d89d2d7 + 29cf828 commit e33a41a

4 files changed

Lines changed: 215 additions & 1 deletion

File tree

src/app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ThemeProvider } from '@/context/ThemeContext';
1111
import { I18nProvider } from '@/i18n';
1212
import { NetworkProvider } from '@/context/NetworkContext';
1313
import { SocketIOProvider } from '@/context/SocketIOContext';
14+
import DevWalletSwitcher from '@/components/DevWalletSwitcher';
1415

1516
const inter = Inter({ subsets: ['latin'] });
1617

@@ -41,6 +42,8 @@ export default function RootLayout({ children }: RootLayoutProps): ReactElement
4142
</ErrorProvider>
4243
</SocketIOProvider>
4344
</RoleProvider>
45+
{/* DEV-ONLY: renders null in production */}
46+
<DevWalletSwitcher />
4447
</WalletProvider>
4548
</NetworkProvider>
4649
</ThemeProvider>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { useWallet } from '@/context/WalletContext';
5+
import { KeyRound, X, ChevronDown, ChevronUp } from 'lucide-react';
6+
7+
/**
8+
* DEV-ONLY floating panel that lets you inject an arbitrary Stellar public key
9+
* directly into WalletContext — no wallet extension required.
10+
*
11+
* Both this component AND `WalletContext.setMockPublicKey` independently
12+
* check `process.env.NODE_ENV`, ensuring the feature is a guaranteed no-op
13+
* in production even if only one guard were to be removed.
14+
*/
15+
export default function DevWalletSwitcher() {
16+
// Independent production guard (layer 1 of 2).
17+
// Returning null *before* any hooks would violate the Rules of Hooks, so we
18+
// place the early-return after the hook declarations.
19+
const { setMockPublicKey } = useWallet();
20+
const [collapsed, setCollapsed] = useState(true);
21+
const [inputValue, setInputValue] = useState('');
22+
const [dismissed, setDismissed] = useState(false);
23+
24+
// Layer 1 guard: component renders nothing in production.
25+
if (process.env.NODE_ENV === 'production') {
26+
return null;
27+
}
28+
29+
if (dismissed) return null;
30+
31+
return (
32+
<div
33+
className="fixed bottom-4 right-4 z-[9999] w-72 rounded-xl border border-amber-400/40 bg-slate-900/95 shadow-2xl backdrop-blur-sm"
34+
role="complementary"
35+
aria-label="Dev wallet switcher"
36+
>
37+
{/* ── Header bar ─────────────────────────────────────────────────────── */}
38+
<div className="flex items-center justify-between px-3 py-2 border-b border-slate-700/60">
39+
<button
40+
type="button"
41+
onClick={() => setCollapsed((c) => !c)}
42+
className="flex items-center gap-1.5 text-amber-400 text-xs font-semibold tracking-wide uppercase focus:outline-none focus:ring-2 focus:ring-amber-400/60 rounded"
43+
aria-expanded={!collapsed}
44+
aria-controls="dev-wallet-switcher-body"
45+
>
46+
<KeyRound className="w-3.5 h-3.5" aria-hidden="true" />
47+
Dev Wallet
48+
{collapsed
49+
? <ChevronDown className="w-3.5 h-3.5 ml-0.5" aria-hidden="true" />
50+
: <ChevronUp className="w-3.5 h-3.5 ml-0.5" aria-hidden="true" />}
51+
</button>
52+
53+
<button
54+
type="button"
55+
onClick={() => setDismissed(true)}
56+
aria-label="Dismiss dev wallet panel"
57+
className="text-slate-500 hover:text-slate-300 transition-colors focus:outline-none focus:ring-2 focus:ring-amber-400/60 rounded"
58+
>
59+
<X className="w-3.5 h-3.5" aria-hidden="true" />
60+
</button>
61+
</div>
62+
63+
{/* ── Collapsible body ────────────────────────────────────────────────── */}
64+
{!collapsed && (
65+
<div
66+
id="dev-wallet-switcher-body"
67+
className="px-3 py-3 flex flex-col gap-2"
68+
>
69+
<label
70+
htmlFor="dev-wallet-input"
71+
className="text-xs text-slate-400 font-medium"
72+
>
73+
Stellar public key
74+
</label>
75+
<input
76+
id="dev-wallet-input"
77+
type="text"
78+
value={inputValue}
79+
onChange={(e) => setInputValue(e.target.value)}
80+
placeholder="G..."
81+
spellCheck={false}
82+
className="w-full rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 text-xs font-mono text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-amber-400/60 transition-colors"
83+
/>
84+
<button
85+
type="button"
86+
onClick={() => {
87+
if (inputValue.trim()) {
88+
setMockPublicKey(inputValue.trim());
89+
}
90+
}}
91+
disabled={!inputValue.trim()}
92+
className="flex items-center justify-center gap-2 rounded-lg bg-amber-500 hover:bg-amber-400 disabled:opacity-40 disabled:cursor-not-allowed text-slate-900 text-xs font-semibold px-3 py-2 transition-colors focus:outline-none focus:ring-2 focus:ring-amber-400/60 focus:ring-offset-2 focus:ring-offset-slate-900"
93+
>
94+
<KeyRound className="w-3.5 h-3.5" aria-hidden="true" />
95+
Apply key
96+
</button>
97+
<p className="text-[10px] text-slate-500 leading-snug">
98+
⚠ Dev only — this panel and the underlying method are both no-ops in production.
99+
</p>
100+
</div>
101+
)}
102+
</div>
103+
);
104+
}

src/context/WalletContext.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ interface WalletContextType {
4343
/** Connect with a specific provider; defaults to Freighter when omitted. */
4444
connect: (providerId?: WalletProviderId) => Promise<void>;
4545
disconnect: () => void;
46+
/**
47+
* DEV-ONLY: Manually override the active public key without going through
48+
* a real wallet handshake. Always a no-op in production.
49+
*/
50+
setMockPublicKey: (key: string) => void;
4651
}
4752

4853
const WalletContext = createContext<WalletContextType | undefined>(undefined);
@@ -280,6 +285,17 @@ export function WalletProvider({ children }: { children: ReactNode }) {
280285
clearWalletState();
281286
}, [publicKey, activeProvider, clearWalletState, emit]);
282287

288+
/** DEV-ONLY: override the active public key for local development convenience. */
289+
const setMockPublicKey = useCallback(
290+
(key: string) => {
291+
if (process.env.NODE_ENV === 'production') {
292+
return;
293+
}
294+
applyVerifiedPublicKey(key, DEFAULT_WALLET_PROVIDER_ID);
295+
},
296+
[applyVerifiedPublicKey]
297+
);
298+
283299
useEffect(() => {
284300
const unsubscribe = sessionManager.subscribe(() => {
285301
disconnect();
@@ -308,8 +324,9 @@ export function WalletProvider({ children }: { children: ReactNode }) {
308324
availableProviders,
309325
connect,
310326
disconnect,
327+
setMockPublicKey,
311328
}),
312-
[activeProvider, availableProviders, connect, disconnect, error, isLoading, publicKey, reputation]
329+
[activeProvider, availableProviders, connect, disconnect, error, isLoading, publicKey, reputation, setMockPublicKey]
313330
);
314331

315332
return <WalletContext.Provider value={value}>{children}</WalletContext.Provider>;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// src/context/__tests__/WalletContext.setMockPublicKey.test.ts
2+
//
3+
// Mirrors the test pattern established in src/utils/__tests__/diff.test.ts.
4+
// Verifies that setMockPublicKey is a guaranteed no-op when
5+
// NODE_ENV === 'production'.
6+
7+
// Store the original NODE_ENV so we can restore it after each test.
8+
const originalNodeEnv = process.env.NODE_ENV;
9+
10+
afterEach(() => {
11+
// Restore env to avoid cross-test contamination.
12+
Object.defineProperty(process.env, 'NODE_ENV', {
13+
writable: true,
14+
configurable: true,
15+
value: originalNodeEnv,
16+
});
17+
jest.resetAllMocks();
18+
});
19+
20+
describe('WalletContext – setMockPublicKey production guard', () => {
21+
/**
22+
* We test the guard logic in isolation rather than wiring up the full React
23+
* context, because the constraint is pure conditional logic that does not
24+
* require rendering.
25+
*
26+
* The extracted helper below reproduces the exact logic used in the
27+
* WalletProvider implementation:
28+
*
29+
* if (process.env.NODE_ENV === 'production') return;
30+
* applyVerifiedPublicKey(key, DEFAULT_WALLET_PROVIDER_ID);
31+
*/
32+
function makeSetMockPublicKey(applyVerifiedPublicKey: (key: string, providerId: string) => void) {
33+
const DEFAULT_WALLET_PROVIDER_ID = 'freighter';
34+
return function setMockPublicKey(key: string) {
35+
if (process.env.NODE_ENV === 'production') {
36+
return;
37+
}
38+
applyVerifiedPublicKey(key, DEFAULT_WALLET_PROVIDER_ID);
39+
};
40+
}
41+
42+
test('does NOT call applyVerifiedPublicKey when NODE_ENV is production', () => {
43+
Object.defineProperty(process.env, 'NODE_ENV', {
44+
writable: true,
45+
configurable: true,
46+
value: 'production',
47+
});
48+
49+
const applyVerifiedPublicKey = jest.fn();
50+
const setMockPublicKey = makeSetMockPublicKey(applyVerifiedPublicKey);
51+
52+
setMockPublicKey('GABCDE12345');
53+
54+
expect(applyVerifiedPublicKey).not.toHaveBeenCalled();
55+
});
56+
57+
test('DOES call applyVerifiedPublicKey when NODE_ENV is development', () => {
58+
Object.defineProperty(process.env, 'NODE_ENV', {
59+
writable: true,
60+
configurable: true,
61+
value: 'development',
62+
});
63+
64+
const applyVerifiedPublicKey = jest.fn();
65+
const setMockPublicKey = makeSetMockPublicKey(applyVerifiedPublicKey);
66+
const testKey = 'GABCDE12345';
67+
68+
setMockPublicKey(testKey);
69+
70+
expect(applyVerifiedPublicKey).toHaveBeenCalledTimes(1);
71+
expect(applyVerifiedPublicKey).toHaveBeenCalledWith(testKey, 'freighter');
72+
});
73+
74+
test('DOES call applyVerifiedPublicKey when NODE_ENV is test', () => {
75+
Object.defineProperty(process.env, 'NODE_ENV', {
76+
writable: true,
77+
configurable: true,
78+
value: 'test',
79+
});
80+
81+
const applyVerifiedPublicKey = jest.fn();
82+
const setMockPublicKey = makeSetMockPublicKey(applyVerifiedPublicKey);
83+
const testKey = 'GXYZ9876';
84+
85+
setMockPublicKey(testKey);
86+
87+
expect(applyVerifiedPublicKey).toHaveBeenCalledTimes(1);
88+
expect(applyVerifiedPublicKey).toHaveBeenCalledWith(testKey, 'freighter');
89+
});
90+
});

0 commit comments

Comments
 (0)