@@ -21,10 +21,11 @@ import {
2121import SSMainLayout from '@/layouts/SSMainLayout'
2222import SSVStack from '@/layouts/SSVStack'
2323import { t } from '@/locales'
24- import { deleteItem , getKeySecret } from '@/storage/encrypted'
24+ import { deleteItem , getEcashMnemonic , getKeySecret } from '@/storage/encrypted'
2525import { clearAllStorage } from '@/storage/mmkv'
2626import { useAccountsStore } from '@/store/accounts'
2727import { useAuthStore } from '@/store/auth'
28+ import { useEcashStore } from '@/store/ecash'
2829import { useNostrStore } from '@/store/nostr'
2930import { useSettingsStore } from '@/store/settings'
3031import { useWalletsStore } from '@/store/wallets'
@@ -41,6 +42,7 @@ import {
4142} from '@/utils/crypto'
4243import { resetInstance as resetNostrSync } from '@/utils/nostrSyncService'
4344import { performRecoverOverwrite } from '@/utils/recoverBackup'
45+ import { pickFile , saveFile } from '@/utils/filesystem'
4446
4547export default function Developer ( ) {
4648 const accounts = useAccountsStore ( ( state ) => state . accounts )
@@ -134,9 +136,39 @@ export default function Developer() {
134136 } ) )
135137 )
136138
139+ const nostrState = useNostrStore . getState ( )
140+ const ecashState = useEcashStore . getState ( )
141+ const ecashMnemonics = Object . fromEntries (
142+ await Promise . all (
143+ ecashState . accounts . map ( async ( account ) => [
144+ account . id ,
145+ await getEcashMnemonic ( account . id )
146+ ] )
147+ )
148+ )
149+
137150 const backupData = {
138151 accounts : accountsWithSeeds ,
152+ ecash : {
153+ accounts : ecashState . accounts ,
154+ activeAccountId : ecashState . activeAccountId ,
155+ counters : ecashState . counters ,
156+ mints : ecashState . mints ,
157+ mnemonics : ecashMnemonics ,
158+ proofs : ecashState . proofs ,
159+ quotes : ecashState . quotes ,
160+ transactions : ecashState . transactions
161+ } ,
139162 exportedAt : new Date ( ) . toISOString ( ) ,
163+ nostr : {
164+ lastDataExchangeEOSE : nostrState . lastDataExchangeEOSE ,
165+ lastProtocolEOSE : nostrState . lastProtocolEOSE ,
166+ members : nostrState . members ,
167+ processedEvents : nostrState . processedEvents ,
168+ processedMessageIds : nostrState . processedMessageIds ,
169+ profiles : nostrState . profiles ,
170+ trustedDevices : nostrState . trustedDevices
171+ } ,
140172 settings : { currencyUnit, mnemonicWordList, useZeroPadding } ,
141173 version : 1
142174 }
@@ -194,6 +226,44 @@ export default function Developer() {
194226 }
195227 }
196228
229+ async function handleEncryptAndSaveFile ( ) {
230+ if ( ! backupPreviewPayload ) {
231+ return
232+ }
233+ if (
234+ backupPassphrase . length === 0 ||
235+ ! PASSPHRASE_ALLOWED_REGEX . test ( backupPassphrase )
236+ ) {
237+ toast . error ( t ( 'settings.developer.backupPassphraseInvalid' ) )
238+ return
239+ }
240+ const filename = `satsigner-backup-${ Date . now ( ) } .json`
241+ try {
242+ const salt = await generateSalt ( )
243+ const key = await pbkdf2Encrypt ( backupPassphrase , salt )
244+ const iv = randomIv ( )
245+ const cipher = await aesEncrypt ( backupPreviewPayload , key , iv )
246+ const encryptedPayload = JSON . stringify ( {
247+ cipher,
248+ iv,
249+ salt,
250+ v : 1
251+ } )
252+ await saveFile ( {
253+ dialogTitle : t ( 'settings.developer.backupData' ) ,
254+ fileContent : encryptedPayload ,
255+ filename,
256+ mimeType : 'application/json'
257+ } )
258+ toast . success ( t ( 'settings.developer.backupSuccess' ) )
259+ setBackupPreviewVisible ( false )
260+ setBackupPreviewPayload ( null )
261+ setBackupPassphrase ( '' )
262+ } catch {
263+ toast . error ( t ( 'settings.developer.backupError' ) )
264+ }
265+ }
266+
197267 async function handleRecoverPaste ( ) {
198268 try {
199269 const text = await Clipboard . getStringAsync ( )
@@ -208,6 +278,19 @@ export default function Developer() {
208278 }
209279 }
210280
281+ async function handleRecoverUploadFile ( ) {
282+ try {
283+ const text = await pickFile ( { type : 'application/json' } )
284+ if ( ! text ) {
285+ return
286+ }
287+ setRecoverEncryptedInput ( text )
288+ toast . success ( t ( 'common.success.dataPasted' ) )
289+ } catch {
290+ toast . error ( t ( 'settings.developer.recoverUploadError' ) )
291+ }
292+ }
293+
211294 async function handleRecoverDecrypt ( ) {
212295 const raw = recoverEncryptedInput . trim ( )
213296 if ( ! raw || ! recoverPassphrase . trim ( ) ) {
@@ -439,7 +522,7 @@ export default function Developer() {
439522 closeButtonVariant = "ghost"
440523 fullOpacity
441524 >
442- < SSVStack gap = "lg" style = { styles . backupPreviewModal } >
525+ < SSVStack gap = "lg" widthFull style = { styles . backupPreviewModal } >
443526 < SSText center size = "lg" uppercase >
444527 { t ( 'settings.developer.backupModalTitle' ) }
445528 </ SSText >
@@ -457,7 +540,7 @@ export default function Developer() {
457540 value = { backupPreviewPayload ?? '' }
458541 />
459542 </ ScrollView >
460- < SSVStack gap = "xs" >
543+ < SSVStack gap = "xs" widthFull >
461544 < SSText color = "muted" size = "sm" >
462545 { t ( 'settings.developer.backupPassphraseLabel' ) }
463546 </ SSText >
@@ -475,12 +558,17 @@ export default function Developer() {
475558 < SSText color = "muted" size = "xs" >
476559 { t ( 'settings.developer.backupEncryptionNote' ) }
477560 </ SSText >
478- < SSVStack gap = "sm" >
561+ < SSVStack gap = "sm" widthFull >
479562 < SSButton
480563 label = { t ( 'settings.developer.backupEncryptShare' ) }
481564 onPress = { handleEncryptAndShare }
482565 variant = "default"
483566 />
567+ < SSButton
568+ label = { t ( 'settings.developer.backupEncryptSaveFile' ) }
569+ onPress = { handleEncryptAndSaveFile }
570+ variant = "secondary"
571+ />
484572 </ SSVStack >
485573 </ SSVStack >
486574 </ SSModal >
@@ -497,13 +585,13 @@ export default function Developer() {
497585 closeButtonVariant = "ghost"
498586 fullOpacity
499587 >
500- < SSVStack gap = "lg" style = { styles . recoverModal } >
588+ < SSVStack gap = "lg" widthFull style = { styles . recoverModal } >
501589 < SSText center size = "lg" uppercase >
502590 { t ( 'settings.developer.recoverTitle' ) }
503591 </ SSText >
504592 { recoverDecrypted === null ? (
505593 < >
506- < SSVStack gap = "xs" >
594+ < SSVStack gap = "xs" widthFull >
507595 < SSText color = "muted" size = "sm" >
508596 { t ( 'settings.developer.recoverEncryptedLabel' ) }
509597 </ SSText >
@@ -521,13 +609,20 @@ export default function Developer() {
521609 onChangeText = { setRecoverEncryptedInput }
522610 />
523611 </ ScrollView >
524- < SSButton
525- label = { t ( 'common.paste' ) }
526- onPress = { handleRecoverPaste }
527- variant = "secondary"
528- />
612+ < SSVStack gap = "sm" widthFull >
613+ < SSButton
614+ label = { t ( 'common.paste' ) }
615+ onPress = { handleRecoverPaste }
616+ variant = "secondary"
617+ />
618+ < SSButton
619+ label = { t ( 'settings.developer.recoverUploadFile' ) }
620+ onPress = { handleRecoverUploadFile }
621+ variant = "secondary"
622+ />
623+ </ SSVStack >
529624 </ SSVStack >
530- < SSVStack gap = "xs" >
625+ < SSVStack gap = "xs" widthFull >
531626 < SSText color = "muted" size = "sm" >
532627 { t ( 'settings.developer.backupPassphraseLabel' ) }
533628 </ SSText >
@@ -541,15 +636,17 @@ export default function Developer() {
541636 onChangeText = { setRecoverPassphrase }
542637 />
543638 </ SSVStack >
544- < SSButton
545- label = { t ( 'settings.developer.recoverDecrypt' ) }
546- onPress = { handleRecoverDecrypt }
547- variant = "secondary"
548- />
639+ < SSVStack widthFull >
640+ < SSButton
641+ label = { t ( 'settings.developer.recoverDecrypt' ) }
642+ onPress = { handleRecoverDecrypt }
643+ variant = "secondary"
644+ />
645+ </ SSVStack >
549646 </ >
550647 ) : (
551648 < >
552- < SSVStack gap = "xs" >
649+ < SSVStack gap = "xs" widthFull >
553650 < SSText color = "muted" size = "sm" >
554651 { t ( 'settings.developer.recoverDecryptedLabel' ) }
555652 </ SSText >
@@ -565,17 +662,21 @@ export default function Developer() {
565662 />
566663 </ ScrollView >
567664 </ SSVStack >
568- < SSButton
569- label = { t ( 'settings.developer.recoverOverwrite' ) }
570- onPress = { ( ) => setRecoverConfirmOverwrite ( true ) }
571- variant = "secondary"
572- />
573- { recoverConfirmOverwrite && (
665+ < SSVStack widthFull >
574666 < SSButton
575- label = { t ( 'settings.developer.recoverImSure ' ) }
576- onPress = { handleRecoverImSure }
577- variant = "danger "
667+ label = { t ( 'settings.developer.recoverOverwrite ' ) }
668+ onPress = { ( ) => setRecoverConfirmOverwrite ( true ) }
669+ variant = "secondary "
578670 />
671+ </ SSVStack >
672+ { recoverConfirmOverwrite && (
673+ < SSVStack widthFull >
674+ < SSButton
675+ label = { t ( 'settings.developer.recoverImSure' ) }
676+ onPress = { handleRecoverImSure }
677+ variant = "danger"
678+ />
679+ </ SSVStack >
579680 ) }
580681 </ >
581682 ) }
@@ -588,7 +689,8 @@ export default function Developer() {
588689const styles = StyleSheet . create ( {
589690 backupPreviewModal : {
590691 maxHeight : '80%' ,
591- paddingVertical : 8
692+ paddingVertical : 8 ,
693+ width : '100%'
592694 } ,
593695 backupPreviewText : {
594696 color : Colors . gray [ '200' ] ,
@@ -600,7 +702,8 @@ const styles = StyleSheet.create({
600702 borderColor : Colors . gray [ 500 ] ,
601703 borderRadius : 4 ,
602704 borderWidth : 1 ,
603- maxHeight : 320
705+ maxHeight : 320 ,
706+ width : '100%'
604707 } ,
605708 modalTextAreaScrollContent : {
606709 paddingBottom : 16
@@ -610,10 +713,12 @@ const styles = StyleSheet.create({
610713 borderRadius : 4 ,
611714 borderWidth : 1 ,
612715 color : Colors . gray [ '200' ] ,
613- padding : 12
716+ padding : 12 ,
717+ width : '100%'
614718 } ,
615719 recoverModal : {
616720 maxHeight : '85%' ,
617- paddingVertical : 8
721+ paddingVertical : 8 ,
722+ width : '100%'
618723 }
619724} )
0 commit comments