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`
},
}