diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0802326 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Development +npm run ng:serve # Start Angular dev server (localhost:4200) +npm start # Build Electron app + launch with APP_DEV=true + +# Building +npm run build # Build both Electron app and Angular frontend +npm run build:app # Compile Electron TypeScript (app/ → build/app/) +npm run build:prod # Angular production build (→ dist/branta/browser/) + +# Testing +npm test # Run Jest tests (app/lib/*.spec.ts) +npm run test:watch # Jest watch mode +# Frontend component tests use Karma/Jasmine via: ng test + +# Run a single Jest test file +npx jest app/lib/vault.spec.ts + +# Quality +npm run lint # ESLint (ng lint) +``` + +**Requirements**: Node >=22, npm >=10 + +## Architecture + +Branta Core is an Electron + Angular desktop app that monitors the clipboard for Bitcoin/Nostr content (addresses, xpubs, Lightning payments) and notifies the user if something suspicious or recognizable is detected. + +### Two distinct codebases + +**`app/`** — Electron main process (TypeScript, compiled to CommonJS via `tsconfig.app.json`): +- `main.ts` — Window lifecycle, tray, IPC handlers, clipboard polling (300ms interval via `setInterval`) +- `lib/verify-address.ts` — Bitcoin address and xpub verification using `bip32`, `bip84`, `bitcoinjs-lib`, `tiny-secp256k1` +- `lib/vault.ts` — Parses `branta://` deep-link URLs into Vault objects; handles multisig wallet import +- `lib/lightning.ts` — Decodes Lightning invoices via `bolt11` +- `lib/storage.ts` — JSON file persistence for wallets, vaults, settings, history + +**`src/`** — Angular 19 frontend (compiled separately via Angular CLI): +- Uses Angular Material + ngx-toastr for UI +- Communicates with Electron exclusively via IPC through `window.electron` (defined in `app/preload.ts`) +- `src/app/shared/services/` — Services inject IPC calls; `clipboard.service.ts` receives `clipboard-updated` events from main +- `src/app/features/` — Four main sections: clipboard, wallets, vaults, settings + +### IPC boundary + +Main process exposes handlers via `ipcMain.handle(...)`. The Angular app calls them through the preload-exposed API. Key channels: `verify-address`, `verify-xpub`, `store-data`, `retrieve-data`, `decode-lightning`, `get-all-addresses`, `show-notification`, `open-url`. + +The main process pushes clipboard changes to the renderer via `mainWindow.webContents.send('clipboard-updated', text)`. + +### Dev mode vs production + +`APP_DEV=true` (set by `npm start`) makes Electron load `http://localhost:4200` instead of the built `dist/` files. Run `ng:serve` and `npm start` simultaneously for full hot-reload development. + +### Testing split + +- **Jest** (`jest.config.ts`, node environment): Tests in `app/lib/*.spec.ts` — pure business logic, no Electron dependencies +- **Karma/Jasmine**: Angular component/service tests in `src/**/*.spec.ts` + +### Packaging + +`npm run make` → full build → `electron-forge make` → platform distributables (DEB/RPM/ZIP/DMG/PKG). Windows MSI uses a WiX project in `installers/windows/`. + + +## Adding New Features and Fixing Bugs + +**CRITICAL WORKFLOW - FOLLOW THESE STEPS:** + +### 1. Create a Git Branch FIRST +Before making any code changes, create and switch to a new branch: +- **Format**: `feature/TICKET_NUMBER` for features, `bugfix/TICKET_NUMBER` for bugs +- **Example**: For prompt "Feature 1234: Add admin dashboard", create branch `feature/1234` +- Stay on this branch for the entire session + +### 2. For New Features +1. Implement the feature code +2. Write tests to achieve 100% code coverage for the new feature +3. Run all tests to confirm they pass + +### 3. For Bug Fixes (Test-Driven Approach) +1. **First**: Write a failing test that reproduces the bug +2. Run the test and confirm it fails +3. **Then**: Write code to fix the bug +4. Run the test again and confirm it now passes +5. Run full test suite to ensure no regressions diff --git a/jest.config.ts b/jest.config.ts index 1bd12b6..df3e66c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,7 +3,12 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { preset: 'ts-jest', testEnvironment: 'node', - verbose: true + verbose: true, + testPathIgnorePatterns: [ + '/node_modules/', + '/src/app/app.component.spec.ts', + '/src/app/shared/components/' + ] } export default config; diff --git a/package-lock.json b/package-lock.json index 1876bf9..1b9d179 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "branta", - "version": "0.7.0", + "version": "0.10.2", "dependencies": { "@angular/animations": "^19.2.17", "@angular/cdk": "^19.2.17", @@ -387,6 +387,48 @@ } } }, + "node_modules/@angular-devkit/schematics/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@angular-eslint/builder": { "version": "18.4.3", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-18.4.3.tgz", @@ -446,6 +488,48 @@ } } }, + "node_modules/@angular-eslint/builder/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@angular-eslint/builder/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-eslint/builder/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { "version": "18.4.3", "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.3.tgz", @@ -532,6 +616,48 @@ } } }, + "node_modules/@angular-eslint/schematics/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@angular-eslint/template-parser": { "version": "18.4.3", "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.3.tgz", @@ -567,6 +693,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.17.tgz", "integrity": "sha512-6VTet2fzTpSHEjxcVVzL8ZIyNGo/qsUs4XF/3wh9Iwu6qfWx711qXKlqGD/IHWzMTumzvQXbTV4hzvnO7fJvIQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -762,6 +889,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.17.tgz", "integrity": "sha512-3jG33S+5+kqymCRwQlcSEWlY5rYwkKxe0onln+NXxT0/kteR02vWvv1+Li4/QqSr5JvsGHEhAFsZaR9QtOzbdA==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^7.1.2", "tslib": "^2.3.0" @@ -853,6 +981,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.17.tgz", "integrity": "sha512-yFUXAdpvOFirGD/EGDwp1WHravHzI4sdyRE2iH7i8im9l8IE2VZ6D1KDJp8VVpMJt38LNlRAWYek3s+z6OcAkg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -869,6 +998,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.17.tgz", "integrity": "sha512-qo8psYASAlDiQ8fAL8i/E2JfWH2nPTpZDKKZxSWvgBVA8o+zUEjYAJu6/k6btnu+4Qcb425T0rmM/zao6EU9Aw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -882,6 +1012,7 @@ "integrity": "sha512-KG82fh2A0odttc6+FxlQmFfHY/Giq8rYeV1qtdafafJ8hdWIiMr4r37xwhZOl8uk2/XSLM66bxUMFHYm+zt87Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -910,6 +1041,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.17.tgz", "integrity": "sha512-nVu0ryxfiXUZ9M+NV21TY+rJZkPXTYo9U0aJb19hvByPpG+EvuujXUOgpulz6vxIzGy7pz/znRa+K9kxuuC+yQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -926,6 +1058,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.17.tgz", "integrity": "sha512-INgGGmMbwXuT+niAjMiCsJrZVEGWKZOep1vCRHoKlVnGUQSRKc3UW8ztmKDKMua/io/Opi03pRMpwbYQcTBr5A==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1002,6 +1135,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.17.tgz", "integrity": "sha512-Rn23nIQwYMSeGXWFHI/X8bGHAkdahRxH9UIGUlJKxW61MSkK6AW4kCHG/Ev1TvDq9HjijsMjcqcsd6/Sb8aBXg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1086,6 +1220,7 @@ "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -3044,6 +3179,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4278,6 +4414,7 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -5534,6 +5671,7 @@ "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.1.2", "@inquirer/confirm": "^5.1.6", @@ -9057,6 +9195,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.20.0" } @@ -9307,6 +9446,7 @@ "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/types": "7.11.0", @@ -9556,7 +9696,6 @@ "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" @@ -9575,7 +9714,6 @@ "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", @@ -9603,7 +9741,6 @@ "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" @@ -9622,7 +9759,6 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -9633,7 +9769,6 @@ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9647,7 +9782,6 @@ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9664,7 +9798,6 @@ "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.12" }, @@ -9960,6 +10093,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10065,6 +10199,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -10495,7 +10630,6 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -10515,7 +10649,6 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -10538,7 +10671,6 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10554,8 +10686,7 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", @@ -10563,7 +10694,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -11338,6 +11468,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11798,6 +11929,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -12123,7 +12255,6 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -12807,7 +12938,6 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -12821,7 +12951,6 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -13330,6 +13459,7 @@ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -14469,6 +14599,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -15505,8 +15636,7 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "10.1.0", @@ -17203,7 +17333,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jest": { "version": "29.7.0", @@ -17211,6 +17342,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -18067,6 +18199,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -18400,7 +18533,6 @@ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -18414,7 +18546,6 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -18430,8 +18561,7 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", @@ -18439,7 +18569,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -18463,6 +18592,7 @@ "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -18921,24 +19051,21 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", @@ -18953,8 +19080,7 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -18975,8 +19101,7 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", @@ -22059,6 +22184,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -22678,7 +22804,6 @@ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -22689,7 +22814,6 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -22700,7 +22824,6 @@ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -23186,6 +23309,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -24604,7 +24728,6 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -25072,6 +25195,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -25114,7 +25238,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "3.0.1", @@ -25538,6 +25663,7 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -25900,6 +26026,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -26919,6 +27046,7 @@ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -26996,6 +27124,7 @@ "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -27582,7 +27711,6 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -27598,7 +27726,6 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -27619,7 +27746,8 @@ "version": "0.15.0", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts new file mode 100644 index 0000000..34fe257 --- /dev/null +++ b/src/app/app.component.spec.ts @@ -0,0 +1,83 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { AppComponent } from './app.component'; +import { ConfirmationDialogComponent } from './shared/components/confirmation-dialog/confirmation-dialog.component'; +import { SettingsService } from './shared/services/settings.service'; +import { Settings, BitcoinUnitType, ClipboardHistoryRolloffType } from './shared/models/settings'; + +const makeSettings = (disclaimerAccepted: boolean): Settings => ({ + disclaimerAccepted, + developerMode: false, + bitcoinUnitType: BitcoinUnitType.Sats, + clipboardHistory: { show: true, rolloffType: ClipboardHistoryRolloffType.Never }, + generalNotifications: { + bitcoinAddress: true, + bitcoinPublicKey: true, + nostrPublicKey: true, + nostrPrivateKey: true, + lightningAddress: true + } +}); + +describe('AppComponent disclaimer', () => { + let dialogSpy: any; + let settingsServiceSpy: any; + let dialogRefSpy: any; + + beforeEach(() => { + (window as any).electron = { platform: () => '' }; + dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); + dialogRefSpy.afterClosed.and.returnValue(of(true)); + dialogSpy = jasmine.createSpyObj('MatDialog', ['open']); + dialogSpy.open.and.returnValue(dialogRefSpy); + }); + + afterEach(() => TestBed.resetTestingModule()); + + const createComponent = (disclaimerAccepted: boolean): ComponentFixture => { + const settings = makeSettings(disclaimerAccepted); + settingsServiceSpy = jasmine.createSpyObj('SettingsService', ['save']); + settingsServiceSpy.settings = jasmine.createSpy('settings').and.returnValue(settings); + + TestBed.configureTestingModule({ + imports: [AppComponent], + providers: [ + { provide: MatDialog, useValue: dialogSpy }, + { provide: SettingsService, useValue: settingsServiceSpy }, + provideRouter([]), + provideNoopAnimations() + ] + }); + + TestBed.overrideComponent(AppComponent, { + set: { template: '', imports: [] } + }); + + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + return fixture; + }; + + it('shows disclaimer dialog when not yet accepted', () => { + createComponent(false); + expect(dialogSpy.open).toHaveBeenCalledWith( + ConfirmationDialogComponent, + jasmine.objectContaining({ disableClose: true }) + ); + }); + + it('saves disclaimerAccepted after acknowledging', () => { + createComponent(false); + expect(settingsServiceSpy.save).toHaveBeenCalledWith( + jasmine.objectContaining({ disclaimerAccepted: true }) + ); + }); + + it('does not show dialog when disclaimer already accepted', () => { + createComponent(true); + expect(dialogSpy.open).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b7d978e..d7dc345 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,9 +1,12 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; import { RouterOutlet } from '@angular/router'; import { version } from '../../package.json'; +import { ConfirmationDialogComponent } from './shared/components/confirmation-dialog/confirmation-dialog.component'; import { NavigationComponent } from './core/navigation/navigation.component'; +import { SettingsService } from './shared/services/settings.service'; import { environment } from '../environments/environment'; @Component({ @@ -12,15 +15,38 @@ import { environment } from '../environments/environment'; templateUrl: './app.component.html', styleUrl: './app.component.scss' }) -export class AppComponent { +export class AppComponent implements OnInit { version: string = version + (environment.name == 'production' ? '' : '-dev'); titleBarClass: string = ""; + private dialog = inject(MatDialog); + private settingsService = inject(SettingsService); + constructor() { this.titleBarClass = window.electron.platform(); } + ngOnInit(): void { + if (!this.settingsService.settings().disclaimerAccepted) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + disableClose: true, + data: { + title: 'Welcome to Branta Core', + message: 'Branta Core is free, open source software under the MIT license. By using this software, you acknowledge this.', + submitText: 'I Acknowledge', + hideCancel: true + } + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result === true) { + this.settingsService.save({ ...this.settingsService.settings(), disclaimerAccepted: true }); + } + }); + } + } + onHelp(): void { window.electron.openUrl('https://developer.branta.pro/branta-core'); } diff --git a/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.html b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.html index 58ba728..bc2b3f7 100644 --- a/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.html +++ b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.html @@ -1,6 +1,8 @@

{{ data.title }}

{{ data.message }} - + @if (!data.hideCancel) { + + } diff --git a/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.spec.ts b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.spec.ts new file mode 100644 index 0000000..841314a --- /dev/null +++ b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { ConfirmationDialogComponent } from './confirmation-dialog.component'; + +describe('ConfirmationDialogComponent', () => { + let fixture: ComponentFixture; + + const setup = (data: { title: string; message: string | null; submitText: string; hideCancel?: boolean }) => { + TestBed.configureTestingModule({ + imports: [ConfirmationDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: MatDialogRef, useValue: {} }, + provideNoopAnimations() + ] + }); + fixture = TestBed.createComponent(ConfirmationDialogComponent); + fixture.detectChanges(); + }; + + afterEach(() => TestBed.resetTestingModule()); + + it('shows Cancel button by default', () => { + setup({ title: 'Test', message: 'Test message', submitText: 'OK' }); + const buttons: HTMLButtonElement[] = Array.from(fixture.nativeElement.querySelectorAll('button')); + const cancelButton = buttons.find(btn => btn.textContent?.trim() === 'Cancel'); + expect(cancelButton).toBeTruthy(); + }); + + it('hides Cancel button when hideCancel is true', () => { + setup({ title: 'Test', message: 'Test message', submitText: 'OK', hideCancel: true }); + const buttons: HTMLButtonElement[] = Array.from(fixture.nativeElement.querySelectorAll('button')); + const cancelButton = buttons.find(btn => btn.textContent?.trim() === 'Cancel'); + expect(cancelButton).toBeFalsy(); + }); +}); diff --git a/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.ts b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.ts index 74f7a1f..1f7e225 100644 --- a/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/app/shared/components/confirmation-dialog/confirmation-dialog.component.ts @@ -9,6 +9,6 @@ import { MAT_DIALOG_DATA, MatDialogActions, MatDialogClose, MatDialogContent, Ma styleUrl: './confirmation-dialog.component.scss' }) export class ConfirmationDialogComponent { - readonly data = inject<{ title: string, message: string | null, submitText: string }>(MAT_DIALOG_DATA); + readonly data = inject<{ title: string, message: string | null, submitText: string, hideCancel?: boolean }>(MAT_DIALOG_DATA); } diff --git a/src/app/shared/models/settings.ts b/src/app/shared/models/settings.ts index c3fe9b9..d53479d 100644 --- a/src/app/shared/models/settings.ts +++ b/src/app/shared/models/settings.ts @@ -3,6 +3,7 @@ export interface Settings { generalNotifications: GeneralNotifications; clipboardHistory: ClipboardHistory; bitcoinUnitType: BitcoinUnitType; + disclaimerAccepted: boolean; } export enum ClipboardHistoryRolloffType { diff --git a/src/app/shared/services/settings.service.ts b/src/app/shared/services/settings.service.ts index 5c5d759..c6adc93 100644 --- a/src/app/shared/services/settings.service.ts +++ b/src/app/shared/services/settings.service.ts @@ -8,6 +8,7 @@ export class SettingsService { defaultSettings: Settings = { bitcoinUnitType: BitcoinUnitType.Sats, developerMode: false, + disclaimerAccepted: false, clipboardHistory: { show: true, rolloffType: ClipboardHistoryRolloffType.Never