Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import ExportOptions from "./components/export-options";
import { UpdateChecker } from "./components/update-checker";
import { Button } from "./components/ui/button";
import { formatDateForFilename } from "./utils/helpers";
import { calculateQuickFileHash } from "./utils/fileHash";
import { usePasswordCacheStore } from "./stores/passwordCacheStore";

function App() {
const [files, setFiles] = useState<File[]>([]);
const [fileHashes, setFileHashes] = useState<Map<number, string>>(new Map());
const [status, setStatus] = useState<FileStatus>(FileStatus.IDLE);
const [error, setError] = useState<string | undefined>(undefined);
const [statements, setStatements] = useState<MPesaStatement[]>([]);
Expand All @@ -40,6 +43,9 @@ function App() {
const [downloadSuccess, setDownloadSuccess] = useState<boolean>(false);
const [savedFilePath, setSavedFilePath] = useState<string>("");
const [currentPlatform, setCurrentPlatform] = useState<string>("");
const [cachedPassword, setCachedPassword] = useState<string | null>(null);

const { getPassword, savePassword } = usePasswordCacheStore();

const getDefaultFileName = () => {
return (
Expand Down Expand Up @@ -103,9 +109,30 @@ function App() {
setError(undefined);
setStatements([]);
setCurrentFileIndex(0);

// Calculate hashes for all files
console.log('[App] Starting file hash calculation for', selectedFiles.length, 'files');
const hashMap = new Map<number, string>();
try {
await Promise.all(
selectedFiles.map(async (file, index) => {
try {
const hash = await calculateQuickFileHash(file);
hashMap.set(index, hash);
console.log('[App] Hash calculated for file', index, ':', file.name, '->', hash.substring(0, 16) + '...');
} catch (err) {
console.error(`[App] Failed to hash file ${file.name}:`, err);
}
})
);
setFileHashes(hashMap);
console.log('[App] All hashes calculated. Total:', hashMap.size);
} catch (err) {
console.error('[App] Error calculating file hashes:', err);
}

try {
const result = await processFiles(selectedFiles);
const result = await processFiles(selectedFiles, 0, [], hashMap);
if (result?.error) {
setStatus(FileStatus.ERROR);
setError(result.error);
Expand All @@ -121,7 +148,8 @@ function App() {
const processFiles = async (
filesToProcess: File[],
startIndex: number = 0,
existingStatements: MPesaStatement[] = []
existingStatements: MPesaStatement[] = [],
hashMap?: Map<number, string>
) => {
const processedStatements: MPesaStatement[] = [...existingStatements];

Expand All @@ -130,6 +158,12 @@ function App() {
const file = filesToProcess[i];

try {
// Get cached password if available
const fileHash = (hashMap || fileHashes).get(i);
console.log('[App] Processing file', i, '- Hash:', fileHash ? fileHash.substring(0, 16) + '...' : 'NO HASH');
const foundCachedPassword = fileHash ? getPassword(fileHash) : null;
console.log('[App] Cached password for file', i, ':', foundCachedPassword ? '***FOUND***' : 'NOT FOUND');

const result = (await Promise.race([
PdfService.loadPdf(file),
new Promise((_, reject) =>
Expand All @@ -141,10 +175,17 @@ function App() {
])) as { isProtected: boolean; pdf?: any };

if (result.isProtected) {
// Set cached password so the PasswordPrompt can auto-fill
if (foundCachedPassword) {
console.log('[App] ✅ Setting cached password for auto-fill');
setCachedPassword(foundCachedPassword);
}
setStatus(FileStatus.PROTECTED);
return { needsPassword: true, fileIndex: i, processedStatements };
} else if (result.pdf) {
setStatus(FileStatus.PROCESSING);
console.log('[App] ℹ️ No password needed for:', file.name);

const statement = (await Promise.race([
PdfService.parseMpesaStatement(result.pdf),
new Promise((_, reject) =>
Expand Down Expand Up @@ -224,11 +265,27 @@ function App() {

setStatus(FileStatus.PROCESSING);
setError(undefined);
setCachedPassword(null); // Clear cached password after use

try {
const currentFile = files[currentFileIndex];
const pdf = await PdfService.unlockPdf(currentFile, password);

// Save password to cache if file was successfully unlocked
const fileHash = fileHashes.get(currentFileIndex);
console.log('[App] Attempting to save password for file', currentFileIndex);
console.log('[App] File hash:', fileHash ? fileHash.substring(0, 16) + '...' : 'NO HASH FOUND');
console.log('[App] Current fileHashes map size:', fileHashes.size);
console.log('[App] All hashes in map:', Array.from(fileHashes.entries()).map(([k, v]) => `${k}: ${v.substring(0, 16)}...`));

if (fileHash) {
console.log('[App] ✅ Saving password to cache for:', currentFile.name);
savePassword(fileHash, password, currentFile.name);
console.log('[App] ✅ Password saved successfully');
} else {
console.error('[App] ❌ Cannot save password - no hash found for file index:', currentFileIndex);
}

const statement = await PdfService.parseMpesaStatement(pdf);
statement.fileName = currentFile.name;

Expand Down Expand Up @@ -270,6 +327,7 @@ function App() {
if (files.length === 0) return;

setError(undefined);
setCachedPassword(null); // Clear cached password when skipping
const nextIndex = currentFileIndex + 1;

if (nextIndex < files.length) {
Expand Down Expand Up @@ -306,13 +364,15 @@ function App() {

const handleReset = () => {
setFiles([]);
setFileHashes(new Map());
setStatus(FileStatus.IDLE);
setError(undefined);
setStatements([]);
setCurrentFileIndex(0);
setIsDownloading(false);
setDownloadSuccess(false);
setSavedFilePath("");
setCachedPassword(null);

if (exportLink) {
URL.revokeObjectURL(exportLink);
Expand Down Expand Up @@ -454,6 +514,7 @@ function App() {
currentFileName={files[currentFileIndex]?.name}
currentFileIndex={currentFileIndex}
totalFiles={files.length}
defaultPassword={cachedPassword}
/>
</div>
) : status === FileStatus.PROCESSING ? (
Expand Down
22 changes: 20 additions & 2 deletions src/components/password-prompt.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { FileStatus } from "../types";
import { Lock } from "lucide-react";
import { cn } from "../lib/utils";
Expand All @@ -14,6 +14,7 @@ interface PasswordPromptProps {
currentFileName?: string;
currentFileIndex?: number;
totalFiles?: number;
defaultPassword?: string | null;
}

const PasswordPrompt: React.FC<PasswordPromptProps> = ({
Expand All @@ -25,8 +26,19 @@ const PasswordPrompt: React.FC<PasswordPromptProps> = ({
currentFileName,
currentFileIndex,
totalFiles,
defaultPassword,
}) => {
const [password, setPassword] = useState<string>("");
const [isAutoFilled, setIsAutoFilled] = useState<boolean>(false);

// Auto-fill password when cached password is available
useEffect(() => {
if (defaultPassword) {
console.log('[PasswordPrompt] Auto-filling password from cache');
setPassword(defaultPassword);
setIsAutoFilled(true);
}
}, [defaultPassword]);

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -56,8 +68,14 @@ const PasswordPrompt: React.FC<PasswordPromptProps> = ({
)}

<p className=" max-w-md text-center">
This PDF is password protected. Please enter the password to unlock it.
This PDF is password protected. {isAutoFilled ? 'Password auto-filled from cache.' : 'Please enter the password to unlock it.'}
</p>

{isAutoFilled && (
<div className="text-sm text-green-600 dark:text-green-400 font-medium">
✅ Password remembered - click Unlock to continue
</div>
)}

<form onSubmit={handleSubmit} className="w-full mt-4">
<div className="space-y-4">
Expand Down
103 changes: 103 additions & 0 deletions src/stores/passwordCacheStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface PasswordCacheEntry {
fileHash: string;
password: string;
fileName: string;
timestamp: number;
}

interface PasswordCacheState {
passwords: Record<string, PasswordCacheEntry>;
getPassword: (fileHash: string) => string | null;
savePassword: (fileHash: string, password: string, fileName: string) => void;
removePassword: (fileHash: string) => void;
clearAll: () => void;
getAll: () => PasswordCacheEntry[];
}

const MAX_CACHE_SIZE = 50;

/**
* Store for caching PDF passwords based on file hash
* Persisted to localStorage
*/
export const usePasswordCacheStore = create<PasswordCacheState>()(
persist(
(set, get) => ({
passwords: {},

getPassword: (fileHash: string) => {
const entry = get().passwords[fileHash];
console.log('[PasswordCache] getPassword called:', {
fileHash: fileHash.substring(0, 16) + '...',
found: !!entry,
allHashes: Object.keys(get().passwords).map(h => h.substring(0, 16) + '...'),
});
return entry ? entry.password : null;
},

savePassword: (fileHash: string, password: string, fileName: string) => {
console.log('[PasswordCache] savePassword called:', {
fileHash: fileHash.substring(0, 16) + '...',
fileName,
passwordLength: password.length,
});

set((state) => {
const currentPasswords = { ...state.passwords };
const currentSize = Object.keys(currentPasswords).length;

// Add or update the password
currentPasswords[fileHash] = {
fileHash,
password,
fileName,
timestamp: Date.now(),
};

// If we exceeded the limit, remove oldest entries
if (currentSize >= MAX_CACHE_SIZE && !state.passwords[fileHash]) {
console.log('[PasswordCache] Cache limit reached, purging oldest entries');

// Sort entries by timestamp (oldest first)
const sortedEntries = Object.entries(currentPasswords)
.sort(([, a], [, b]) => a.timestamp - b.timestamp);

// Remove oldest entries until we're under the limit
const entriesToRemove = sortedEntries.length - MAX_CACHE_SIZE + 1;
for (let i = 0; i < entriesToRemove; i++) {
const [hashToRemove] = sortedEntries[i];
console.log('[PasswordCache] Removing old entry:', currentPasswords[hashToRemove].fileName);
delete currentPasswords[hashToRemove];
}
}

return { passwords: currentPasswords };
});

console.log('[PasswordCache] After save, total entries:', Object.keys(get().passwords).length);
},

removePassword: (fileHash: string) => {
set((state) => {
const { [fileHash]: _, ...rest } = state.passwords;
return { passwords: rest };
});
},

clearAll: () => {
set({ passwords: {} });
},

getAll: () => {
return Object.values(get().passwords);
},
}),
{
name: 'mpesa2csv-password-cache',
version: 1,
}
)
);
Loading