From c55390228bfa060bbf993738460fdfb7e73f7f63 Mon Sep 17 00:00:00 2001 From: LC mac Date: Wed, 28 Jan 2026 23:54:02 +0800 Subject: [PATCH] feat(v1.2.0): add QR scanner with camera/upload support - Add QRScanner component with camera and image upload - Add QR code download button with auto-naming (SeedPGP_DATE_TIME.png) - Split state for backup/restore (separate public/private keys and passwords) - Improve QR generation settings (margin: 4, errorCorrection: M) - Fix Safari camera permissions and Continuity Camera support - Add React timing fix for Html5Qrcode initialization Features: - Camera scanning with live preview - Image file upload scanning - Automatic SEEDPGP1 validation - User-friendly error messages - 512x512px high-quality QR generation --- bun.lock | 17 +- package.json | 1 + src/App.tsx | 569 ++++++++++++++++++----------------- src/components/QRScanner.tsx | 224 ++++++++++++++ src/components/QrDisplay.tsx | 49 ++- 5 files changed, 576 insertions(+), 284 deletions(-) create mode 100644 src/components/QRScanner.tsx diff --git a/bun.lock b/bun.lock index 24cbf50..ebcf2f2 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "seedpgp-web", "dependencies": { + "html5-qrcode": "^2.3.8", "lucide-react": "^0.462.0", "openpgp": "^6.3.0", "qrcode": "^1.5.4", @@ -195,7 +196,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -223,7 +224,7 @@ "autoprefixer": ["autoprefixer@10.4.23", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], @@ -231,7 +232,7 @@ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], - "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], @@ -297,6 +298,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "html5-qrcode": ["html5-qrcode@2.3.8", "", {}, "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -459,12 +462,8 @@ "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - "@types/qrcode/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "bun-types/@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], - "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -472,9 +471,5 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - - "@types/qrcode/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/package.json b/package.json index db56fb9..6d7184a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "html5-qrcode": "^2.3.8", "lucide-react": "^0.462.0", "openpgp": "^6.3.0", "qrcode": "^1.5.4", diff --git a/src/App.tsx b/src/App.tsx index 4684668..b58d463 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,17 +14,22 @@ import { } from 'lucide-react'; import { PgpKeyInput } from './components/PgpKeyInput'; import { QrDisplay } from './components/QrDisplay'; +import QRScanner from './components/QRScanner'; import { validateBip39Mnemonic } from './lib/bip39'; import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp'; import type { SeedPgpPlaintext } from './lib/types'; import * as openpgp from 'openpgp'; + console.log("OpenPGP.js version:", openpgp.config.versionString); function App() { const [activeTab, setActiveTab] = useState<'backup' | 'restore'>('backup'); const [mnemonic, setMnemonic] = useState(''); - const [messagePassword, setMessagePassword] = useState(''); - const [pgpKeyInput, setPgpKeyInput] = useState(''); + const [backupMessagePassword, setBackupMessagePassword] = useState(''); + const [restoreMessagePassword, setRestoreMessagePassword] = useState(''); + + const [publicKeyInput, setPublicKeyInput] = useState(''); + const [privateKeyInput, setPrivateKeyInput] = useState(''); const [privateKeyPassphrase, setPrivateKeyPassphrase] = useState(''); const [hasBip39Passphrase, setHasBip39Passphrase] = useState(false); const [qrPayload, setQrPayload] = useState(''); @@ -35,6 +40,7 @@ function App() { const [loading, setLoading] = useState(false); const [showMnemonic, setShowMnemonic] = useState(false); const [copied, setCopied] = useState(false); + const [showQRScanner, setShowQRScanner] = useState(false); const copyToClipboard = async (text: string) => { try { @@ -42,7 +48,6 @@ function App() { setCopied(true); window.setTimeout(() => setCopied(false), 1500); } catch { - // Fallback for environments where Clipboard API is blocked const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; @@ -57,7 +62,6 @@ function App() { } }; - const handleBackup = async () => { setLoading(true); setError(''); @@ -74,8 +78,8 @@ function App() { const result = await encryptToSeedPgp({ plaintext, - publicKeyArmored: pgpKeyInput || undefined, - messagePassword: messagePassword || undefined, + publicKeyArmored: publicKeyInput || undefined, + messagePassword: backupMessagePassword || undefined, // Changed }); setQrPayload(result.framed); @@ -97,11 +101,12 @@ function App() { try { const result = await decryptSeedPgp({ frameText: restoreInput, - privateKeyArmored: pgpKeyInput || undefined, + privateKeyArmored: privateKeyInput || undefined, privateKeyPassphrase: privateKeyPassphrase || undefined, - messagePassword: messagePassword || undefined, + messagePassword: restoreMessagePassword || undefined, // Changed }); + setRestoredData(result); } catch (e) { setError(e instanceof Error ? e.message : 'Decryption failed'); @@ -110,296 +115,322 @@ function App() { } }; + return ( -
-
+ <> +
+
- {/* Header */} -
-
-
- -
-
-

- SeedPGP v1.1 -

-

OpenPGP-secured BIP39 backup

-
-
-
- - -
-
- -
- {/* Error Display */} - {error && ( -
- + {/* Header */} +
+
+
+ +
-

Error

-

{error}

+

+ SeedPGP v1.2 +

+

OpenPGP-secured BIP39 backup

- )} - - {/* Info Banner */} - {recipientFpr && activeTab === 'backup' && ( -
- -
- Recipient Key: {recipientFpr} -
+
+ +
- )} +
- {/* Main Content Grid */} -
-
- {activeTab === 'backup' ? ( - <> -
- -