Skip to content

Commit d7408bd

Browse files
Copilot0xrinegade
andcommitted
Implement major wallet improvements: API security, timer cleanup, popup handling, focus management
Co-authored-by: 0xrinegade <[email protected]>
1 parent 14c5cda commit d7408bd

File tree

5 files changed

+581
-98
lines changed

5 files changed

+581
-98
lines changed

.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22

33
# Para SDK API Key (required for OAuth authentication)
44
# Get your API key from: https://para.build/
5+
#
6+
# ⚠️ SECURITY WARNING:
7+
# - NEXT_PUBLIC_PARA_API_KEY is exposed to the browser (public prefix)
8+
# - Only use API keys with read-only/limited scopes for frontend
9+
# - Never include admin/write permissions in this key
10+
# - Ensure the key only has OAuth authentication permissions
511
NEXT_PUBLIC_PARA_API_KEY=your_para_api_key_here
612

713
# Alternative environment variable name (fallback)
814
PARA_API_KEY=your_para_api_key_here
915

16+
# ⚠️ IMPORTANT: Never commit .env.local with real API keys to version control!
17+
1018
# Network Configuration
1119
# Options: localnet, devnet, mainnet-beta
1220
NEXT_PUBLIC_SOLANA_NETWORK=devnet
@@ -16,4 +24,5 @@ NODE_ENV=development
1624
NEXT_PUBLIC_APP_ENV=development
1725

1826
# Example .env.local file (copy this to .env.local and update values)
19-
# NEXT_PUBLIC_PARA_API_KEY=your_actual_api_key_here
27+
# ⚠️ NEVER commit .env.local to git - it should contain your real API keys
28+
# NEXT_PUBLIC_PARA_API_KEY=your_actual_api_key_here

src/components/ReconnectionModal.js

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,95 @@
22
* Reconnection Modal Component
33
*
44
* Displays reconnection progress to users with cancellation option
5+
* Includes proper focus management and accessibility features
56
*/
67

7-
import React from 'react';
8+
import React, { useEffect, useRef } from 'react';
89

910
/**
10-
* Reconnection progress modal
11+
* Trap focus within modal for accessibility
12+
* @param {HTMLElement} element - Modal element to trap focus within
13+
*/
14+
const trapFocus = (element) => {
15+
const focusableElements = element.querySelectorAll(
16+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
17+
);
18+
const firstElement = focusableElements[0];
19+
const lastElement = focusableElements[focusableElements.length - 1];
20+
21+
const handleTabKey = (e) => {
22+
if (e.key !== 'Tab') return;
23+
24+
if (e.shiftKey) {
25+
if (document.activeElement === firstElement) {
26+
lastElement.focus();
27+
e.preventDefault();
28+
}
29+
} else {
30+
if (document.activeElement === lastElement) {
31+
firstElement.focus();
32+
e.preventDefault();
33+
}
34+
}
35+
};
36+
37+
element.addEventListener('keydown', handleTabKey);
38+
return () => element.removeEventListener('keydown', handleTabKey);
39+
};
40+
41+
/**
42+
* Reconnection progress modal with accessibility features
1143
* @param {Object} props - Component props
1244
* @param {boolean} props.isVisible - Whether modal is visible
1345
* @param {Object} props.progress - Reconnection progress state
1446
* @param {Function} props.onCancel - Cancel callback
1547
*/
1648
export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
49+
const modalRef = useRef(null);
50+
const previousActiveElementRef = useRef(null);
51+
52+
// Focus management
53+
useEffect(() => {
54+
if (isVisible && modalRef.current) {
55+
// Store currently focused element
56+
previousActiveElementRef.current = document.activeElement;
57+
58+
// Focus the modal container
59+
modalRef.current.focus();
60+
61+
// Set up focus trap
62+
const removeFocusTrap = trapFocus(modalRef.current);
63+
64+
// Prevent background scrolling
65+
document.body.style.overflow = 'hidden';
66+
67+
return () => {
68+
// Cleanup
69+
removeFocusTrap();
70+
document.body.style.overflow = '';
71+
72+
// Restore previous focus
73+
if (previousActiveElementRef.current) {
74+
previousActiveElementRef.current.focus();
75+
}
76+
};
77+
}
78+
}, [isVisible]);
79+
80+
// Handle escape key
81+
useEffect(() => {
82+
const handleEscape = (e) => {
83+
if (e.key === 'Escape' && isVisible && progress.canCancel) {
84+
onCancel();
85+
}
86+
};
87+
88+
if (isVisible) {
89+
document.addEventListener('keydown', handleEscape);
90+
return () => document.removeEventListener('keydown', handleEscape);
91+
}
92+
}, [isVisible, progress.canCancel, onCancel]);
93+
1794
if (!isVisible) return null;
1895

1996
const { attempt, maxAttempts, nextRetryIn, canCancel } = progress;
@@ -27,12 +104,22 @@ export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
27104
const strokeDashoffset = circumference - (progressPercentage / 100) * circumference;
28105

29106
return (
30-
<div className="reconnection-modal">
31-
<div className="modal-content">
107+
<div
108+
className="reconnection-modal"
109+
role="dialog"
110+
aria-modal="true"
111+
aria-labelledby="reconnection-title"
112+
aria-describedby="reconnection-description"
113+
>
114+
<div
115+
className="modal-content"
116+
ref={modalRef}
117+
tabIndex={-1}
118+
>
32119
{nextRetryIn > 0 ? (
33120
<>
34121
{/* Countdown display */}
35-
<div className="progress-ring">
122+
<div className="progress-ring" aria-hidden="true">
36123
<svg width="80" height="80" className="transform -rotate-90">
37124
<circle
38125
cx="40"
@@ -50,32 +137,34 @@ export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
50137
/>
51138
</svg>
52139
<div className="absolute inset-0 flex items-center justify-center">
53-
<span className="text-2xl font-bold text-gray-700">{nextRetryIn}</span>
140+
<span className="text-2xl font-bold text-gray-700" aria-live="polite">
141+
{nextRetryIn}
142+
</span>
54143
</div>
55144
</div>
56145

57-
<h3 className="text-lg font-semibold text-gray-900 mb-2">
146+
<h3 id="reconnection-title" className="text-lg font-semibold text-gray-900 mb-2">
58147
Connection Lost
59148
</h3>
60-
<p className="text-gray-600 mb-4">
149+
<p id="reconnection-description" className="text-gray-600 mb-4">
61150
Attempting to reconnect in {nextRetryIn} second{nextRetryIn !== 1 ? 's' : ''}...
62151
</p>
63-
<p className="text-sm text-gray-500 mb-6">
152+
<p className="text-sm text-gray-500 mb-6" aria-live="polite">
64153
Attempt {attempt} of {maxAttempts}
65154
</p>
66155
</>
67156
) : (
68157
<>
69158
{/* Connecting display */}
70-
<div className="spinner"></div>
159+
<div className="spinner" aria-hidden="true"></div>
71160

72-
<h3 className="text-lg font-semibold text-gray-900 mb-2">
161+
<h3 id="reconnection-title" className="text-lg font-semibold text-gray-900 mb-2">
73162
Reconnecting...
74163
</h3>
75-
<p className="text-gray-600 mb-4">
164+
<p id="reconnection-description" className="text-gray-600 mb-4">
76165
Attempting to restore connection
77166
</p>
78-
<p className="text-sm text-gray-500 mb-6">
167+
<p className="text-sm text-gray-500 mb-6" aria-live="polite">
79168
Attempt {attempt} of {maxAttempts}
80169
</p>
81170
</>
@@ -85,13 +174,15 @@ export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
85174
<div className="flex gap-3 justify-center">
86175
<button
87176
onClick={onCancel}
88-
className="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium"
177+
className="px-4 py-2 text-gray-600 hover:text-gray-800 font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
178+
aria-label="Cancel reconnection"
89179
>
90180
Cancel
91181
</button>
92182
<button
93183
onClick={() => window.location.reload()}
94-
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 font-medium"
184+
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
185+
aria-label="Refresh page to retry connection"
95186
>
96187
Refresh Page
97188
</button>
@@ -102,7 +193,8 @@ export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
102193
<div className="flex gap-3 justify-center">
103194
<button
104195
onClick={() => window.location.reload()}
105-
className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 font-medium"
196+
className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 font-medium focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
197+
aria-label="Refresh page - all reconnection attempts failed"
106198
>
107199
Refresh Page
108200
</button>
@@ -113,4 +205,4 @@ export const ReconnectionModal = ({ isVisible, progress, onCancel }) => {
113205
);
114206
};
115207

116-
export default ReconnectionModal;
208+
export default ReconnectionModal;

src/config/networks.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* SVM Networks Configuration
3+
*
4+
* Centralized network configuration for all Solana Virtual Machine networks
5+
* including endpoints, fallbacks, and connection parameters.
6+
*/
7+
8+
import { clusterApiUrl } from '@solana/web3.js';
9+
10+
/**
11+
* SVM Networks configuration with primary and fallback endpoints
12+
*/
13+
export const SVM_NETWORKS = {
14+
'solana': {
15+
name: 'Solana',
16+
endpoint: clusterApiUrl('devnet'),
17+
programId: 'YOUR_SOLANA_PROGRAM_ID',
18+
icon: '/images/solana-logo.svg',
19+
color: '#9945FF',
20+
explorerUrl: 'https://explorer.solana.com',
21+
fallbackEndpoints: [
22+
'https://api.devnet.solana.com',
23+
'https://solana-devnet-rpc.allthatnode.com',
24+
],
25+
connectionConfig: {
26+
commitment: 'confirmed',
27+
confirmTransactionInitialTimeout: 60000,
28+
}
29+
},
30+
'sonic': {
31+
name: 'Sonic',
32+
endpoint: 'https://sonic-api.example.com',
33+
programId: 'YOUR_SONIC_PROGRAM_ID',
34+
icon: '/images/sonic-logo.svg',
35+
color: '#00C2FF',
36+
explorerUrl: 'https://explorer.sonic.example.com',
37+
fallbackEndpoints: [],
38+
connectionConfig: {
39+
commitment: 'confirmed',
40+
confirmTransactionInitialTimeout: 60000,
41+
}
42+
}
43+
};
44+
45+
/**
46+
* Get network configuration by name
47+
* @param {string} networkName - Network name (e.g., 'solana', 'sonic')
48+
* @returns {Object|null} Network configuration or null if not found
49+
*/
50+
export const getNetworkConfig = (networkName) => {
51+
return SVM_NETWORKS[networkName] || null;
52+
};
53+
54+
/**
55+
* Get default network configuration (Solana)
56+
* @returns {Object} Default network configuration
57+
*/
58+
export const getDefaultNetworkConfig = () => {
59+
return SVM_NETWORKS.solana;
60+
};
61+
62+
/**
63+
* Get all available network names
64+
* @returns {string[]} Array of network names
65+
*/
66+
export const getAvailableNetworks = () => {
67+
return Object.keys(SVM_NETWORKS);
68+
};
69+
70+
/**
71+
* Validate network configuration
72+
* @param {Object} config - Network configuration to validate
73+
* @returns {boolean} True if valid, false otherwise
74+
*/
75+
export const validateNetworkConfig = (config) => {
76+
return !!(
77+
config &&
78+
config.name &&
79+
config.endpoint &&
80+
config.connectionConfig
81+
);
82+
};

0 commit comments

Comments
 (0)