From e17b24df8370b176b608070102f516311b7ac5ba Mon Sep 17 00:00:00 2001 From: Marc Platt Date: Wed, 14 May 2025 12:48:44 -0400 Subject: [PATCH] update wallet display screen for clearer ux --- src/components/WalletModal.js | 420 ++++++++++++-------------- src/components/WalletModal.module.css | 177 ++++++++++- 2 files changed, 360 insertions(+), 237 deletions(-) diff --git a/src/components/WalletModal.js b/src/components/WalletModal.js index bc5832b..c13ab0d 100644 --- a/src/components/WalletModal.js +++ b/src/components/WalletModal.js @@ -1,18 +1,85 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import styles from './WalletModal.module.css'; -import { Eye, EyeOff } from 'lucide-react'; +import { Eye, EyeOff, Info, X } from 'lucide-react'; const WalletModal = () => { const { isLoading, error } = useSelector(state => state.walletModal); const [copiedStates, setCopiedStates] = useState({}); - const [revealedMnemonics, setRevealedMnemonics] = useState({ - master: false, - layer1: false, - layer2_thunder: false, - layer2_bitnames: false, - layer2_zside: false - }); + const [revealedPaths, setRevealedPaths] = useState({ master: false }); + const [, setConfig] = useState(null); + const [derivationPaths, setDerivationPaths] = useState({}); + const [mnemonics, setMnemonics] = useState({ master: '••••••••••••' }); + const [chainsList, setChainsList] = useState([]); + const [showInfoModal, setShowInfoModal] = useState(false); + + // Load config on component mount + useEffect(() => { + const fetchConfig = async () => { + try { + const result = await window.electronAPI.getConfig(); + setConfig(result); + + if (result && result.chains) { + // Find the L1 chain (enforcer) + const enforcerChain = result.chains.find(chain => chain.id === 'enforcer'); + + // Setup initial paths object with L1 (hardcoded path as specified) + const paths = {}; + if (enforcerChain) { + paths['enforcer'] = "m/44'/0'/256'"; + } + + // Find all L2 chains and compute their paths based on slot + const l2Chains = result.chains.filter(chain => chain.chain_layer === 2 && chain.slot); + + // Process L2 chains + l2Chains.forEach(chain => { + paths[chain.id] = `m/44'/0'/${chain.slot}'`; + }); + + setDerivationPaths(paths); + + // Initialize revealed state for all chains + const initialRevealState = { master: false }; + if (enforcerChain) { + initialRevealState['enforcer'] = false; + } + + l2Chains.forEach(chain => { + initialRevealState[chain.id] = false; + }); + + setRevealedPaths(initialRevealState); + + // Setup the chains list for rendering + const chains = []; + + // Add L1 chain + if (enforcerChain) { + chains.push({ + ...enforcerChain, + type: 'L1', + display_name: 'Bitcoin Core (Patched)' + }); + } + + // Add L2 chains + l2Chains.forEach(chain => { + chains.push({ + ...chain, + type: 'L2' + }); + }); + + setChainsList(chains); + } + } catch (error) { + console.error("Error fetching chain config:", error); + } + }; + fetchConfig(); + }, []); const handleCopy = async (text, type, event) => { try { @@ -46,53 +113,22 @@ const WalletModal = () => { mnemonicCol.scrollLeft += event.deltaY; } }; - const [mnemonics, setMnemonics] = useState({ - master: '••••••••••••', - layer1: '••••••••••••', - layer2_thunder: '••••••••••••', - layer2_bitnames: '••••••••••••', - layer2_zside: '••••••••••••' - }); - - const toggleMnemonic = async (key) => { - // If we're hiding the current one, just hide it - if (revealedMnemonics[key]) { - setRevealedMnemonics(prev => ({ - master: false, - layer1: false, - layer2_thunder: false, - layer2_bitnames: false, - layer2_zside: false - })); - return; - } + + const toggleReveal = async (key) => { + // Toggle the current path's reveal state + setRevealedPaths(prev => ({ + ...prev, + [key]: !prev[key] + })); - // If we're revealing a new one, hide all others first - if (!revealedMnemonics[key] && mnemonics[key] === '••••••••••••') { + // For master key, we need to fetch the actual mnemonic + if (key === 'master' && !revealedPaths[key] && mnemonics[key] === '••••••••••••') { try { - let type; - switch (key) { - case 'master': - type = 'master'; - break; - case 'layer1': - type = 'layer1'; - break; - case 'layer2_thunder': - type = 'thunder'; - break; - case 'layer2_bitnames': - type = 'bitnames'; - break; - case 'layer2_zside': - type = 'zside'; - break; - } - const result = await window.electronAPI.getWalletStarter(type); + const result = await window.electronAPI.getWalletStarter('master'); if (result.success) { setMnemonics(prev => ({ ...prev, - [key]: result.data + master: result.data })); } } catch (error) { @@ -100,16 +136,23 @@ const WalletModal = () => { } } - // Set only the current one to visible - setRevealedMnemonics({ - master: false, - layer1: false, - layer2_thunder: false, - layer2_bitnames: false, - layer2_zside: false, - [key]: true - }); + // For chain wallets, we don't need to fetch actual mnemonics, just reveal their paths + }; + + const getChainType = (chain) => { + if (chain.type === 'L1') { + return styles.l1Badge; + } else if (chain.type === 'L2') { + return styles.l2Badge; + } else { + return ''; + } }; + + const getChainTypeText = (chain) => { + return chain.type; + }; + return (
@@ -119,23 +162,33 @@ const WalletModal = () => {
{/* Starter section */}
-
+
+ +
Starter
-
Mnemonic
+
Mnemonic / Derivation Path
+ + {/* Master seed row */}
-
L1
+
M
Master
revealedMnemonics.master && handleCopy(mnemonics.master, 'master', e)} + className={`${revealedPaths.master ? styles.copyableText : ''} ${copiedStates.master?.copied ? styles.copied : ''}`} + onClick={(e) => revealedPaths.master && handleCopy(mnemonics.master, 'master', e)} onWheel={handleWheel} > - {revealedMnemonics.master ? mnemonics.master : '••••••••••••'} + {revealedPaths.master ? mnemonics.master : '••••••••••••'} {copiedStates.master?.copied && (
{
- {/* Delete functionality not yet implemented - - */} -
-
- -
-
-
L1
-
-
Bitcoin Core
-
- revealedMnemonics.layer1 && handleCopy(mnemonics.layer1, 'layer1', e)} - onWheel={handleWheel} - > - {revealedMnemonics.layer1 ? mnemonics.layer1 : '••••••••••••'} - {copiedStates.layer1?.copied && ( -
- Copied! -
- )} -
-
-
- - {/* Delete functionality not yet implemented - - */} -
-
- -
-
-
L2
-
-
Thunder
-
- revealedMnemonics.layer2_thunder && handleCopy(mnemonics.layer2_thunder, 'layer2_thunder', e)} - onWheel={handleWheel} - > - {revealedMnemonics.layer2_thunder ? mnemonics.layer2_thunder : '••••••••••••'} - {copiedStates.layer2_thunder?.copied && ( -
- Copied! -
- )} -
-
-
- - {/* Delete functionality not yet implemented - - */}
-
-
-
L2
-
-
Bitnames
-
- revealedMnemonics.layer2_bitnames && handleCopy(mnemonics.layer2_bitnames, 'layer2_bitnames', e)} - onWheel={handleWheel} - > - {revealedMnemonics.layer2_bitnames ? mnemonics.layer2_bitnames : '••••••••••••'} - {copiedStates.layer2_bitnames?.copied && ( -
- Copied! -
- )} -
-
-
- - {/* Delete functionality not yet implemented - - */} + {/* Chain rows - dynamically generated based on config */} + {chainsList.map(chain => ( +
+
+
+ {getChainTypeText(chain)} +
+
+
{chain.display_name}
+
+ revealedPaths[chain.id] && handleCopy(`derived from Master at ${derivationPaths[chain.id]}`, chain.id, e)} + onWheel={handleWheel} + > + {revealedPaths[chain.id] + ? `derived from Master at ${derivationPaths[chain.id]}` + : '••••••••••••'} + {copiedStates[chain.id]?.copied && ( +
+ Copied! +
+ )} +
+
+
+ +
-
+ ))} +
+
-
-
-
L2
-
-
zSide
-
- revealedMnemonics.layer2_zside && handleCopy(mnemonics.layer2_zside, 'layer2_zside', e)} - onWheel={handleWheel} - > - {revealedMnemonics.layer2_zside ? mnemonics.layer2_zside : '••••••••••••'} - {copiedStates.layer2_zside?.copied && ( -
- Copied! -
- )} -
-
-
- - {/* Delete functionality not yet implemented - - */} + {/* Information Modal */} + {showInfoModal && ( +
setShowInfoModal(false)}> +
e.stopPropagation()}> + +

Wallet Information

+
+
+ Important: You must back up your Master Seed if you want to restore your drivechain wallets. +
+ +

+ Your wallet seeds are stored locally and are never transmitted over the internet. +

-
-
+ )}
); }; diff --git a/src/components/WalletModal.module.css b/src/components/WalletModal.module.css index b0d1dbe..2634008 100644 --- a/src/components/WalletModal.module.css +++ b/src/components/WalletModal.module.css @@ -4,9 +4,11 @@ left: 0; right: 0; bottom: 0; - background-color: var(--modal-bg); + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; z-index: 1000; - overflow: hidden; } .modalContent { @@ -185,6 +187,42 @@ color: #3498db; } +.masterBadge { + background-color: rgba(156, 39, 176, 0.15); + color: #9c27b0; + font-weight: 700; +} + +.infoBox { + display: flex; + align-items: flex-start; + gap: 10px; + background-color: rgba(52, 152, 219, 0.1); + border: 1px solid rgba(52, 152, 219, 0.2); + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 20px; + width: 100%; + max-width: 1200px; +} + +:global(.dark) .infoBox { + background-color: rgba(52, 152, 219, 0.05); + border-color: rgba(52, 152, 219, 0.15); +} + +.infoBox p { + margin: 0; + font-size: 14px; + line-height: 1.5; +} + +.derivationPath { + font-family: monospace; + font-size: 14px; + color: var(--text-secondary); +} + .mnemonicCol { text-align: left; font-family: monospace; @@ -354,3 +392,138 @@ .submitButton:hover { background-color: var(--download-btn-hover); } + +.infoButton { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary, #555); + padding: 4px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.infoButton:hover { + background-color: rgba(0, 0, 0, 0.05); + color: var(--accent-color, #3498db); +} + +:global(.dark) .infoButton:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.infoModalContent { + background-color: var(--bg-color, white); + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 600px; + padding: 20px; + position: relative; + max-height: 80vh; + overflow-y: auto; +} + +:global(.dark) .infoModalContent { + background-color: #1a1a1a; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.closeModalButton { + position: absolute; + top: 12px; + right: 12px; + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary, #555); + padding: 4px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.closeModalButton:hover { + background-color: rgba(0, 0, 0, 0.05); + color: var(--accent-color, #3498db); +} + +:global(.dark) .closeModalButton:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.infoModalContent h2 { + margin-top: 0; + color: var(--text-primary, #333); + border-bottom: 1px solid var(--border-color, #eee); + padding-bottom: 12px; + margin-bottom: 16px; +} + +:global(.dark) .infoModalContent h2 { + color: #e1e1e1; + border-color: #333; +} + +.infoContent h3 { + color: var(--text-primary, #333); + margin-top: 16px; + margin-bottom: 8px; + font-size: 18px; +} + +:global(.dark) .infoContent h3 { + color: #e1e1e1; +} + +.infoContent p { + color: var(--text-secondary, #555); + margin-bottom: 16px; + line-height: 1.5; +} + +:global(.dark) .infoContent p { + color: #aaa; +} + +.infoContent ul { + margin-bottom: 16px; + padding-left: 24px; +} + +.infoContent li { + color: var(--text-secondary, #555); + margin-bottom: 8px; + line-height: 1.5; +} + +:global(.dark) .infoContent li { + color: #aaa; +} + +.alertBox { + background-color: rgba(255, 152, 0, 0.1); + border-left: 4px solid #ff9800; + padding: 16px; + margin-bottom: 20px; + border-radius: 4px; + color: var(--text-primary, #333); + font-size: 16px; + line-height: 1.5; +} + +:global(.dark) .alertBox { + background-color: rgba(255, 152, 0, 0.05); + color: #e1e1e1; +} + +.securityNote { + font-style: italic; + opacity: 0.8; + font-size: 14px; +}