@@ -19,6 +19,7 @@ import {
1919 humanPermission ,
2020 validateKey ,
2121 isNcryptsec ,
22+ looksLikeSeedPhrase ,
2223} from './utilities/utils' ;
2324import { api } from './utilities/browser-polyfill' ;
2425import QRCode from 'qrcode' ;
@@ -58,7 +59,15 @@ const state = {
5859 ncryptsecExportError : '' ,
5960 ncryptsecExportLoading : false ,
6061 ncryptsecExportCopied : false ,
61-
62+
63+ // Seed phrase state
64+ seedPhraseDetected : false ,
65+ seedPhraseError : '' ,
66+ seedPhraseLoading : false ,
67+ seedPhraseRevealed : false ,
68+ seedPhraseText : '' ,
69+ seedPhraseCopied : false ,
70+
6271 // Bunker state
6372 profileType : 'local' ,
6473 bunkerUrl : '' ,
@@ -125,6 +134,19 @@ function initElements() {
125134 elements . ncryptsecExportResult = $ ( 'ncryptsec-export-result' ) ;
126135 elements . copyNcryptsecBtn = document . querySelector ( '[data-action="copyNcryptsecExport"]' ) ;
127136
137+ // Seed phrase import
138+ elements . seedPhraseImportSection = document . querySelector ( '[data-section="seedphrase-import"]' ) ;
139+ elements . seedPhraseImportError = $ ( 'seedphrase-import-error' ) ;
140+ elements . importSeedPhraseBtn = document . querySelector ( '[data-action="importSeedPhrase"]' ) ;
141+
142+ // Seed phrase export
143+ elements . seedPhraseExportSection = $ ( 'seedphrase-export-section' ) ;
144+ elements . revealSeedPhraseBtn = document . querySelector ( '[data-action="revealSeedPhrase"]' ) ;
145+ elements . seedPhraseRevealBox = $ ( 'seedphrase-export-reveal' ) ;
146+ elements . seedPhraseText = $ ( 'seedphrase-export-text' ) ;
147+ elements . copySeedPhraseBtn = document . querySelector ( '[data-action="copySeedPhrase"]' ) ;
148+ elements . hideSeedPhraseBtn = document . querySelector ( '[data-action="hideSeedPhrase"]' ) ;
149+
128150 // Bunker section
129151 elements . bunkerSection = document . querySelector ( '[data-section="bunker"]' ) ;
130152 elements . bunkerProfileNameInput = $ ( 'bunker-profile-name' ) ;
@@ -254,11 +276,42 @@ function renderLocalProfile() {
254276 if ( elements . ncryptsecSection ) {
255277 elements . ncryptsecSection . style . display = hasNcryptsec ? 'block' : 'none' ;
256278 }
279+
280+ // Seed phrase import section
281+ if ( elements . seedPhraseImportSection ) {
282+ elements . seedPhraseImportSection . style . display = state . seedPhraseDetected && ! hasNcryptsec ? 'block' : 'none' ;
283+ }
284+ if ( elements . seedPhraseImportError ) {
285+ elements . seedPhraseImportError . textContent = state . seedPhraseError ;
286+ elements . seedPhraseImportError . style . display = state . seedPhraseError ? 'block' : 'none' ;
287+ }
288+ if ( elements . importSeedPhraseBtn ) {
289+ elements . importSeedPhraseBtn . disabled = state . seedPhraseLoading ;
290+ elements . importSeedPhraseBtn . textContent = state . seedPhraseLoading ? 'Importing...' : 'Import from Seed Phrase' ;
291+ }
292+
257293 if ( elements . saveProfileBtn ) {
258- elements . saveProfileBtn . style . display = hasNcryptsec ? 'none' : 'block' ;
294+ elements . saveProfileBtn . style . display = hasNcryptsec || state . seedPhraseDetected ? 'none' : 'block' ;
259295 const needsSave = state . privKey !== state . pristinePrivKey || state . profileName !== state . pristineProfileName ;
260296 elements . saveProfileBtn . disabled = ! needsSave || ! validateKey ( state . privKey ) ;
261297 }
298+
299+ // Seed phrase export section (only for local profiles with an existing key)
300+ if ( elements . seedPhraseExportSection ) {
301+ elements . seedPhraseExportSection . style . display = state . pristinePrivKey && ! hasNcryptsec ? 'block' : 'none' ;
302+ }
303+ if ( elements . seedPhraseRevealBox ) {
304+ elements . seedPhraseRevealBox . style . display = state . seedPhraseRevealed ? 'block' : 'none' ;
305+ }
306+ if ( elements . seedPhraseText ) {
307+ elements . seedPhraseText . value = state . seedPhraseText ;
308+ }
309+ if ( elements . revealSeedPhraseBtn ) {
310+ elements . revealSeedPhraseBtn . style . display = state . seedPhraseRevealed ? 'none' : 'inline-block' ;
311+ }
312+ if ( elements . copySeedPhraseBtn ) {
313+ elements . copySeedPhraseBtn . textContent = state . seedPhraseCopied ? 'Copied!' : 'Copy' ;
314+ }
262315
263316 // QR codes
264317 if ( elements . npubQrContainer && state . npubQrDataUrl ) {
@@ -552,14 +605,20 @@ async function refreshProfile() {
552605 payload : state . profileIndex ,
553606 } ) ;
554607
555- // Reset QR and ncryptsec state
608+ // Reset QR, ncryptsec, and seed phrase state
556609 state . npubQrDataUrl = '' ;
557610 state . nsecQrDataUrl = '' ;
558611 state . showNsecQr = false ;
559612 state . ncryptsecExportResult = '' ;
560613 state . ncryptsecExportError = '' ;
561614 state . ncryptsecExportPassword = '' ;
562615 state . ncryptsecExportConfirm = '' ;
616+ state . seedPhraseDetected = false ;
617+ state . seedPhraseError = '' ;
618+ state . seedPhraseLoading = false ;
619+ state . seedPhraseRevealed = false ;
620+ state . seedPhraseText = '' ;
621+ state . seedPhraseCopied = false ;
563622
564623 if ( state . profileType === 'local' ) {
565624 await loadLocalProfile ( ) ;
@@ -696,6 +755,8 @@ function handleProfileNameInput(e) {
696755
697756function handlePrivKeyInput ( e ) {
698757 state . privKey = e . target . value ;
758+ state . seedPhraseDetected = looksLikeSeedPhrase ( state . privKey ) ;
759+ state . seedPhraseError = '' ;
699760 render ( ) ;
700761}
701762
@@ -803,6 +864,69 @@ async function handleCopyNcryptsecExport() {
803864 } , 1500 ) ;
804865}
805866
867+ async function handleImportSeedPhrase ( ) {
868+ state . seedPhraseError = '' ;
869+ state . seedPhraseLoading = true ;
870+ render ( ) ;
871+
872+ try {
873+ const result = await api . runtime . sendMessage ( {
874+ kind : 'seedPhrase.toKey' ,
875+ payload : state . privKey ,
876+ } ) ;
877+
878+ if ( result . success ) {
879+ await savePrivateKey ( state . profileIndex , result . hexKey ) ;
880+ await refreshProfile ( ) ;
881+ } else {
882+ state . seedPhraseError = result . error || 'Invalid seed phrase' ;
883+ }
884+ } catch ( e ) {
885+ state . seedPhraseError = e . message || 'Import failed' ;
886+ }
887+
888+ state . seedPhraseLoading = false ;
889+ render ( ) ;
890+ }
891+
892+ async function handleRevealSeedPhrase ( ) {
893+ try {
894+ const result = await api . runtime . sendMessage ( {
895+ kind : 'seedPhrase.fromKey' ,
896+ payload : state . profileIndex ,
897+ } ) ;
898+
899+ if ( result . success ) {
900+ state . seedPhraseText = result . seedPhrase ;
901+ state . seedPhraseRevealed = true ;
902+ } else {
903+ state . seedPhraseText = '' ;
904+ state . seedPhraseRevealed = false ;
905+ alert ( result . error || 'Failed to reveal seed phrase' ) ;
906+ }
907+ } catch ( e ) {
908+ alert ( e . message || 'Failed to reveal seed phrase' ) ;
909+ }
910+ render ( ) ;
911+ }
912+
913+ function handleHideSeedPhrase ( ) {
914+ state . seedPhraseText = '' ;
915+ state . seedPhraseRevealed = false ;
916+ state . seedPhraseCopied = false ;
917+ render ( ) ;
918+ }
919+
920+ async function handleCopySeedPhrase ( ) {
921+ await navigator . clipboard . writeText ( state . seedPhraseText ) ;
922+ state . seedPhraseCopied = true ;
923+ render ( ) ;
924+ setTimeout ( ( ) => {
925+ state . seedPhraseCopied = false ;
926+ render ( ) ;
927+ } , 1500 ) ;
928+ }
929+
806930async function handleConnectBunker ( ) {
807931 state . bunkerError = '' ;
808932 state . bunkerConnecting = true ;
@@ -1104,6 +1228,20 @@ function bindEvents() {
11041228 elements . copyNcryptsecBtn . addEventListener ( 'click' , handleCopyNcryptsecExport ) ;
11051229 }
11061230
1231+ // Seed phrase
1232+ if ( elements . importSeedPhraseBtn ) {
1233+ elements . importSeedPhraseBtn . addEventListener ( 'click' , handleImportSeedPhrase ) ;
1234+ }
1235+ if ( elements . revealSeedPhraseBtn ) {
1236+ elements . revealSeedPhraseBtn . addEventListener ( 'click' , handleRevealSeedPhrase ) ;
1237+ }
1238+ if ( elements . hideSeedPhraseBtn ) {
1239+ elements . hideSeedPhraseBtn . addEventListener ( 'click' , handleHideSeedPhrase ) ;
1240+ }
1241+ if ( elements . copySeedPhraseBtn ) {
1242+ elements . copySeedPhraseBtn . addEventListener ( 'click' , handleCopySeedPhrase ) ;
1243+ }
1244+
11071245 // Input changes for ncryptsec export
11081246 if ( elements . ncryptsecExportPassword ) {
11091247 elements . ncryptsecExportPassword . addEventListener ( 'input' , ( e ) => {
0 commit comments