From 58e9053867281cfcb463d4ee3a6032ed5f1ea993 Mon Sep 17 00:00:00 2001 From: woikos Date: Tue, 6 Jan 2026 17:30:42 +0100 Subject: [PATCH] Release v1.2.0 - Streamlined vault creation flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove sync preference welcome page, default to no-sync - Redesign vault-create home with nickname + nsec input - Add generate key button, visibility toggle, clipboard copy - Add vault file import with persistent snapshot list - Navigate to profile view after identity creation - Fix router state access for identity data passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 4 +- package.json | 2 +- projects/chrome/public/manifest.json | 2 +- projects/chrome/src/app/app.routes.ts | 5 - .../vault-create/home/home.component.html | 124 ++++++++++++-- .../vault-create/home/home.component.scss | 81 ++++++++- .../vault-create/home/home.component.ts | 156 +++++++++++++++++- .../vault-create/new/new.component.ts | 33 +++- .../components/welcome/welcome.component.html | 64 ------- .../components/welcome/welcome.component.scss | 46 ------ .../welcome/welcome.component.spec.ts | 23 --- .../components/welcome/welcome.component.ts | 41 ----- .../lib/services/startup/startup.service.ts | 6 +- projects/firefox/public/manifest.json | 10 +- projects/firefox/src/app/app.routes.ts | 5 - .../vault-create/home/home.component.html | 126 +++++++++++--- .../vault-create/home/home.component.scss | 81 ++++++++- .../vault-create/home/home.component.ts | 155 ++++++++++++++++- .../vault-create/new/new.component.ts | 33 +++- .../components/welcome/welcome.component.html | 64 ------- .../components/welcome/welcome.component.scss | 46 ------ .../welcome/welcome.component.spec.ts | 23 --- .../components/welcome/welcome.component.ts | 41 ----- 23 files changed, 747 insertions(+), 424 deletions(-) delete mode 100644 projects/chrome/src/app/components/welcome/welcome.component.html delete mode 100644 projects/chrome/src/app/components/welcome/welcome.component.scss delete mode 100644 projects/chrome/src/app/components/welcome/welcome.component.spec.ts delete mode 100644 projects/chrome/src/app/components/welcome/welcome.component.ts delete mode 100644 projects/firefox/src/app/components/welcome/welcome.component.html delete mode 100644 projects/firefox/src/app/components/welcome/welcome.component.scss delete mode 100644 projects/firefox/src/app/components/welcome/welcome.component.spec.ts delete mode 100644 projects/firefox/src/app/components/welcome/welcome.component.ts diff --git a/package-lock.json b/package-lock.json index 4ba3fd9..ac66597 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plebeian-signer", - "version": "v1.0.8", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plebeian-signer", - "version": "v1.0.8", + "version": "1.2.0", "dependencies": { "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", diff --git a/package.json b/package.json index 8666646..f6bea44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plebeian-signer", - "version": "1.1.6", + "version": "1.2.0", "custom": { "chrome": { "version": "v1.1.6" diff --git a/projects/chrome/public/manifest.json b/projects/chrome/public/manifest.json index 83004b5..b8182f3 100644 --- a/projects/chrome/public/manifest.json +++ b/projects/chrome/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Plebeian Signer - Nostr Identity Manager & Signer", "description": "Manage and switch between multiple identities while interacting with Nostr apps", - "version": "1.1.5", + "version": "1.1.6", "homepage_url": "https://github.com/PlebeianApp/plebeian-signer", "options_page": "options.html", "permissions": [ diff --git a/projects/chrome/src/app/app.routes.ts b/projects/chrome/src/app/app.routes.ts index 37c833a..882e7fd 100644 --- a/projects/chrome/src/app/app.routes.ts +++ b/projects/chrome/src/app/app.routes.ts @@ -4,7 +4,6 @@ import { VaultLoginComponent } from './components/vault-login/vault-login.compon import { VaultCreateComponent } from './components/vault-create/vault-create.component'; import { HomeComponent as VaultCreateHomeComponent } from './components/vault-create/home/home.component'; import { NewComponent as VaultCreateNewComponent } from './components/vault-create/new/new.component'; -import { WelcomeComponent } from './components/welcome/welcome.component'; import { IdentitiesComponent } from './components/home/identities/identities.component'; import { IdentityComponent } from './components/home/identity/identity.component'; import { InfoComponent } from './components/home/info/info.component'; @@ -25,10 +24,6 @@ import { WhitelistedAppsComponent } from './components/whitelisted-apps/whitelis import { ProfileEditComponent } from './components/profile-edit/profile-edit.component'; export const routes: Routes = [ - { - path: 'welcome', - component: WelcomeComponent, - }, { path: 'vault-login', component: VaultLoginComponent, diff --git a/projects/chrome/src/app/components/vault-create/home/home.component.html b/projects/chrome/src/app/components/vault-create/home/home.component.html index a11a11a..ab51205 100644 --- a/projects/chrome/src/app/components/vault-create/home/home.component.html +++ b/projects/chrome/src/app/components/vault-create/home/home.component.html @@ -1,32 +1,120 @@ -
-
-
- Plebeian Signer +
+
+
+ +
+ Plebeian Signer +
-
- -
+ +
+

Restore or Create New Identity

+ Create a new nostr identity or paste in your current nsec. + + + +
+ + + +
+ +
- or -
+ + +
+

Import a Vault

+ + + +
+ + + @if (snapshots.length > 0) { +
+ + +
+ } +
+
diff --git a/projects/chrome/src/app/components/vault-create/home/home.component.scss b/projects/chrome/src/app/components/vault-create/home/home.component.scss index 225ac6b..a2a93c9 100644 --- a/projects/chrome/src/app/components/vault-create/home/home.component.scss +++ b/projects/chrome/src/app/components/vault-create/home/home.component.scss @@ -2,18 +2,26 @@ height: 100%; display: flex; flex-direction: column; + overflow-y: auto; - .vertically-centered { - height: 100%; + .container { display: flex; - justify-content: center; + flex-direction: column; + padding: var(--size); + gap: var(--size); + } + + .logo-section { + display: flex; + flex-direction: column; align-items: center; + gap: var(--size-half); + padding-bottom: var(--size-half); } .title { font-size: 20px; font-weight: 500; - margin-bottom: var(--size); } .logo-frame { @@ -21,8 +29,73 @@ border-radius: 100%; } + .section { + display: flex; + flex-direction: column; + gap: var(--size); + margin-top: var(--size); + } + + .section-heading { + font-size: 16px; + font-weight: 600; + margin: 0; + } + + .section-note { + font-size: 14px; + color: var(--muted-foreground); + } + + .button-row { + display: flex; + gap: var(--size); + justify-content: flex-end; + } + + .generate-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + + .continue-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + .file-input { position: absolute; visibility: hidden; } + + .file-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + + .import-controls { + display: flex; + flex-direction: column; + gap: var(--size); + } + + .import-row { + display: flex; + gap: var(--size-half); + + select { + flex: 1; + } + } + + .icon-btn { + width: 42px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + } } diff --git a/projects/chrome/src/app/components/vault-create/home/home.component.ts b/projects/chrome/src/app/components/vault-create/home/home.component.ts index b41f328..9568401 100644 --- a/projects/chrome/src/app/components/vault-create/home/home.component.ts +++ b/projects/chrome/src/app/components/vault-create/home/home.component.ts @@ -1,13 +1,161 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; -import { NavComponent } from '@common'; +import { + NavComponent, + NostrHelper, + StorageService, + StartupService, + SignerMetaData_VaultSnapshot, + BrowserSyncData, +} from '@common'; +import { generateSecretKey } from 'nostr-tools'; +import { bytesToHex } from '@noble/hashes/utils'; +import { v4 as uuidv4 } from 'uuid'; +import browser from 'webextension-polyfill'; +import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage-service-config'; + +const VAULT_SNAPSHOTS_KEY = 'vaultSnapshots'; @Component({ selector: 'app-home', - imports: [], + imports: [FormsModule], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) -export class HomeComponent extends NavComponent { +export class HomeComponent extends NavComponent implements OnInit { readonly router = inject(Router); + readonly #storage = inject(StorageService); + readonly #startup = inject(StartupService); + + nickname = ''; + nsecInput = ''; + isNsecValid = false; + snapshots: SignerMetaData_VaultSnapshot[] = []; + selectedSnapshot: SignerMetaData_VaultSnapshot | undefined; + + ngOnInit(): void { + this.#loadSnapshots(); + } + + generateKey() { + const sk = generateSecretKey(); + const privkey = bytesToHex(sk); + this.nsecInput = NostrHelper.privkey2nsec(privkey); + this.validateNsec(); + } + + toggleVisibility(element: HTMLInputElement) { + element.type = element.type === 'password' ? 'text' : 'password'; + } + + async copyToClipboard() { + if (this.nsecInput) { + await navigator.clipboard.writeText(this.nsecInput); + } + } + + validateNsec() { + if (!this.nsecInput) { + this.isNsecValid = false; + return; + } + + try { + NostrHelper.getNostrPrivkeyObject(this.nsecInput.toLowerCase()); + this.isNsecValid = true; + } catch { + this.isNsecValid = false; + } + } + + onContinueWithNsec() { + if (!this.isNsecValid || !this.nickname) { + return; + } + // Navigate to password step, passing nsec and nickname in state + this.router.navigateByUrl('/vault-create/new', { + state: { nsec: this.nsecInput, nickname: this.nickname }, + }); + } + + async onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + const files = input.files; + if (!files || files.length === 0) { + return; + } + + try { + const file = files[0]; + const text = await file.text(); + const vault = JSON.parse(text) as BrowserSyncData; + + // Check if file already exists + if (this.snapshots.some((s) => s.fileName === file.name)) { + input.value = ''; + return; + } + + const newSnapshot: SignerMetaData_VaultSnapshot = { + id: uuidv4(), + fileName: file.name, + createdAt: new Date().toISOString(), + data: vault, + identityCount: vault.identities?.length ?? 0, + reason: 'manual', + }; + + this.snapshots = [...this.snapshots, newSnapshot].sort((a, b) => + b.fileName.localeCompare(a.fileName) + ); + this.selectedSnapshot = newSnapshot; + + await this.#saveSnapshots(); + } catch (error) { + console.error('Failed to load vault file:', error); + } + + // Reset input so same file can be selected again + input.value = ''; + } + + async onImport() { + if (!this.selectedSnapshot) { + return; + } + + try { + await this.#storage.deleteVault(true); + await this.#storage.importVault(this.selectedSnapshot.data); + + // Restart the app to properly reinitialize and route to vault-login + this.#storage.isInitialized = false; + this.#startup.startOver(getNewStorageServiceConfig()); + } catch (error) { + console.error('Failed to import vault:', error); + } + } + + async #loadSnapshots() { + const data = (await browser.storage.local.get(VAULT_SNAPSHOTS_KEY)) as { + vaultSnapshots?: SignerMetaData_VaultSnapshot[]; + }; + + this.snapshots = data.vaultSnapshots + ? [...data.vaultSnapshots].sort((a, b) => + b.fileName.localeCompare(a.fileName) + ) + : []; + + if (this.snapshots.length > 0) { + this.selectedSnapshot = this.snapshots[0]; + } + } + + async #saveSnapshots() { + await browser.storage.local.set({ + [VAULT_SNAPSHOTS_KEY]: this.snapshots, + }); + } } diff --git a/projects/chrome/src/app/components/vault-create/new/new.component.ts b/projects/chrome/src/app/components/vault-create/new/new.component.ts index 1cc497d..24f3cd7 100644 --- a/projects/chrome/src/app/components/vault-create/new/new.component.ts +++ b/projects/chrome/src/app/components/vault-create/new/new.component.ts @@ -1,7 +1,12 @@ import { Component, inject, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; -import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common'; +import { + LoggerService, + NavComponent, + StorageService, + DerivingModalComponent, +} from '@common'; @Component({ selector: 'app-new', @@ -18,6 +23,15 @@ export class NewComponent extends NavComponent { readonly #storage = inject(StorageService); readonly #logger = inject(LoggerService); + // Access router state via history.state (persists after navigation completes) + get #nsec(): string | undefined { + return history.state?.nsec; + } + + get #nickname(): string | undefined { + return history.state?.nickname; + } + toggleType(element: HTMLInputElement) { if (element.type === 'password') { element.type = 'text'; @@ -35,9 +49,22 @@ export class NewComponent extends NavComponent { this.derivingModal.show('Creating secure vault'); try { await this.#storage.createNewVault(this.password); - this.derivingModal.hide(); this.#logger.logVaultCreated(); - this.#router.navigateByUrl('/home/identities'); + + // If nsec and nickname were passed, add the identity + if (this.#nsec && this.#nickname) { + try { + await this.#storage.addIdentity({ + nick: this.#nickname, + privkeyString: this.#nsec, + }); + } catch (error) { + console.error('Failed to add identity:', error); + } + } + + this.derivingModal.hide(); + this.#router.navigateByUrl('/home/identity'); } catch (error) { this.derivingModal.hide(); console.error('Failed to create vault:', error); diff --git a/projects/chrome/src/app/components/welcome/welcome.component.html b/projects/chrome/src/app/components/welcome/welcome.component.html deleted file mode 100644 index aa47ce1..0000000 --- a/projects/chrome/src/app/components/welcome/welcome.component.html +++ /dev/null @@ -1,64 +0,0 @@ -
- Plebeian Signer Setup - Sync Preference -
- - - Plebeian Signer always encrypts sensitive data like private keys and site permissions - independent of the chosen sync mode. - - -Sync : Google Chrome - - - Your encrypted data is synced between browser instances. You need to be signed - in with your account. - - - - -Offline - - - Your encrypted data is never uploaded to any servers. It remains in your local - browser instance. - - - - -
-
- Important for Cashu wallet users -

- Browser sync storage is limited to ~100KB shared across all data - (identities, permissions, relays, and Cashu tokens). -

-

- If you plan to use the Cashu ecash wallet with significant balances, - choose "Sync OFF" which provides ~5MB of local storage - (enough for ~18,000+ tokens vs ~300-400 with sync). -

-

- Note: Cashu tokens are bearer assets. If you lose your - vault backup, you lose your tokens permanently. Make sure to configure - regular backups. -

-
-
- -
- - - Your preference can later be changed at any time. - diff --git a/projects/chrome/src/app/components/welcome/welcome.component.scss b/projects/chrome/src/app/components/welcome/welcome.component.scss deleted file mode 100644 index 171f861..0000000 --- a/projects/chrome/src/app/components/welcome/welcome.component.scss +++ /dev/null @@ -1,46 +0,0 @@ -:host { - height: 100%; - display: flex; - flex-direction: column; - - padding-left: var(--size); - padding-right: var(--size); -} - -.storage-info { - margin-top: 1rem; - width: 100%; - - details { - background: rgba(255, 193, 7, 0.1); - border: 1px solid var(--warning, #ffc107); - border-radius: 6px; - padding: 0.5rem; - - summary { - cursor: pointer; - font-weight: 500; - font-size: 0.9rem; - color: var(--warning, #ffc107); - - &:hover { - text-decoration: underline; - } - } - - p { - margin: 0.75rem 0 0 0; - font-size: 0.85rem; - line-height: 1.4; - color: var(--text-muted, #6c757d); - - &:last-child { - margin-bottom: 0.5rem; - } - - strong { - color: var(--text, #212529); - } - } - } -} diff --git a/projects/chrome/src/app/components/welcome/welcome.component.spec.ts b/projects/chrome/src/app/components/welcome/welcome.component.spec.ts deleted file mode 100644 index 92182b5..0000000 --- a/projects/chrome/src/app/components/welcome/welcome.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { WelcomeComponent } from './welcome.component'; - -describe('WelcomeComponent', () => { - let component: WelcomeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WelcomeComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(WelcomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/chrome/src/app/components/welcome/welcome.component.ts b/projects/chrome/src/app/components/welcome/welcome.component.ts deleted file mode 100644 index 3cc4797..0000000 --- a/projects/chrome/src/app/components/welcome/welcome.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { BrowserSyncFlow, StorageService } from '@common'; - -@Component({ - selector: 'app-welcome', - imports: [], - templateUrl: './welcome.component.html', - styleUrl: './welcome.component.scss', -}) -export class WelcomeComponent { - readonly router = inject(Router); - readonly #storage = inject(StorageService); - - async onClickSync(enabled: boolean) { - const flow: BrowserSyncFlow = enabled - ? BrowserSyncFlow.BROWSER_SYNC - : BrowserSyncFlow.NO_SYNC; - - await this.#storage.enableBrowserSyncFlow(flow); - - // In case the user has selected the BROWSER_SYNC flow, - // we have to check if there is sync data available (e.g. from - // another browser instance). - // If so, navigate to /vault-login, otherwise to /vault-create/home. - if (flow === BrowserSyncFlow.BROWSER_SYNC) { - const browserSyncData = - await this.#storage.loadAndMigrateBrowserSyncData(); - - if ( - typeof browserSyncData !== 'undefined' && - Object.keys(browserSyncData).length > 0 - ) { - await this.router.navigateByUrl('/vault-login'); - return; - } - } - - await this.router.navigateByUrl('/vault-create/home'); - } -} diff --git a/projects/common/src/lib/services/startup/startup.service.ts b/projects/common/src/lib/services/startup/startup.service.ts index b0cedc3..4ba7236 100644 --- a/projects/common/src/lib/services/startup/startup.service.ts +++ b/projects/common/src/lib/services/startup/startup.service.ts @@ -5,6 +5,7 @@ import { StorageService, StorageServiceConfig, } from '../storage/storage.service'; +import { SyncFlow } from '../storage/types'; @Injectable({ providedIn: 'root', @@ -25,8 +26,9 @@ export class StartupService { // Step 1: Load the user settings const signerMetaData = await this.#storage.loadSignerMetaData(); if (typeof signerMetaData?.syncFlow === 'undefined') { - // Very first run. The user has not set up Plebeian Signer yet. - this.#router.navigateByUrl('/welcome'); + // Very first run - default to NO_SYNC (sync can be enabled later via export/import) + await this.#storage.enableBrowserSyncFlow(SyncFlow.NO_SYNC); + this.#router.navigateByUrl('/vault-create/home'); return; } this.#storage.enableBrowserSyncFlow(signerMetaData.syncFlow); diff --git a/projects/firefox/public/manifest.json b/projects/firefox/public/manifest.json index 41a55f2..b8fed53 100644 --- a/projects/firefox/public/manifest.json +++ b/projects/firefox/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Plebeian Signer", "description": "Nostr Identity Manager & Signer", - "version": "1.1.5", + "version": "1.1.6", "homepage_url": "https://github.com/PlebeianApp/plebeian-signer", "options_page": "options.html", "permissions": [ @@ -51,7 +51,13 @@ ], "browser_specific_settings": { "gecko": { - "id": "plebian-signer@mleku.dev" + "id": "plebian-signer@mleku.dev", + "data_collection_permissions": { + "required": [ + "none" + ], + "optional": [] + } } } } diff --git a/projects/firefox/src/app/app.routes.ts b/projects/firefox/src/app/app.routes.ts index 3859a3c..cb343c5 100644 --- a/projects/firefox/src/app/app.routes.ts +++ b/projects/firefox/src/app/app.routes.ts @@ -17,7 +17,6 @@ import { KeysComponent as EditIdentityKeysComponent } from './components/edit-id import { NcryptsecComponent as EditIdentityNcryptsecComponent } from './components/edit-identity/ncryptsec/ncryptsec.component'; import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component'; import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component'; -import { WelcomeComponent } from './components/welcome/welcome.component'; import { VaultLoginComponent } from './components/vault-login/vault-login.component'; import { VaultCreateComponent } from './components/vault-create/vault-create.component'; import { VaultImportComponent } from './components/vault-import/vault-import.component'; @@ -25,10 +24,6 @@ import { WhitelistedAppsComponent } from './components/whitelisted-apps/whitelis import { ProfileEditComponent } from './components/profile-edit/profile-edit.component'; export const routes: Routes = [ - { - path: 'welcome', - component: WelcomeComponent, - }, { path: 'vault-login', component: VaultLoginComponent, diff --git a/projects/firefox/src/app/components/vault-create/home/home.component.html b/projects/firefox/src/app/components/vault-create/home/home.component.html index 48bca67..ab51205 100644 --- a/projects/firefox/src/app/components/vault-create/home/home.component.html +++ b/projects/firefox/src/app/components/vault-create/home/home.component.html @@ -1,32 +1,120 @@ -
-
-
- Plebeian Signer +
+
+
+ +
+ Plebeian Signer +
-
- -
+ +
+

Restore or Create New Identity

+ Create a new nostr identity or paste in your current nsec. + + + +
+ + + +
+ +
- or -
-
\ No newline at end of file + + +
+

Import a Vault

+ + + +
+ + + @if (snapshots.length > 0) { +
+ + +
+ } +
+
+
diff --git a/projects/firefox/src/app/components/vault-create/home/home.component.scss b/projects/firefox/src/app/components/vault-create/home/home.component.scss index 225ac6b..a2a93c9 100644 --- a/projects/firefox/src/app/components/vault-create/home/home.component.scss +++ b/projects/firefox/src/app/components/vault-create/home/home.component.scss @@ -2,18 +2,26 @@ height: 100%; display: flex; flex-direction: column; + overflow-y: auto; - .vertically-centered { - height: 100%; + .container { display: flex; - justify-content: center; + flex-direction: column; + padding: var(--size); + gap: var(--size); + } + + .logo-section { + display: flex; + flex-direction: column; align-items: center; + gap: var(--size-half); + padding-bottom: var(--size-half); } .title { font-size: 20px; font-weight: 500; - margin-bottom: var(--size); } .logo-frame { @@ -21,8 +29,73 @@ border-radius: 100%; } + .section { + display: flex; + flex-direction: column; + gap: var(--size); + margin-top: var(--size); + } + + .section-heading { + font-size: 16px; + font-weight: 600; + margin: 0; + } + + .section-note { + font-size: 14px; + color: var(--muted-foreground); + } + + .button-row { + display: flex; + gap: var(--size); + justify-content: flex-end; + } + + .generate-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + + .continue-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + .file-input { position: absolute; visibility: hidden; } + + .file-btn { + display: flex; + align-items: center; + gap: var(--size-half); + } + + .import-controls { + display: flex; + flex-direction: column; + gap: var(--size); + } + + .import-row { + display: flex; + gap: var(--size-half); + + select { + flex: 1; + } + } + + .icon-btn { + width: 42px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + } } diff --git a/projects/firefox/src/app/components/vault-create/home/home.component.ts b/projects/firefox/src/app/components/vault-create/home/home.component.ts index efe03ce..c8bdccc 100644 --- a/projects/firefox/src/app/components/vault-create/home/home.component.ts +++ b/projects/firefox/src/app/components/vault-create/home/home.component.ts @@ -1,12 +1,161 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; +import { + NavComponent, + NostrHelper, + StorageService, + StartupService, + SignerMetaData_VaultSnapshot, + BrowserSyncData, +} from '@common'; +import { generateSecretKey } from 'nostr-tools'; +import { bytesToHex } from '@noble/hashes/utils'; +import { v4 as uuidv4 } from 'uuid'; +import browser from 'webextension-polyfill'; +import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage-service-config'; + +const VAULT_SNAPSHOTS_KEY = 'vaultSnapshots'; @Component({ selector: 'app-vault-create-home', - imports: [], + imports: [FormsModule], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) -export class HomeComponent { +export class HomeComponent extends NavComponent implements OnInit { readonly router = inject(Router); + readonly #storage = inject(StorageService); + readonly #startup = inject(StartupService); + + nickname = ''; + nsecInput = ''; + isNsecValid = false; + snapshots: SignerMetaData_VaultSnapshot[] = []; + selectedSnapshot: SignerMetaData_VaultSnapshot | undefined; + + ngOnInit(): void { + this.#loadSnapshots(); + } + + generateKey() { + const sk = generateSecretKey(); + const privkey = bytesToHex(sk); + this.nsecInput = NostrHelper.privkey2nsec(privkey); + this.validateNsec(); + } + + toggleVisibility(element: HTMLInputElement) { + element.type = element.type === 'password' ? 'text' : 'password'; + } + + async copyToClipboard() { + if (this.nsecInput) { + await navigator.clipboard.writeText(this.nsecInput); + } + } + + validateNsec() { + if (!this.nsecInput) { + this.isNsecValid = false; + return; + } + + try { + NostrHelper.getNostrPrivkeyObject(this.nsecInput.toLowerCase()); + this.isNsecValid = true; + } catch { + this.isNsecValid = false; + } + } + + onContinueWithNsec() { + if (!this.isNsecValid || !this.nickname) { + return; + } + // Navigate to password step, passing nsec and nickname in state + this.router.navigateByUrl('/vault-create/new', { + state: { nsec: this.nsecInput, nickname: this.nickname }, + }); + } + + async onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + const files = input.files; + if (!files || files.length === 0) { + return; + } + + try { + const file = files[0]; + const text = await file.text(); + const vault = JSON.parse(text) as BrowserSyncData; + + // Check if file already exists + if (this.snapshots.some((s) => s.fileName === file.name)) { + input.value = ''; + return; + } + + const newSnapshot: SignerMetaData_VaultSnapshot = { + id: uuidv4(), + fileName: file.name, + createdAt: new Date().toISOString(), + data: vault, + identityCount: vault.identities?.length ?? 0, + reason: 'manual', + }; + + this.snapshots = [...this.snapshots, newSnapshot].sort((a, b) => + b.fileName.localeCompare(a.fileName) + ); + this.selectedSnapshot = newSnapshot; + + await this.#saveSnapshots(); + } catch (error) { + console.error('Failed to load vault file:', error); + } + + // Reset input so same file can be selected again + input.value = ''; + } + + async onImport() { + if (!this.selectedSnapshot) { + return; + } + + try { + await this.#storage.deleteVault(true); + await this.#storage.importVault(this.selectedSnapshot.data); + + // Restart the app to properly reinitialize and route to vault-login + this.#storage.isInitialized = false; + this.#startup.startOver(getNewStorageServiceConfig()); + } catch (error) { + console.error('Failed to import vault:', error); + } + } + + async #loadSnapshots() { + const data = (await browser.storage.local.get(VAULT_SNAPSHOTS_KEY)) as { + vaultSnapshots?: SignerMetaData_VaultSnapshot[]; + }; + + this.snapshots = data.vaultSnapshots + ? [...data.vaultSnapshots].sort((a, b) => + b.fileName.localeCompare(a.fileName) + ) + : []; + + if (this.snapshots.length > 0) { + this.selectedSnapshot = this.snapshots[0]; + } + } + + async #saveSnapshots() { + await browser.storage.local.set({ + [VAULT_SNAPSHOTS_KEY]: this.snapshots, + }); + } } diff --git a/projects/firefox/src/app/components/vault-create/new/new.component.ts b/projects/firefox/src/app/components/vault-create/new/new.component.ts index 1cc497d..24f3cd7 100644 --- a/projects/firefox/src/app/components/vault-create/new/new.component.ts +++ b/projects/firefox/src/app/components/vault-create/new/new.component.ts @@ -1,7 +1,12 @@ import { Component, inject, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; -import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common'; +import { + LoggerService, + NavComponent, + StorageService, + DerivingModalComponent, +} from '@common'; @Component({ selector: 'app-new', @@ -18,6 +23,15 @@ export class NewComponent extends NavComponent { readonly #storage = inject(StorageService); readonly #logger = inject(LoggerService); + // Access router state via history.state (persists after navigation completes) + get #nsec(): string | undefined { + return history.state?.nsec; + } + + get #nickname(): string | undefined { + return history.state?.nickname; + } + toggleType(element: HTMLInputElement) { if (element.type === 'password') { element.type = 'text'; @@ -35,9 +49,22 @@ export class NewComponent extends NavComponent { this.derivingModal.show('Creating secure vault'); try { await this.#storage.createNewVault(this.password); - this.derivingModal.hide(); this.#logger.logVaultCreated(); - this.#router.navigateByUrl('/home/identities'); + + // If nsec and nickname were passed, add the identity + if (this.#nsec && this.#nickname) { + try { + await this.#storage.addIdentity({ + nick: this.#nickname, + privkeyString: this.#nsec, + }); + } catch (error) { + console.error('Failed to add identity:', error); + } + } + + this.derivingModal.hide(); + this.#router.navigateByUrl('/home/identity'); } catch (error) { this.derivingModal.hide(); console.error('Failed to create vault:', error); diff --git a/projects/firefox/src/app/components/welcome/welcome.component.html b/projects/firefox/src/app/components/welcome/welcome.component.html deleted file mode 100644 index 7d779e7..0000000 --- a/projects/firefox/src/app/components/welcome/welcome.component.html +++ /dev/null @@ -1,64 +0,0 @@ -
- Plebeian Signer Setup - Sync Preference -
- - - Plebeian Signer always encrypts sensitive data like private keys and site permissions - independent of the chosen sync mode. - - -Sync : Mozilla Firefox - - - Your encrypted data is synced between browser instances. You - need to be signed in with your account. - - - - -Offline - - - Your encrypted data is never uploaded to any servers. It remains in your local - browser instance. - - - - -
-
- Important for Cashu wallet users -

- Browser sync storage is limited to ~100KB shared across all data - (identities, permissions, relays, and Cashu tokens). -

-

- If you plan to use the Cashu ecash wallet with significant balances, - choose "Sync OFF" which provides ~5MB of local storage - (enough for ~18,000+ tokens vs ~300-400 with sync). -

-

- Note: Cashu tokens are bearer assets. If you lose your - vault backup, you lose your tokens permanently. Make sure to configure - regular backups. -

-
-
- -
- - - Your preference can later be changed at any time. - diff --git a/projects/firefox/src/app/components/welcome/welcome.component.scss b/projects/firefox/src/app/components/welcome/welcome.component.scss deleted file mode 100644 index 171f861..0000000 --- a/projects/firefox/src/app/components/welcome/welcome.component.scss +++ /dev/null @@ -1,46 +0,0 @@ -:host { - height: 100%; - display: flex; - flex-direction: column; - - padding-left: var(--size); - padding-right: var(--size); -} - -.storage-info { - margin-top: 1rem; - width: 100%; - - details { - background: rgba(255, 193, 7, 0.1); - border: 1px solid var(--warning, #ffc107); - border-radius: 6px; - padding: 0.5rem; - - summary { - cursor: pointer; - font-weight: 500; - font-size: 0.9rem; - color: var(--warning, #ffc107); - - &:hover { - text-decoration: underline; - } - } - - p { - margin: 0.75rem 0 0 0; - font-size: 0.85rem; - line-height: 1.4; - color: var(--text-muted, #6c757d); - - &:last-child { - margin-bottom: 0.5rem; - } - - strong { - color: var(--text, #212529); - } - } - } -} diff --git a/projects/firefox/src/app/components/welcome/welcome.component.spec.ts b/projects/firefox/src/app/components/welcome/welcome.component.spec.ts deleted file mode 100644 index 92182b5..0000000 --- a/projects/firefox/src/app/components/welcome/welcome.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { WelcomeComponent } from './welcome.component'; - -describe('WelcomeComponent', () => { - let component: WelcomeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WelcomeComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(WelcomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/firefox/src/app/components/welcome/welcome.component.ts b/projects/firefox/src/app/components/welcome/welcome.component.ts deleted file mode 100644 index 3cc4797..0000000 --- a/projects/firefox/src/app/components/welcome/welcome.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { BrowserSyncFlow, StorageService } from '@common'; - -@Component({ - selector: 'app-welcome', - imports: [], - templateUrl: './welcome.component.html', - styleUrl: './welcome.component.scss', -}) -export class WelcomeComponent { - readonly router = inject(Router); - readonly #storage = inject(StorageService); - - async onClickSync(enabled: boolean) { - const flow: BrowserSyncFlow = enabled - ? BrowserSyncFlow.BROWSER_SYNC - : BrowserSyncFlow.NO_SYNC; - - await this.#storage.enableBrowserSyncFlow(flow); - - // In case the user has selected the BROWSER_SYNC flow, - // we have to check if there is sync data available (e.g. from - // another browser instance). - // If so, navigate to /vault-login, otherwise to /vault-create/home. - if (flow === BrowserSyncFlow.BROWSER_SYNC) { - const browserSyncData = - await this.#storage.loadAndMigrateBrowserSyncData(); - - if ( - typeof browserSyncData !== 'undefined' && - Object.keys(browserSyncData).length > 0 - ) { - await this.router.navigateByUrl('/vault-login'); - return; - } - } - - await this.router.navigateByUrl('/vault-create/home'); - } -}