@@ -70,7 +70,12 @@ interface SendSafelyModalProps {
7070 saveCredentials : ( key : string , secret : string ) => void ;
7171 loadSendSafelyFile : ( file : PackageFile ) => void ;
7272 setLoading : ( loading : boolean ) => void ;
73+ loadingMessage : string ;
7374 setLoadingMessage : ( message : string ) => void ;
75+ recoveryEntriesCount : number | null ;
76+ setRecoveryEntriesCount : ( count : number | null ) => void ;
77+ fileLoading : boolean ;
78+ setFileLoading : ( loading : boolean ) => void ;
7479 waitForWorkers : ( ) => Promise < import ( "../state/types" ) . IWorkerManager > ;
7580}
7681
@@ -97,14 +102,19 @@ function SendSafelyModalContent({
97102 saveCredentials,
98103 loadSendSafelyFile,
99104 setLoading,
105+ loadingMessage,
100106 setLoadingMessage,
107+ recoveryEntriesCount,
108+ setRecoveryEntriesCount,
109+ fileLoading,
110+ setFileLoading,
111+ waitForWorkers,
101112} : SendSafelyModalProps ) {
102113 const [ showOtherFiles , setShowOtherFiles ] = useState ( false ) ;
103114 const [ forceStep1 , setForceStep1 ] = useState ( false ) ;
104115 const [ showValidateButton , setShowValidateButton ] = useState ( true ) ;
105116 const [ showSaveButton , setShowSaveButton ] = useState ( false ) ;
106117 const [ credentialsSaved , setCredentialsSaved ] = useState ( false ) ;
107- const [ fileLoading , setFileLoading ] = useState ( false ) ;
108118 const [ fileError , setFileError ] = useState < string | null > ( null ) ;
109119
110120 // On input change: reset button states and optionally auto-validate
@@ -707,14 +717,16 @@ function SendSafelyModalContent({
707717 ) ;
708718 try {
709719 await loadSendSafelyFile ( file ) ;
720+ // Note: Don't close modal here - it will be closed by:
721+ // 1. User clicking "Proceed" button if recovery warning is shown
722+ // 2. Normal flow via onLoadingStage if no recovery warning
710723 } catch ( error ) {
711724 setFileError (
712725 error instanceof Error
713726 ? error . message
714727 : "Unknown error occurred" ,
715728 ) ;
716729 setLoading ( false ) ;
717- } finally {
718730 setFileLoading ( false ) ;
719731 }
720732 }
@@ -860,7 +872,7 @@ function SendSafelyModalContent({
860872 </ div >
861873 </ div >
862874
863- { fileLoading && (
875+ { fileLoading && recoveryEntriesCount === null && (
864876 < div style = { { textAlign : "center" , padding : "20px" } } >
865877 < span className = "loading-spinner-small" />
866878 < div
@@ -870,7 +882,67 @@ function SendSafelyModalContent({
870882 color : "var(--text-muted)" ,
871883 } }
872884 >
873- Downloading and decrypting file...
885+ { loadingMessage || "Loading ZIP file metadata..." }
886+ </ div >
887+ </ div >
888+ ) }
889+
890+ { recoveryEntriesCount !== null && (
891+ < div style = { { padding : "20px" } } >
892+ < div
893+ style = { {
894+ textAlign : "center" ,
895+ marginBottom : "16px" ,
896+ fontSize : "24px" ,
897+ } }
898+ >
899+ ⚠️
900+ </ div >
901+ < div
902+ style = { {
903+ marginBottom : "12px" ,
904+ fontSize : "13px" ,
905+ fontWeight : "bold" ,
906+ color : "var(--accent-warning)" ,
907+ textAlign : "center" ,
908+ } }
909+ >
910+ Zip file appears malformed or truncated
911+ </ div >
912+ < div
913+ style = { {
914+ marginBottom : "20px" ,
915+ fontSize : "12px" ,
916+ color : "var(--text-muted)" ,
917+ textAlign : "center" ,
918+ } }
919+ >
920+ File listing for { recoveryEntriesCount . toLocaleString ( ) } { " " }
921+ files was decoded, but could be incomplete
922+ </ div >
923+ < div style = { { display : "flex" , justifyContent : "center" } } >
924+ < button
925+ onClick = { async ( ) => {
926+ // User confirmed - clear recovery warning and tell ZIP worker to proceed
927+ setRecoveryEntriesCount ( null ) ;
928+ const workerManager = await waitForWorkers ( ) ;
929+ await workerManager . proceedWithRecovery ( ) ;
930+ // ZIP worker will emit initializeComplete and continue with normal flow
931+ // Modal will close when we transition to table-loading stage
932+ } }
933+ style = { {
934+ padding : "10px 24px" ,
935+ backgroundColor : "var(--accent-primary)" ,
936+ color : "#fff" ,
937+ border : "none" ,
938+ borderRadius : "4px" ,
939+ cursor : "pointer" ,
940+ fontSize : "13px" ,
941+ fontWeight : "500" ,
942+ } }
943+ >
944+ Proceed
945+ </ button >
874946 </ div >
875947 </ div >
876948 ) }
@@ -948,6 +1020,21 @@ function DropZone() {
9481020 const [ packageLoading , setPackageLoading ] = useState ( false ) ;
9491021 const [ packageError , setPackageError ] = useState < string | null > ( null ) ;
9501022 const [ modalDismissable , setModalDismissable ] = useState ( false ) ;
1023+ const [ fileLoading , setFileLoading ] = useState ( false ) ;
1024+
1025+ // CD recovery warning state - shown in modal when recovery is used
1026+ const [ recoveryEntriesCount , setRecoveryEntriesCount ] = useState < number | null > ( null ) ;
1027+
1028+ // Single function to close the SendSafely modal
1029+ const closeSendSafelyModal = useCallback ( ( ) => {
1030+ setShowSendSafelyModal ( false ) ;
1031+ setValidationStatus ( null ) ;
1032+ setPackageInfo ( null ) ;
1033+ setPackageError ( null ) ;
1034+ setPackageLoading ( false ) ;
1035+ setFileLoading ( false ) ;
1036+ setRecoveryEntriesCount ( null ) ;
1037+ } , [ ] ) ;
9511038
9521039 // Debug page state
9531040 const [ showDebugPage , setShowDebugPage ] = useState ( false ) ;
@@ -1082,10 +1169,17 @@ function DropZone() {
10821169 onLoadingStage : ( stage : string , message : string ) => {
10831170 setLoadingMessage ( message ) ;
10841171
1085- if ( stage === "complete" || stage === "error" ) {
1172+ // Close modal when we move past ZIP loading phase
1173+ if ( stage === "table-loading" || stage === "complete" || stage === "error" ) {
10861174 setLoading ( false ) ;
1175+ closeSendSafelyModal ( ) ;
10871176 }
10881177 } ,
1178+ onCdScanningComplete : ( entriesCount : number ) => {
1179+ // ZIP recovery was used - show warning in modal
1180+ // Keep fileLoading = true so we stay in step 4
1181+ setRecoveryEntriesCount ( entriesCount ) ;
1182+ } ,
10891183 onSendStackFileToIframe : ( path : string , content : string , name ?: string ) => {
10901184 // Detect mode based on file path
10911185 const isLabeled = path . includes ( "stacks_with_labels.txt" ) ;
@@ -1126,7 +1220,7 @@ function DropZone() {
11261220 // stackData should already be populated via ADD_STACK_FILE actions
11271221 dispatch ( { type : "SET_STACKGAZER_READY" , ready : true } ) ;
11281222 } ,
1129- onFileList : ( entries : ZipEntryMeta [ ] ) => {
1223+ onFileList : ( entries : ZipEntryMeta [ ] , _totalFiles : number ) => {
11301224 // Received file list
11311225
11321226 ( window as unknown as { __zipReader : unknown } ) . __zipReader =
@@ -1240,12 +1334,7 @@ function DropZone() {
12401334 } ,
12411335 } ) ;
12421336
1243- // Close the modal after starting the load
1244- setShowSendSafelyModal ( false ) ;
1245- setValidationStatus ( null ) ;
1246- setPackageInfo ( null ) ;
1247- setPackageError ( null ) ;
1248- setPackageLoading ( false ) ;
1337+ // Don't close here - onLoadingStage will call closeSendSafelyModal when complete
12491338 } ;
12501339
12511340 const validateCredentials = async (
@@ -1893,12 +1982,8 @@ function DropZone() {
18931982 justifyContent : "center" ,
18941983 } }
18951984 onClick = { ( ) => {
1896- if ( modalDismissable ) {
1897- setShowSendSafelyModal ( false ) ;
1898- setValidationStatus ( null ) ;
1899- setPackageInfo ( null ) ;
1900- setPackageError ( null ) ;
1901- setPackageLoading ( false ) ;
1985+ if ( modalDismissable && recoveryEntriesCount === null ) {
1986+ closeSendSafelyModal ( ) ;
19021987 }
19031988 } }
19041989 >
@@ -1960,20 +2045,20 @@ function DropZone() {
19602045 </ button >
19612046 < button
19622047 onClick = { ( ) => {
1963- setShowSendSafelyModal ( false ) ;
1964- setValidationStatus ( null ) ;
1965- setPackageInfo ( null ) ;
1966- setPackageError ( null ) ;
1967- setPackageLoading ( false ) ;
2048+ // Don't allow closing modal while recovery warning is shown
2049+ if ( recoveryEntriesCount === null ) {
2050+ closeSendSafelyModal ( ) ;
2051+ }
19682052 } }
19692053 style = { {
19702054 background : "transparent" ,
19712055 border : "none" ,
19722056 color : "var(--text-secondary)" ,
19732057 fontSize : "18px" ,
1974- cursor : "pointer" ,
2058+ cursor : recoveryEntriesCount === null ? "pointer" : "not-allowed ",
19752059 padding : "0 4px" ,
19762060 lineHeight : "1" ,
2061+ opacity : recoveryEntriesCount === null ? 1 : 0.5 ,
19772062 } }
19782063 >
19792064 ×
@@ -2162,7 +2247,12 @@ function DropZone() {
21622247 validateCredentials = { validateCredentials }
21632248 loadSendSafelyFile = { loadSendSafelyFile }
21642249 setLoading = { setLoading }
2250+ loadingMessage = { loadingMessage }
21652251 setLoadingMessage = { setLoadingMessage }
2252+ recoveryEntriesCount = { recoveryEntriesCount }
2253+ setRecoveryEntriesCount = { setRecoveryEntriesCount }
2254+ fileLoading = { fileLoading }
2255+ setFileLoading = { setFileLoading }
21662256 waitForWorkers = { waitForWorkers }
21672257 />
21682258 ) }
0 commit comments