From 294a6f27c4472781c0b99bc0af6f7807178e8414 Mon Sep 17 00:00:00 2001 From: Austin Kelsay Date: Fri, 2 Jan 2026 17:09:54 -0600 Subject: [PATCH] qr code display for shares and group --- package-lock.json | 351 +++++++++++++++++++++++- package.json | 2 + src/components/keyset/KeysetCreate.tsx | 2 + src/components/keyset/KeysetLoad.tsx | 53 +++- src/components/keyset/ShareSaver.tsx | 53 +++- src/components/ui/CredentialDisplay.tsx | 31 +++ src/components/ui/QRCodeDisplay.tsx | 78 ++++++ 7 files changed, 549 insertions(+), 21 deletions(-) create mode 100644 src/components/ui/CredentialDisplay.tsx create mode 100644 src/components/ui/QRCodeDisplay.tsx diff --git a/package-lock.json b/package-lock.json index 405aeb7..b2775d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@noble/hashes": "^2.0.1", "ink": "^6.3.1", "nostr-tools": "^2.17.0", + "qrcode": "^1.5.4", "react": "^19.1.1", "ws": "^8.18.0" }, @@ -25,6 +26,7 @@ }, "devDependencies": { "@types/node": "^24.6.1", + "@types/qrcode": "^1.5.6", "@types/react": "^19.1.16", "tsup": "^8.5.0", "tsx": "^4.20.6", @@ -1162,6 +1164,16 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.2", "devOptional": true, @@ -1265,6 +1277,15 @@ "node": ">=8" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "5.6.2", "license": "MIT", @@ -1350,6 +1371,96 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/code-excerpt": { "version": "4.0.0", "license": "MIT", @@ -1362,7 +1473,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1373,7 +1483,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -1438,6 +1547,21 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, @@ -1529,6 +1653,19 @@ } } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fix-dts-default-cjs-exports": { "version": "1.0.1", "dev": true, @@ -1577,6 +1714,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "license": "MIT", @@ -1751,6 +1897,18 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash.sortby": { "version": "4.7.0", "dev": true, @@ -1918,6 +2076,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "dev": true, @@ -1930,6 +2124,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "dev": true, @@ -1993,6 +2196,15 @@ "pathe": "^2.0.1" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss-load-config": { "version": "6.0.1", "dev": true, @@ -2042,6 +2254,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/react": { "version": "19.2.0", "license": "MIT", @@ -2075,6 +2304,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve-from": { "version": "5.0.0", "dev": true, @@ -2149,6 +2393,12 @@ "version": "0.26.0", "license": "MIT" }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -2514,6 +2764,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/widest-line": { "version": "5.0.0", "license": "MIT", @@ -2637,6 +2893,97 @@ } } }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yoga-layout": { "version": "3.2.1", "license": "MIT" diff --git a/package.json b/package.json index 4e05904..d2795e1 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,13 @@ "@noble/hashes": "^2.0.1", "ink": "^6.3.1", "nostr-tools": "^2.17.0", + "qrcode": "^1.5.4", "react": "^19.1.1", "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^24.6.1", + "@types/qrcode": "^1.5.6", "@types/react": "^19.1.16", "tsup": "^8.5.0", "tsx": "^4.20.6", diff --git a/src/components/keyset/KeysetCreate.tsx b/src/components/keyset/KeysetCreate.tsx index 9adb483..79c6dc8 100644 --- a/src/components/keyset/KeysetCreate.tsx +++ b/src/components/keyset/KeysetCreate.tsx @@ -130,6 +130,7 @@ export function KeysetCreate({flags}: KeysetCreateProps) { const passwordFilePath = typeof flags['password-file'] === 'string' ? flags['password-file'] : undefined; const outputDirFlag = typeof flags.output === 'string' ? flags.output : undefined; const resolvedOutputDir = outputDirFlag ? path.resolve(process.cwd(), outputDirFlag) : undefined; + const showQR = flags.qr === true || flags['show-qr'] === true; const [automationPassword, setAutomationPassword] = useState(directPassword); const [automationError, setAutomationError] = useState(null); const [automationLoading, setAutomationLoading] = useState(Boolean(passwordFilePath && !directPassword)); @@ -495,6 +496,7 @@ export function KeysetCreate({flags}: KeysetCreateProps) { }} autoPassword={automationPassword} outputDir={resolvedOutputDir} + showQR={showQR} /> diff --git a/src/components/keyset/KeysetLoad.tsx b/src/components/keyset/KeysetLoad.tsx index a3cb940..0c581c5 100644 --- a/src/components/keyset/KeysetLoad.tsx +++ b/src/components/keyset/KeysetLoad.tsx @@ -1,8 +1,9 @@ import React, {useEffect, useMemo, useState} from 'react'; -import {Box, Text} from 'ink'; +import {Box, Text, useInput, useStdin} from 'ink'; import {decodeGroup, decodeShare} from '@frostr/igloo-core'; import {readShareFiles, decryptShareCredential, ShareMetadata} from '../../keyset/index.js'; import {Prompt} from '../ui/Prompt.js'; +import {CredentialDisplay} from '../ui/CredentialDisplay.js'; import {useShareEchoListener} from './useShareEchoListener.js'; type KeysetLoadProps = { @@ -26,6 +27,10 @@ export function KeysetLoad({args, flags}: KeysetLoadProps) { const [autoDecrypting, setAutoDecrypting] = useState(false); const [autoError, setAutoError] = useState(null); + const showQRFlag = flags?.qr === true || flags?.['show-qr'] === true; + const [qrVisible, setQrVisible] = useState(showQRFlag); + const {isRawModeSupported} = useStdin(); + useEffect(() => { void (async () => { try { @@ -139,6 +144,12 @@ export function KeysetLoad({args, flags}: KeysetLoadProps) { skipEcho ? undefined : decryptedShare ); + useInput((input) => { + if (input === 'q' || input === 'Q') { + setQrVisible(v => !v); + } + }, {isActive: isRawModeSupported && phase === 'result'}); + if (state.loading) { return ( @@ -275,12 +286,23 @@ export function KeysetLoad({args, flags}: KeysetLoadProps) { return ( Share decrypted successfully. - Share credential - {result.share} - - Group credential - - {result.group} + + + {isRawModeSupported ? ( + Press Q to {qrVisible ? 'hide' : 'show'} QR codes + ) : null} {shareIndex !== undefined ? ( Share details @@ -311,6 +333,23 @@ export function KeysetLoad({args, flags}: KeysetLoadProps) { Echo confirmed by the receiving device. ) : null} + {isRawModeSupported ? ( + + { + try { + if (typeof process !== 'undefined' && typeof process.exit === 'function') { + process.exit(0); + } + } catch {} + return undefined; + }} + /> + + ) : null} ); } diff --git a/src/components/keyset/ShareSaver.tsx b/src/components/keyset/ShareSaver.tsx index 70d7e25..e2757e7 100644 --- a/src/components/keyset/ShareSaver.tsx +++ b/src/components/keyset/ShareSaver.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useMemo, useState} from 'react'; -import {Box, Text} from 'ink'; +import {Box, Text, useInput} from 'ink'; import {decodeShare} from '@frostr/igloo-core'; import { deriveSecret, @@ -15,6 +15,7 @@ import { createDefaultPolicy } from '../../keyset/index.js'; import {Prompt} from '../ui/Prompt.js'; +import {CredentialDisplay} from '../ui/CredentialDisplay.js'; import {useShareEchoListener} from './useShareEchoListener.js'; type ShareSaverProps = { @@ -27,6 +28,7 @@ type ShareSaverProps = { }) => void; autoPassword?: string; outputDir?: string; + showQR?: boolean; }; type ShareState = { @@ -42,7 +44,8 @@ export function ShareSaver({ shareCredentials, onComplete, autoPassword, - outputDir + outputDir, + showQR = false }: ShareSaverProps) { const [currentIndex, setCurrentIndex] = useState(0); const [phase, setPhase] = useState('password'); @@ -53,6 +56,7 @@ export function ShareSaver({ const [notified, setNotified] = useState(false); const [autoState, setAutoState] = useState<'idle' | 'running' | 'done' | 'error'>('idle'); const [autoError, setAutoError] = useState(null); + const [qrVisible, setQrVisible] = useState(showQR); const shares = useMemo(() => { return shareCredentials.map((credential, idx) => { @@ -85,25 +89,47 @@ export function ShareSaver({ share?.credential ); + useInput((input) => { + if (input === 'q' || input === 'Q') { + setQrVisible(v => !v); + } + }, {isActive: shouldPrompt}); + + const qrHint = ( + Press Q to {qrVisible ? 'hide' : 'show'} QR codes + ); + const shareCredentialBlock = ( - - Share credential - {share?.credential ?? 'unknown'} - + ); const groupCredentialBlock = ( - - Group credential - {groupCredential} - + ); const summaryView = ( All shares processed. - Group credential: - {groupCredential} + + {shouldPrompt ? qrHint : null} {savedPaths.length > 0 ? ( Saved files @@ -368,6 +394,7 @@ export function ShareSaver({ Encrypting share {share.index}… {shareCredentialBlock} {groupCredentialBlock} + {shouldPrompt ? qrHint : null} {renderEchoStatus()} ); @@ -380,6 +407,7 @@ export function ShareSaver({ {feedback ? {feedback} : null} {shareCredentialBlock} {groupCredentialBlock} + {shouldPrompt ? qrHint : null} {renderEchoStatus()} Share {share.index} of {shareCredentials.length} {shareCredentialBlock} {groupCredentialBlock} + {shouldPrompt ? qrHint : null} {renderEchoStatus()} Set a password to encrypt this share. Leave blank to skip saving and handle it manually. diff --git a/src/components/ui/CredentialDisplay.tsx b/src/components/ui/CredentialDisplay.tsx new file mode 100644 index 0000000..419b502 --- /dev/null +++ b/src/components/ui/CredentialDisplay.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {Box, Text} from 'ink'; +import {QRCodeDisplay} from './QRCodeDisplay.js'; + +type CredentialDisplayProps = { + label: string; + credential: string; + labelColor?: string; + textColor?: string; + showQR?: boolean; +}; + +export function CredentialDisplay({ + label, + credential, + labelColor = 'cyanBright', + textColor = 'white', + showQR = false +}: CredentialDisplayProps) { + return ( + + {label} + {credential} + {showQR ? ( + + + + ) : null} + + ); +} diff --git a/src/components/ui/QRCodeDisplay.tsx b/src/components/ui/QRCodeDisplay.tsx new file mode 100644 index 0000000..1b84069 --- /dev/null +++ b/src/components/ui/QRCodeDisplay.tsx @@ -0,0 +1,78 @@ +import {useEffect, useState} from 'react'; +import {Box, Text} from 'ink'; +import QRCode from 'qrcode'; + +type QRCodeDisplayProps = { + value: string; + label?: string; + labelColor?: string; + small?: boolean; +}; + +export function QRCodeDisplay({ + value, + label, + labelColor = 'cyan', + small = true +}: QRCodeDisplayProps) { + const [qrString, setQrString] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!value) { + setQrString(null); + setError(null); + return; + } + + let canceled = false; + + void (async () => { + try { + const result = await QRCode.toString(value, { + type: 'terminal', + small, + errorCorrectionLevel: 'M' + }); + if (!canceled) { + setQrString(result); + setError(null); + } + } catch (err: any) { + if (!canceled) { + setError(err?.message ?? 'Failed to generate QR code'); + setQrString(null); + } + } + })(); + + return () => { + canceled = true; + }; + }, [value, small]); + + if (error) { + return ( + + {label ? {label} : null} + QR error: {error} + + ); + } + + if (!qrString) { + return ( + + {label ? {label} : null} + Generating QR code... + + ); + } + + return ( + + {label ? {label} : null} + {qrString} + + ); +}