diff --git a/frontend/src/components/HDWallet.jsx b/frontend/src/components/HDWallet.jsx index 331407d..f56ba50 100644 --- a/frontend/src/components/HDWallet.jsx +++ b/frontend/src/components/HDWallet.jsx @@ -230,7 +230,7 @@ export default function HDWallet() { value={saveFilename} onChange={e => setSaveFilename(e.target.value)} /> - .json · saved to ~/.wallet-generator/hdwallet/ + .json · Desktop: ~/.wallet-generator/hdwallet/ · Android: app storage
diff --git a/frontend/src/components/SingleAddress.jsx b/frontend/src/components/SingleAddress.jsx index c92f651..1795bf8 100644 --- a/frontend/src/components/SingleAddress.jsx +++ b/frontend/src/components/SingleAddress.jsx @@ -156,7 +156,7 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) { value={filename} onChange={e => setFilename(e.target.value)} /> - .json · saved to ~/.wallet-generator/single/ + .json · Desktop: ~/.wallet-generator/single/ · Android: app storage
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 0fe592f..9b9fc06 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -22,6 +22,7 @@ const WALLET_DIRS = { } let _fs = null +let _storageDirectory = null async function fs() { if (!_fs) { const { Filesystem, Directory } = await import('@capacitor/filesystem') @@ -30,49 +31,141 @@ async function fs() { return _fs } +function walletDir(kind) { + const dir = WALLET_DIRS[kind] + if (!dir) throw new Error(`Unsupported wallet kind: '${kind}'.`) + return dir +} + +function normalizeFilename(filename) { + if (typeof filename !== 'string') throw new Error('Filename must be a string.') + const trimmed = filename.trim() + if (!trimmed) throw new Error('Filename is required.') + if (trimmed.includes('/') || trimmed.includes('\\') || trimmed === '.' || trimmed === '..') { + throw new Error('Invalid filename.') + } + return trimmed +} + +function unique(arr) { + return [...new Set(arr.filter(Boolean))] +} + +function toMtime(value) { + if (!value) return 0 + if (typeof value === 'number') return value + const parsed = Date.parse(value) + return Number.isNaN(parsed) ? 0 : parsed +} + +function isMissingFileError(err) { + const msg = String(err?.message || err || '').toLowerCase() + return ( + msg.includes('not found') || + msg.includes('does not exist') || + msg.includes('no such file') || + msg.includes('directory at') + ) +} + +function directoryLabel(directory, Directory) { + if (directory === Directory.Documents) return 'Documents' + if (directory === Directory.Data) return 'Data' + return 'Storage' +} + +async function directoryCandidates() { + const { Directory } = await fs() + return unique([Directory.Documents, Directory.Data]) +} + +async function resolveWritableDirectory() { + if (_storageDirectory) return _storageDirectory + const { Filesystem } = await fs() + let lastErr = null + for (const directory of await directoryCandidates()) { + try { + await Filesystem.mkdir({ path: 'wallet-generator', directory, recursive: true }) + _storageDirectory = directory + return directory + } catch (err) { + lastErr = err + } + } + throw lastErr || new Error('No writable storage directory available.') +} + +function orderedDirectories(preferred, all) { + if (!preferred) return all + return [preferred, ...all.filter(d => d !== preferred)] +} + async function androidSaveWallet(filename, data, kind) { const { Filesystem, Directory } = await fs() - const path = `${WALLET_DIRS[kind]}/${filename}` - await Filesystem.mkdir({ path: WALLET_DIRS[kind], directory: Directory.Documents, recursive: true }) + const safeName = normalizeFilename(filename) + const dirPath = walletDir(kind) + const path = `${dirPath}/${safeName}` + const directory = await resolveWritableDirectory() + await Filesystem.mkdir({ path: dirPath, directory, recursive: true }) await Filesystem.writeFile({ path, data: JSON.stringify(data, null, 2), - directory: Directory.Documents, + directory, encoding: 'utf8', }) - return path + return `${directoryLabel(directory, Directory)}/${path}` } async function androidListWallets() { - const { Filesystem, Directory } = await fs() + const { Filesystem } = await fs() const out = { hd: [], single: [] } + const preferred = await resolveWritableDirectory().catch(() => null) + const dirs = orderedDirectories(preferred, await directoryCandidates()) for (const [kind, dir] of Object.entries(WALLET_DIRS)) { + const byName = new Map() try { - await Filesystem.mkdir({ path: dir, directory: Directory.Documents, recursive: true }) - const { files } = await Filesystem.readdir({ path: dir, directory: Directory.Documents }) - out[kind] = files - .filter(f => (f.type === 'file' || !f.type) && f.name.toLowerCase().endsWith('.json')) - .map(f => ({ - name: f.name, - size: f.size || 0, - mtimeMs: f.mtime ? (typeof f.mtime === 'number' ? f.mtime : Date.parse(f.mtime)) : 0, - })) - .sort((a, b) => b.mtimeMs - a.mtimeMs) - } catch { - out[kind] = [] + for (const directory of dirs) { + try { + await Filesystem.mkdir({ path: dir, directory, recursive: true }) + const { files = [] } = await Filesystem.readdir({ path: dir, directory }) + files + .filter(f => (f.type === 'file' || !f.type) && f.name?.toLowerCase?.().endsWith('.json')) + .forEach(f => { + const item = { + name: f.name, + size: f.size || 0, + mtimeMs: toMtime(f.mtime), + } + const prev = byName.get(item.name) + if (!prev || item.mtimeMs > prev.mtimeMs) byName.set(item.name, item) + }) + } catch { + // Try next storage directory. + } + } + } finally { + out[kind] = Array.from(byName.values()).sort((a, b) => b.mtimeMs - a.mtimeMs) } } return out } async function androidReadWallet(kind, filename) { - const { Filesystem, Directory } = await fs() - const { data } = await Filesystem.readFile({ - path: `${WALLET_DIRS[kind]}/${filename}`, - directory: Directory.Documents, - encoding: 'utf8', - }) - return JSON.parse(data) + const { Filesystem } = await fs() + const path = `${walletDir(kind)}/${normalizeFilename(filename)}` + const preferred = await resolveWritableDirectory().catch(() => null) + const dirs = orderedDirectories(preferred, await directoryCandidates()) + let lastErr = null + for (const directory of dirs) { + try { + const { data } = await Filesystem.readFile({ path, directory, encoding: 'utf8' }) + return JSON.parse(data) + } catch (err) { + lastErr = err + if (!isMissingFileError(err)) throw err + } + } + throw lastErr || new Error('Wallet file not found.') } // ── Exported API ───────────────────────────────────────────────────────────── @@ -145,6 +238,8 @@ export const api = { getWalletDir: async () => { if (isElectron) return window.electronAPI.getWalletDir() - return 'Documents/wallet-generator' + const { Directory } = await fs() + const directory = await resolveWritableDirectory() + return `${directoryLabel(directory, Directory)}/wallet-generator` }, }