Compare commits
No commits in common. "dev" and "main" have entirely different histories.
|
|
@ -13,16 +13,4 @@ Thumbs.db
|
|||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.code-workspace
|
||||
|
||||
# Build output
|
||||
build-output/
|
||||
|
||||
# Private project/agent docs — never commit
|
||||
PROJECT.md
|
||||
PLAN.md
|
||||
FUTURE.md
|
||||
HISTORY.md
|
||||
DEVELOPMENT_LOG.md
|
||||
.learnings/
|
||||
.idea/
|
||||
|
|
@ -11,19 +11,6 @@ const config: CapacitorConfig = {
|
|||
server: {
|
||||
// androidScheme must be https for cookies to work correctly
|
||||
androidScheme: 'https',
|
||||
// In local mode, the WebView navigates to the embedded Node server on
|
||||
// 127.0.0.1 — without this it's treated as an external link and opened
|
||||
// in the system browser instead of the app's WebView.
|
||||
allowNavigation: ['127.0.0.1'],
|
||||
},
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
// Routes fetch()/XHR through native HTTP so the server-mode login
|
||||
// request isn't blocked by CORS, and the resulting session cookie is
|
||||
// written to the WebView's cookie store for the subsequent WebView
|
||||
// navigation to the server URL.
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// better-sqlite3's better_sqlite3.node is architecture-specific (it links
|
||||
// against libnode.so, which is the real Android binary already loaded into
|
||||
// this process by nodejs-mobile-cordova). The copy under node_modules/ is
|
||||
// whichever architecture it happened to be built for last; replace it with
|
||||
// the prebuild matching this device's actual arch before anything requires
|
||||
// better-sqlite3. See scripts/build-better-sqlite3-arm.sh.
|
||||
(function installBetterSqlite3Prebuild() {
|
||||
const archDirs = {
|
||||
arm64: 'arm64-v8a',
|
||||
arm: 'armeabi-v7a',
|
||||
x64: 'x86_64',
|
||||
ia32: 'x86',
|
||||
};
|
||||
const archDir = archDirs[process.arch];
|
||||
if (!archDir) return; // Unknown arch — fall back to whatever's bundled.
|
||||
|
||||
const prebuilt = path.join(__dirname, 'prebuilds', archDir, 'better_sqlite3.node');
|
||||
const target = path.join(__dirname, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node');
|
||||
if (!fs.existsSync(prebuilt)) return;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.copyFileSync(prebuilt, target);
|
||||
} catch (err) {
|
||||
console.error('[main.js] Failed to install better-sqlite3 prebuild for', process.arch, err);
|
||||
}
|
||||
})();
|
||||
|
||||
// Node 12.19 predates crypto.randomUUID (added in Node 14.17), used by
|
||||
// services/authService.js for session token IDs.
|
||||
const nodeCrypto = require('crypto');
|
||||
if (typeof nodeCrypto.randomUUID !== 'function') {
|
||||
nodeCrypto.randomUUID = function () {
|
||||
const b = nodeCrypto.randomBytes(16);
|
||||
b[6] = (b[6] & 0x0f) | 0x40;
|
||||
b[8] = (b[8] & 0x3f) | 0x80;
|
||||
const hex = b.toString('hex');
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20),
|
||||
].join('-');
|
||||
};
|
||||
}
|
||||
|
||||
// Node 12.19 predates crypto.hkdfSync (added in Node 15.0), used by
|
||||
// services/encryptionService.js's deriveKey to derive the token-encryption
|
||||
// key from TOKEN_ENCRYPTION_KEY. Implements HKDF-Extract/Expand (RFC 5869)
|
||||
// via createHmac.
|
||||
if (typeof nodeCrypto.hkdfSync !== 'function') {
|
||||
nodeCrypto.hkdfSync = function (digest, ikm, salt, info, keylen) {
|
||||
const hashLen = nodeCrypto.createHash(digest).digest().length;
|
||||
const saltBuf = salt && salt.length ? Buffer.from(salt) : Buffer.alloc(hashLen, 0);
|
||||
const ikmBuf = Buffer.isBuffer(ikm) ? ikm : Buffer.from(ikm);
|
||||
const infoBuf = Buffer.isBuffer(info) ? info : Buffer.from(info || '');
|
||||
|
||||
const prk = nodeCrypto.createHmac(digest, saltBuf).update(ikmBuf).digest();
|
||||
|
||||
let t = Buffer.alloc(0);
|
||||
let okm = Buffer.alloc(0);
|
||||
let counter = 1;
|
||||
while (okm.length < keylen) {
|
||||
t = nodeCrypto
|
||||
.createHmac(digest, prk)
|
||||
.update(t)
|
||||
.update(infoBuf)
|
||||
.update(Buffer.from([counter]))
|
||||
.digest();
|
||||
okm = Buffer.concat([okm, t]);
|
||||
counter++;
|
||||
}
|
||||
return okm.slice(0, keylen);
|
||||
};
|
||||
}
|
||||
|
||||
// nodejs-mobile-cordova 0.4.3 embeds Node 12.19 built without ICU, so
|
||||
// `Intl` is entirely undefined. Polyfill just the bits the server uses
|
||||
// (utils/money.js, routes/dataSources.js, routes/status.js) — en-US number
|
||||
// formatting with thousands separators, and a no-op DateTimeFormat whose
|
||||
// resolvedOptions().timeZone the callers already treat as optional.
|
||||
if (typeof global.Intl === 'undefined') {
|
||||
global.Intl = {
|
||||
NumberFormat: function (_locale, options) {
|
||||
const opts = options || {};
|
||||
const minFrac = opts.minimumFractionDigits || 0;
|
||||
const maxFrac = Math.max(opts.maximumFractionDigits || 0, minFrac);
|
||||
this.format = function (value) {
|
||||
const fixed = Number(value).toFixed(maxFrac);
|
||||
let [intPart, fracPart] = fixed.split('.');
|
||||
const negative = intPart.startsWith('-');
|
||||
if (negative) intPart = intPart.slice(1);
|
||||
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
let out = (negative ? '-' : '') + intPart;
|
||||
if (fracPart) out += '.' + fracPart;
|
||||
return out;
|
||||
};
|
||||
},
|
||||
DateTimeFormat: function () {
|
||||
this.resolvedOptions = function () {
|
||||
return { timeZone: undefined };
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Server-side paths, relative to this directory (the app's writable storage
|
||||
// on-device — nodejs-mobile-cordova copies nodejs-project here at runtime).
|
||||
process.env.DB_PATH = path.join(__dirname, 'data', 'bills.db');
|
||||
process.env.BACKUP_PATH = path.join(__dirname, 'backups');
|
||||
process.env.PORT = process.env.PORT || '3000';
|
||||
process.env.BIND_HOST = '127.0.0.1';
|
||||
// The Capacitor WebView's origin (androidScheme: 'https') is cross-origin
|
||||
// from this server's http://localhost:3000, so the LoadingScreen's
|
||||
// /api/health poll and the app's API calls need CORS allowed for it.
|
||||
process.env.CORS_ORIGIN = 'https://localhost';
|
||||
|
||||
const cordova = require('cordova-bridge');
|
||||
|
||||
// Wait for the WebView to hand over the device-bound encryption key (stored
|
||||
// in Android Keystore / iOS Keychain — see src/crypto.ts) before starting the
|
||||
// server, so encryptionService.js picks up TOKEN_ENCRYPTION_KEY on first use.
|
||||
cordova.channel.on('message', function (msg) {
|
||||
if (msg && msg.type === 'encryptionKey' && typeof msg.key === 'string') {
|
||||
process.env.TOKEN_ENCRYPTION_KEY = msg.key;
|
||||
require('./server/server.js');
|
||||
}
|
||||
});
|
||||
|
||||
cordova.channel.post('message', { type: 'ready' });
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "nodejs-project",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@simplewebauthn/server": "^13.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^8.0.9",
|
||||
"openid-client": "^5.7.1",
|
||||
"otplib": "^13.4.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodejs-mobile-gyp": "^0.4.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -8,13 +8,11 @@
|
|||
"name": "bill-tracker-mobile",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-biometric-auth": "^10.0.0",
|
||||
"@capacitor/android": "^8.4.0",
|
||||
"@capacitor/app": "^8.1.0",
|
||||
"@capacitor/core": "^8.4.0",
|
||||
"@capacitor/ios": "^8.4.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"capacitor-secure-storage-plugin": "^0.13.0",
|
||||
"nodejs-mobile-cordova": "^0.4.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
|
@ -24,26 +22,10 @@
|
|||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aparajita/capacitor-biometric-auth": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@aparajita/capacitor-biometric-auth/-/capacitor-biometric-auth-10.0.0.tgz",
|
||||
"integrity": "sha512-azjWaRucB8x1/gSzSTp/NxelaOZyg+m765gSzjwUkSQgg30RrZPmPq6pyLeeXVSZNDY3Rxf5Hj8obgZaXirCFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^8.0.2",
|
||||
"@capacitor/app": "^8.0.0",
|
||||
"@capacitor/core": "^8.0.2",
|
||||
"@capacitor/ios": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/android": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.0.tgz",
|
||||
|
|
@ -156,448 +138,6 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/cli-framework-output": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
|
||||
|
|
@ -1435,15 +975,6 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/capacitor-secure-storage-plugin": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/capacitor-secure-storage-plugin/-/capacitor-secure-storage-plugin-0.13.0.tgz",
|
||||
"integrity": "sha512-+rLC/9Z0LTaRRt6L6HjBwcDh5gqgI3NPmDSwo4hk41XQOy3EBrRo81VleIqFsowsMA3oMT+E59Bl8/HiWk0nhQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
|
|
@ -1655,48 +1186,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -12,13 +12,11 @@
|
|||
"android:run": "npm run sync && npm run sync:server && npm run sync:android-assets && npx cap run android --no-sync"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aparajita/capacitor-biometric-auth": "^10.0.0",
|
||||
"@capacitor/android": "^8.4.0",
|
||||
"@capacitor/app": "^8.1.0",
|
||||
"@capacitor/core": "^8.4.0",
|
||||
"@capacitor/ios": "^8.4.0",
|
||||
"@capacitor/preferences": "^8.0.1",
|
||||
"capacitor-secure-storage-plugin": "^0.13.0",
|
||||
"nodejs-mobile-cordova": "^0.4.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
|
@ -28,8 +26,8 @@
|
|||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Cross-compiles better-sqlite3 for arm64-v8a and armeabi-v7a (real Android
|
||||
# devices) and writes the resulting better_sqlite3.node binaries into
|
||||
# nodejs-assets/nodejs-project/prebuilds/<abi>/. main.js installs the one
|
||||
# matching the device's actual process.arch at runtime — see main.js's
|
||||
# installBetterSqlite3Prebuild().
|
||||
#
|
||||
# The existing x86_64 build (emulator) in
|
||||
# nodejs-assets/nodejs-project/node_modules/better-sqlite3/build/Release/
|
||||
# already covers nodejs-assets/nodejs-project/prebuilds/x86_64/ via
|
||||
# fix-better-sqlite3-android.sh; re-copy it manually if that build changes:
|
||||
# cp nodejs-assets/nodejs-project/node_modules/better-sqlite3/build/Release/better_sqlite3.node \
|
||||
# nodejs-assets/nodejs-project/prebuilds/x86_64/
|
||||
#
|
||||
# Requires: Android NDK (ANDROID_HOME or ANDROID_NDK_HOME set), and
|
||||
# `npm install` already run in nodejs-assets/nodejs-project (so
|
||||
# better-sqlite3's source + nodejs-mobile-gyp are present).
|
||||
#
|
||||
# Usage: ./scripts/build-better-sqlite3-arm.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PKG_SRC="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/better-sqlite3"
|
||||
PREBUILDS_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/prebuilds"
|
||||
GYP="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/.bin/nodejs-mobile-gyp"
|
||||
NODEDIR="$MOBILE_DIR/node_modules/nodejs-mobile-cordova/libs/android/libnode"
|
||||
|
||||
NDK="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}"
|
||||
if [[ -z "$NDK" ]]; then
|
||||
SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
|
||||
[[ -n "$SDK" ]] && NDK="$(ls -d "$SDK"/ndk/*/ 2>/dev/null | sort -V | tail -1)"
|
||||
fi
|
||||
NDK="${NDK%/}"
|
||||
|
||||
if [[ ! -d "$PKG_SRC" ]]; then
|
||||
echo "Error: $PKG_SRC not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$NDK" || ! -d "$NDK" ]]; then
|
||||
echo "Error: Android NDK not found. Set ANDROID_NDK_HOME (or ANDROID_HOME with an ndk/ subdir)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$GYP" ]]; then
|
||||
echo "Error: $GYP not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64"
|
||||
API=24 # matches android/variables.gradle minSdkVersion
|
||||
|
||||
# Each ABI's libnode.so ships gzipped; better-sqlite3's binding.gyp (via
|
||||
# nodejs-mobile's common.gypi) needs the real .so to add it as a NEEDED entry.
|
||||
for abi in arm64-v8a armeabi-v7a; do
|
||||
LIBNODE_DIR="$NODEDIR/bin/$abi"
|
||||
if [[ ! -f "$LIBNODE_DIR/libnode.so" && -f "$LIBNODE_DIR/libnode.so.gz" ]]; then
|
||||
gunzip -k "$LIBNODE_DIR/libnode.so.gz"
|
||||
fi
|
||||
done
|
||||
|
||||
build_one() {
|
||||
local abi="$1" gyp_arch="$2" cc="$3" cxx="$4"
|
||||
local build_dir="$MOBILE_DIR/build-output/better-sqlite3-$abi"
|
||||
|
||||
echo "── Building better-sqlite3 for $abi ($gyp_arch) ──"
|
||||
rm -rf "$build_dir"
|
||||
mkdir -p "$build_dir"
|
||||
rsync -a --exclude=build --exclude=prebuilds "$PKG_SRC/" "$build_dir/"
|
||||
|
||||
# v8::Object::CreationContext() was removed in newer V8; nodejs-mobile's
|
||||
# bundled V8 (Node 12.19) only has the old non-checked accessor. Same patch
|
||||
# as fix-better-sqlite3-android.sh.
|
||||
sed -i 's/obj->GetCreationContext().ToLocalChecked()/obj->CreationContext()/' \
|
||||
"$build_dir/src/util/binder.cpp"
|
||||
|
||||
(
|
||||
cd "$build_dir"
|
||||
CC="$TOOLCHAIN/bin/$cc" \
|
||||
CXX="$TOOLCHAIN/bin/$cxx" \
|
||||
AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
GYP_DEFINES="OS=android" \
|
||||
"$GYP" configure --target=12.19.0 --arch="$gyp_arch" --nodedir="$NODEDIR"
|
||||
|
||||
CC="$TOOLCHAIN/bin/$cc" \
|
||||
CXX="$TOOLCHAIN/bin/$cxx" \
|
||||
AR="$TOOLCHAIN/bin/llvm-ar" \
|
||||
"$GYP" build
|
||||
)
|
||||
|
||||
mkdir -p "$PREBUILDS_DIR/$abi"
|
||||
cp "$build_dir/build/Release/better_sqlite3.node" "$PREBUILDS_DIR/$abi/better_sqlite3.node"
|
||||
echo "Wrote $PREBUILDS_DIR/$abi/better_sqlite3.node"
|
||||
}
|
||||
|
||||
build_one arm64-v8a arm64 aarch64-linux-android${API}-clang aarch64-linux-android${API}-clang++
|
||||
build_one armeabi-v7a arm armv7a-linux-androideabi${API}-clang armv7a-linux-androideabi${API}-clang++
|
||||
|
||||
echo "Done. Verify with: file $PREBUILDS_DIR/*/better_sqlite3.node"
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Re-applies the two patches better-sqlite3 needs to load correctly under
|
||||
# nodejs-mobile (Node 12.19 / Android x86_64 emulator). Required after any
|
||||
# fresh `npm install` in nodejs-assets/nodejs-project, since both patches
|
||||
# target files inside node_modules.
|
||||
#
|
||||
# Usage: ./scripts/fix-better-sqlite3-android.sh
|
||||
# Run from the mobile project root, after `npm install` has already built
|
||||
# better-sqlite3 once via nodejs-mobile-gyp (so build/better_sqlite3.target.mk
|
||||
# and the Release/ dir already exist).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MOBILE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PKG_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/node_modules/better-sqlite3"
|
||||
LIBNODE="$MOBILE_DIR/android/app/libs/cdvnodejsmobile/libnode/bin/x86_64/libnode.so"
|
||||
|
||||
if [[ ! -d "$PKG_DIR" ]]; then
|
||||
echo "Error: $PKG_DIR not found. Run npm install in nodejs-assets/nodejs-project first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. v8::Object::CreationContext() was removed in newer V8; nodejs-mobile's
|
||||
# bundled V8 (Node 12.19) only has the old non-checked accessor.
|
||||
BINDER="$PKG_DIR/src/util/binder.cpp"
|
||||
if grep -q 'GetCreationContext().ToLocalChecked()' "$BINDER"; then
|
||||
sed -i 's/obj->GetCreationContext().ToLocalChecked()/obj->CreationContext()/' "$BINDER"
|
||||
echo "Patched $BINDER"
|
||||
else
|
||||
echo "$BINDER already uses CreationContext() (no patch needed)"
|
||||
fi
|
||||
|
||||
# 2. nodejs-mobile-cordova's common.gypi only adds the libnode.so NEEDED entry
|
||||
# when target_arch=="x86_64", but gyp reports "x64" for 64-bit x86, so the
|
||||
# rule never fires. Add it to LIBS directly so the addon can resolve V8/Node
|
||||
# symbols (e.g. v8::Isolate::GetCurrent) at dlopen time on Android.
|
||||
TARGET_MK="$PKG_DIR/build/better_sqlite3.target.mk"
|
||||
if [[ ! -f "$TARGET_MK" ]]; then
|
||||
echo "Error: $TARGET_MK not found. Run npm install to build better-sqlite3 first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "libnode.so" "$TARGET_MK"; then
|
||||
sed -i "/^\t-llog \\\\\$/a\\\\\t$LIBNODE" "$TARGET_MK"
|
||||
echo "Patched $TARGET_MK"
|
||||
else
|
||||
echo "$TARGET_MK already references libnode.so (no patch needed)"
|
||||
fi
|
||||
|
||||
# Rebuild and relink with the patched sources/Makefile.
|
||||
cd "$PKG_DIR/build"
|
||||
rm -f Release/better_sqlite3.node Release/obj.target/better_sqlite3.node
|
||||
make BUILDTYPE=Release better_sqlite3.node
|
||||
|
||||
# Keep the x86_64 prebuild (emulator) in sync — main.js picks the right one
|
||||
# for the device's process.arch at runtime. arm64-v8a/armeabi-v7a prebuilds
|
||||
# are produced separately by build-better-sqlite3-arm.sh.
|
||||
PREBUILD_DIR="$MOBILE_DIR/nodejs-assets/nodejs-project/prebuilds/x86_64"
|
||||
mkdir -p "$PREBUILD_DIR"
|
||||
cp "$PKG_DIR/build/Release/better_sqlite3.node" "$PREBUILD_DIR/better_sqlite3.node"
|
||||
|
||||
echo "Done. Verify with: readelf -d \"$PKG_DIR/build/Release/better_sqlite3.node\" | grep libnode"
|
||||
|
|
@ -87,39 +87,6 @@ if (fs.existsSync(nodeJsJava)) {
|
|||
}
|
||||
}
|
||||
|
||||
// Local mode runs the embedded Node server on http://127.0.0.1:3000, which
|
||||
// Android's Network Security Config blocks by default (cleartext traffic).
|
||||
// `cap sync`/`cap add android` don't know about this, so (re)write the config
|
||||
// and wire it into AndroidManifest.xml's <application> tag each time.
|
||||
const networkSecurityConfigXml = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Local mode runs an embedded Node.js server on 127.0.0.1; the WebView
|
||||
(https://localhost) needs to reach it over plain HTTP. -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">127.0.0.1</domain>
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
`;
|
||||
const networkSecurityConfigPath = path.join(
|
||||
MOBILE_DIR, 'android', 'app', 'src', 'main', 'res', 'xml', 'network_security_config.xml'
|
||||
);
|
||||
fs.mkdirSync(path.dirname(networkSecurityConfigPath), { recursive: true });
|
||||
fs.writeFileSync(networkSecurityConfigPath, networkSecurityConfigXml);
|
||||
|
||||
const manifestPath = path.join(MOBILE_DIR, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
const src = fs.readFileSync(manifestPath, 'utf8');
|
||||
if (!src.includes('android:networkSecurityConfig')) {
|
||||
const patched = src.replace(
|
||||
/(<application\b)/,
|
||||
'$1\n android:networkSecurityConfig="@xml/network_security_config"'
|
||||
);
|
||||
fs.writeFileSync(manifestPath, patched);
|
||||
console.log(`Patched ${path.relative(MOBILE_DIR, manifestPath)} (added networkSecurityConfig)`);
|
||||
}
|
||||
}
|
||||
|
||||
// nodejs-mobile-cordova's build.gradle sets up a CMake build with
|
||||
// `cmake.path = "libs/cdvnodejsmobile/CMakeLists.txt"` (relative to each
|
||||
// module's projectDir) for both `app` and `capacitor-cordova-android-plugins`.
|
||||
|
|
|
|||
|
|
@ -54,19 +54,17 @@ rm -rf "$DEST/db/node_modules"
|
|||
mkdir -p "$DEST/docs"
|
||||
cp "$REPO_ROOT/docs/advisory_non_bill_transaction_filters_us_ms_5000.json" "$DEST/docs/"
|
||||
cp "$REPO_ROOT/docs/top_200_us_subscriptions_researched_2026-06-06.json" "$DEST/docs/"
|
||||
cp "$REPO_ROOT/docs/merchant_store_match_us_nems_online_5k_v0_2.json" "$DEST/docs/"
|
||||
|
||||
# Built frontend, served as static files by server.js (path.join(__dirname, 'dist')).
|
||||
cp -R "$REPO_ROOT/dist" "$DEST/dist"
|
||||
|
||||
# nodejs-mobile-cordova 0.4.3 embeds Node 12.19, which doesn't support
|
||||
# optional chaining (?.) / nullish coalescing (??) used in this project and
|
||||
# in some npm dependencies (e.g. express-rate-limit). Transpile server-side
|
||||
# .js files down to Node 12-compatible syntax in place. Runs before the
|
||||
# frontend dist/ is copied below so its browser ESM bundle isn't rewritten
|
||||
# to CJS (which breaks with "module is not defined" in the WebView).
|
||||
# .js files down to Node 12-compatible syntax in place (excludes the
|
||||
# prebuilt frontend bundle in dist/).
|
||||
node "$MOBILE_DIR/scripts/transpile-node12.js" "$DEST"
|
||||
|
||||
# Built frontend, served as static files by server.js (path.join(__dirname, 'dist')).
|
||||
cp -R "$REPO_ROOT/dist" "$DEST/dist"
|
||||
|
||||
# Also transpile already-installed dependencies, if present (run
|
||||
# `npm install` in nodejs-assets/nodejs-project after this script to pick up
|
||||
# new/updated deps, then re-run this script to transpile them).
|
||||
|
|
@ -75,4 +73,4 @@ if [ -d "$NODE_MODULES" ]; then
|
|||
node "$MOBILE_DIR/scripts/transpile-node12.js" "$NODE_MODULES"
|
||||
fi
|
||||
|
||||
echo "Done. Next: cd \"$MOBILE_DIR/nodejs-assets/nodejs-project\" && npm install"
|
||||
echo "Done. Next: cd \"$MOBILE_DIR/nodejs-assets/nodejs-project\" && npm install"
|
||||
112
src/App.tsx
112
src/App.tsx
|
|
@ -1,110 +1,50 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
import { BiometricAuth } from '@aparajita/capacitor-biometric-auth';
|
||||
import SetupScreen from './SetupScreen';
|
||||
import LoadingScreen from './LoadingScreen';
|
||||
import LoginScreen from './LoginScreen';
|
||||
import BiometricSetup from './BiometricSetup';
|
||||
import BiometricLock from './BiometricLock';
|
||||
|
||||
const LOCAL_URL = 'http://127.0.0.1:3000';
|
||||
const SERVER_URL_KEY = 'serverUrl';
|
||||
const BIOMETRIC_KEY = 'biometricEnabled';
|
||||
|
||||
type Stage =
|
||||
| 'checking'
|
||||
| 'setup'
|
||||
| 'login'
|
||||
| 'biometric-setup'
|
||||
| 'biometric-lock'
|
||||
| 'local'
|
||||
| 'redirecting';
|
||||
const LOCAL_URL = 'http://localhost:3000';
|
||||
|
||||
export default function App() {
|
||||
const [stage, setStage] = useState<Stage>('checking');
|
||||
const [serverUrl, setServerUrl] = useState('');
|
||||
const [ready, setReady] = useState(false);
|
||||
const [localMode, setLocalMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Preferences.get({ key: SERVER_URL_KEY }).then(async ({ value }) => {
|
||||
Preferences.get({ key: 'serverUrl' }).then(({ value }) => {
|
||||
if (value === 'local') {
|
||||
setStage('local');
|
||||
return;
|
||||
setLocalMode(true);
|
||||
} else if (value) {
|
||||
// Navigate the WebView to the saved server URL.
|
||||
// From this point the remote server's UI takes over entirely.
|
||||
window.location.replace(value);
|
||||
} else {
|
||||
setReady(true);
|
||||
}
|
||||
if (value) {
|
||||
setServerUrl(value);
|
||||
const { value: biometricEnabled } = await Preferences.get({ key: BIOMETRIC_KEY });
|
||||
setStage(biometricEnabled === 'true' ? 'biometric-lock' : 'redirecting');
|
||||
return;
|
||||
}
|
||||
setStage('setup');
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (stage === 'redirecting') {
|
||||
window.location.replace(serverUrl);
|
||||
}
|
||||
}, [stage, serverUrl]);
|
||||
|
||||
function handleConnect(url: string) {
|
||||
setServerUrl(url);
|
||||
setStage('login');
|
||||
async function handleConnect(url: string) {
|
||||
await Preferences.set({ key: 'serverUrl', value: url });
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
async function handleLocalMode() {
|
||||
await Preferences.set({ key: SERVER_URL_KEY, value: 'local' });
|
||||
setStage('local');
|
||||
await Preferences.set({ key: 'serverUrl', value: 'local' });
|
||||
setLocalMode(true);
|
||||
}
|
||||
|
||||
async function handleLoginSuccess() {
|
||||
await Preferences.set({ key: SERVER_URL_KEY, value: serverUrl });
|
||||
try {
|
||||
const { isAvailable } = await BiometricAuth.checkBiometry();
|
||||
setStage(isAvailable ? 'biometric-setup' : 'redirecting');
|
||||
} catch {
|
||||
setStage('redirecting');
|
||||
}
|
||||
if (localMode) {
|
||||
return <LoadingScreen onReady={() => window.location.replace(LOCAL_URL)} />;
|
||||
}
|
||||
|
||||
async function handleBiometricSetupDone(enabled: boolean) {
|
||||
await Preferences.set({ key: BIOMETRIC_KEY, value: enabled ? 'true' : 'false' });
|
||||
setStage('redirecting');
|
||||
if (!ready) {
|
||||
// Brief blank while we check preferences before redirecting
|
||||
return (
|
||||
<div style={{ background: '#09090b', height: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleBiometricFallback() {
|
||||
await Preferences.set({ key: BIOMETRIC_KEY, value: 'false' });
|
||||
setStage('login');
|
||||
}
|
||||
|
||||
switch (stage) {
|
||||
case 'local':
|
||||
return <LoadingScreen onReady={() => window.location.replace(LOCAL_URL)} />;
|
||||
|
||||
case 'setup':
|
||||
return <SetupScreen onConnect={handleConnect} onLocalMode={handleLocalMode} />;
|
||||
|
||||
case 'login':
|
||||
return (
|
||||
<LoginScreen
|
||||
serverUrl={serverUrl}
|
||||
onSuccess={handleLoginSuccess}
|
||||
onBack={() => setStage('setup')}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'biometric-setup':
|
||||
return <BiometricSetup onDone={handleBiometricSetupDone} />;
|
||||
|
||||
case 'biometric-lock':
|
||||
return <BiometricLock onUnlocked={() => setStage('redirecting')} onFallback={handleBiometricFallback} />;
|
||||
|
||||
default:
|
||||
// 'checking' / 'redirecting' — brief blank while we check preferences
|
||||
// or hand off to the WebView.
|
||||
return (
|
||||
<div style={{ background: '#09090b', height: '100dvh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <SetupScreen onConnect={handleConnect} onLocalMode={handleLocalMode} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { BiometricAuth, BiometryError, BiometryErrorType } from '@aparajita/capacitor-biometric-auth';
|
||||
|
||||
interface Props {
|
||||
onUnlocked: () => void;
|
||||
/** User can't (or won't) use biometrics — fall back to server login. */
|
||||
onFallback: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* App-lock gate shown on launch when biometric unlock is enabled. The
|
||||
* server session cookie persists in the WebView's cookie store between
|
||||
* launches, so this is what stands between "phone unlocked" and "Bill
|
||||
* Tracker dashboard visible".
|
||||
*/
|
||||
export default function BiometricLock({ onUnlocked, onFallback }: Props) {
|
||||
const [error, setError] = useState('');
|
||||
const [checking, setChecking] = useState(true);
|
||||
|
||||
async function attempt() {
|
||||
setError('');
|
||||
setChecking(true);
|
||||
try {
|
||||
await BiometricAuth.authenticate({
|
||||
reason: 'Unlock Bill Tracker',
|
||||
cancelTitle: 'Cancel',
|
||||
allowDeviceCredential: true,
|
||||
androidTitle: 'Unlock Bill Tracker',
|
||||
});
|
||||
onUnlocked();
|
||||
} catch (err) {
|
||||
if (err instanceof BiometryError && err.code === BiometryErrorType.userCancel) {
|
||||
setError('');
|
||||
} else {
|
||||
setError('Authentication failed. Try again.');
|
||||
}
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
attempt();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="setup-root">
|
||||
<div className="setup-card" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
{checking ? (
|
||||
<div className="spinner" />
|
||||
) : (
|
||||
<>
|
||||
<h1 className="setup-title">Locked</h1>
|
||||
<p className="setup-subtitle">{error || 'Authenticate to continue.'}</p>
|
||||
<button className="btn-primary" onClick={attempt} style={{ width: '100%' }}>
|
||||
Try Again
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onFallback} style={{ width: '100%' }}>
|
||||
Sign In Instead
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { BiometricAuth } from '@aparajita/capacitor-biometric-auth';
|
||||
|
||||
interface Props {
|
||||
/** Called with whether biometric unlock should be enabled going forward. */
|
||||
onDone: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown once, right after a successful server login, while biometry is
|
||||
* available on the device. Confirms the user can actually authenticate
|
||||
* before turning the lock on.
|
||||
*/
|
||||
export default function BiometricSetup({ onDone }: Props) {
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleEnable() {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
await BiometricAuth.authenticate({
|
||||
reason: 'Confirm to enable biometric unlock',
|
||||
cancelTitle: 'Cancel',
|
||||
allowDeviceCredential: true,
|
||||
androidTitle: 'Enable biometric unlock',
|
||||
});
|
||||
onDone(true);
|
||||
} catch {
|
||||
setError('Could not verify your identity. You can try again or skip for now.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="setup-root">
|
||||
<div className="setup-card" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<div className="logo-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" rx="10" fill="#6366f1" fillOpacity="0.15" />
|
||||
<path
|
||||
d="M20 12a6 6 0 0 0-6 6v2a6 6 0 0 0 .5 2.4M20 12a6 6 0 0 1 6 6v2c0 3-1 5-2 6.5M14 20v2c0 2.5.7 4.3 1.8 5.8M26 20v2c0 1.3-.2 2.6-.6 3.8M17.5 27.5c.8.9 1.8 1.7 2.9 2.2M11 14a9 9 0 0 1 18 0"
|
||||
stroke="#6366f1"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 className="setup-title">Enable biometric unlock?</h1>
|
||||
<p className="setup-subtitle">
|
||||
Use your fingerprint or face to quickly unlock Bill Tracker the next time you open the
|
||||
app.
|
||||
</p>
|
||||
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
|
||||
<button className="btn-primary" onClick={handleEnable} disabled={loading} style={{ width: '100%' }}>
|
||||
{loading ? 'Verifying…' : 'Enable Biometric Unlock'}
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={() => onDone(false)} disabled={loading} style={{ width: '100%' }}>
|
||||
Not Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { getOrCreateEncryptionKey } from './crypto';
|
||||
|
||||
const LOCAL_URL = 'http://127.0.0.1:3000';
|
||||
const LOCAL_URL = 'http://localhost:3000';
|
||||
const HEALTH_URL = `${LOCAL_URL}/api/health`;
|
||||
const POLL_INTERVAL_MS = 250;
|
||||
|
||||
|
|
@ -35,31 +34,16 @@ export default function LoadingScreen({ onReady }: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
const nodejs = window.nodejs;
|
||||
if (!nodejs) {
|
||||
if (!window.nodejs) {
|
||||
setError('Local server engine is unavailable on this platform.');
|
||||
return;
|
||||
}
|
||||
|
||||
nodejs.start('main.js', err => {
|
||||
window.nodejs.start('main.js', err => {
|
||||
if (err && !cancelled) {
|
||||
setError('Failed to start local server: ' + String(err));
|
||||
return;
|
||||
}
|
||||
if (cancelled) return;
|
||||
|
||||
// Hand the device-bound encryption key to the embedded Node process over
|
||||
// the nodejs-mobile-cordova channel. main.js waits for this message
|
||||
// before requiring server.js, so encryption is configured from the
|
||||
// first request. The listener is registered synchronously to avoid
|
||||
// missing main.js's 'ready' message; the key itself is fetched async.
|
||||
nodejs.channel.on('message', msg => {
|
||||
if (cancelled || (msg as { type?: string })?.type !== 'ready') return;
|
||||
getOrCreateEncryptionKey().then(key => {
|
||||
if (!cancelled) nodejs.channel.post('message', { type: 'encryptionKey', key });
|
||||
});
|
||||
});
|
||||
|
||||
pollHealth();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
serverUrl: string;
|
||||
/** Called once the backend has confirmed the credentials and set a session cookie. */
|
||||
onSuccess: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
type Stage = 'credentials' | 'totp' | 'webauthn';
|
||||
|
||||
interface ApiError {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
async function postJson(url: string, body: unknown): Promise<{ ok: boolean; data: any }> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
export default function LoginScreen({ serverUrl, onSuccess, onBack }: Props) {
|
||||
const [stage, setStage] = useState<Stage>('credentials');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [challengeToken, setChallengeToken] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
function handleKey(e: React.KeyboardEvent) {
|
||||
if (e.key !== 'Enter') return;
|
||||
if (stage === 'credentials') handleLogin();
|
||||
if (stage === 'totp') handleTotp();
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!username.trim() || !password) {
|
||||
setError('Enter your username and password.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const { ok, data } = await postJson(`${serverUrl}/api/auth/login`, {
|
||||
username: username.trim(),
|
||||
password,
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
setError((data as ApiError)?.message || 'Invalid username or password.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.requires_totp) {
|
||||
setChallengeToken(data.challenge_token);
|
||||
setStage('totp');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.requires_webauthn) {
|
||||
setStage('webauthn');
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError('Could not reach the server. Check the URL and your connection.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTotp() {
|
||||
if (!code.trim()) {
|
||||
setError('Enter your authenticator code.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const { ok, data } = await postJson(`${serverUrl}/api/auth/totp/challenge`, {
|
||||
challenge_token: challengeToken,
|
||||
code: code.trim(),
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
setError((data as ApiError)?.message || 'Invalid authenticator code.');
|
||||
return;
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} catch {
|
||||
setError('Could not reach the server. Check the URL and your connection.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (stage === 'webauthn') {
|
||||
return (
|
||||
<div className="setup-root">
|
||||
<div className="setup-card">
|
||||
<h1 className="setup-title">Security key required</h1>
|
||||
<p className="setup-subtitle">
|
||||
This account signs in with a security key. Continue to sign in through the server's
|
||||
web interface.
|
||||
</p>
|
||||
<button className="btn-primary" onClick={onSuccess}>
|
||||
Continue
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onBack}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (stage === 'totp') {
|
||||
return (
|
||||
<div className="setup-root">
|
||||
<div className="setup-card">
|
||||
<h1 className="setup-title">Two-factor code</h1>
|
||||
<p className="setup-subtitle">Enter the code from your authenticator app.</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="totp-code">Code</label>
|
||||
<input
|
||||
id="totp-code"
|
||||
className="form-input"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
placeholder="123456"
|
||||
value={code}
|
||||
onChange={e => { setCode(e.target.value); setError(''); }}
|
||||
onKeyDown={handleKey}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" onClick={handleTotp} disabled={loading || !code.trim()}>
|
||||
{loading ? 'Verifying…' : 'Verify'}
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onBack} disabled={loading}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="setup-root">
|
||||
<div className="setup-card">
|
||||
<h1 className="setup-title">Sign in</h1>
|
||||
<p className="setup-subtitle">Sign in to {serverUrl}</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="login-username">Username</label>
|
||||
<input
|
||||
id="login-username"
|
||||
className="form-input"
|
||||
type="text"
|
||||
inputMode="text"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => { setUsername(e.target.value); setError(''); }}
|
||||
onKeyDown={handleKey}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="login-password">Password</label>
|
||||
<input
|
||||
id="login-password"
|
||||
className="form-input"
|
||||
type="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={e => { setPassword(e.target.value); setError(''); }}
|
||||
onKeyDown={handleKey}
|
||||
disabled={loading}
|
||||
/>
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" onClick={handleLogin} disabled={loading || !username.trim() || !password}>
|
||||
{loading ? 'Signing in…' : 'Sign In'}
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={onBack} disabled={loading}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ export default function SetupScreen({ onConnect, onLocalMode }: Props) {
|
|||
onClick={handleConnect}
|
||||
disabled={connecting || !url.trim()}
|
||||
>
|
||||
{connecting ? 'Connecting…' : 'Continue'}
|
||||
{connecting ? 'Connecting…' : 'Connect to Server'}
|
||||
</button>
|
||||
|
||||
<div className="divider">
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin';
|
||||
|
||||
const KEY_NAME = 'tokenEncryptionKey';
|
||||
const KEY_BYTES = 48;
|
||||
|
||||
function generateHexKey(bytes: number): string {
|
||||
const arr = new Uint8Array(bytes);
|
||||
globalThis.crypto.getRandomValues(arr);
|
||||
return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the device's local-mode database encryption key (TOKEN_ENCRYPTION_KEY),
|
||||
* generating and persisting one in secure storage (Android Keystore / iOS Keychain)
|
||||
* on first launch.
|
||||
*/
|
||||
export async function getOrCreateEncryptionKey(): Promise<string> {
|
||||
try {
|
||||
const { value } = await SecureStoragePlugin.get({ key: KEY_NAME });
|
||||
if (value) return value;
|
||||
} catch {
|
||||
// Not found — fall through to generate one.
|
||||
}
|
||||
|
||||
const key = generateHexKey(KEY_BYTES);
|
||||
await SecureStoragePlugin.set({ key: KEY_NAME, value: key });
|
||||
return key;
|
||||
}
|
||||
|
|
@ -4,9 +4,5 @@
|
|||
interface Window {
|
||||
nodejs?: {
|
||||
start: (filename: string, callback?: (err: unknown) => void) => void;
|
||||
channel: {
|
||||
on: (event: string, callback: (msg: unknown) => void) => void;
|
||||
post: (event: string, message: unknown) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue