Release v1.2.0 - Streamlined vault creation flow
- 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 <noreply@anthropic.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "plebeian-signer",
|
"name": "plebeian-signer",
|
||||||
"version": "v1.0.8",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "plebeian-signer",
|
"name": "plebeian-signer",
|
||||||
"version": "v1.0.8",
|
"version": "1.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.0",
|
"@angular/animations": "^19.0.0",
|
||||||
"@angular/common": "^19.0.0",
|
"@angular/common": "^19.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "plebeian-signer",
|
"name": "plebeian-signer",
|
||||||
"version": "1.1.6",
|
"version": "1.2.0",
|
||||||
"custom": {
|
"custom": {
|
||||||
"chrome": {
|
"chrome": {
|
||||||
"version": "v1.1.6"
|
"version": "v1.1.6"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
"name": "Plebeian Signer - Nostr Identity Manager & Signer",
|
||||||
"description": "Manage and switch between multiple identities while interacting with Nostr apps",
|
"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",
|
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { VaultLoginComponent } from './components/vault-login/vault-login.compon
|
|||||||
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
||||||
import { HomeComponent as VaultCreateHomeComponent } from './components/vault-create/home/home.component';
|
import { HomeComponent as VaultCreateHomeComponent } from './components/vault-create/home/home.component';
|
||||||
import { NewComponent as VaultCreateNewComponent } from './components/vault-create/new/new.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 { IdentitiesComponent } from './components/home/identities/identities.component';
|
||||||
import { IdentityComponent } from './components/home/identity/identity.component';
|
import { IdentityComponent } from './components/home/identity/identity.component';
|
||||||
import { InfoComponent } from './components/home/info/info.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';
|
import { ProfileEditComponent } from './components/profile-edit/profile-edit.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
|
||||||
path: 'welcome',
|
|
||||||
component: WelcomeComponent,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'vault-login',
|
path: 'vault-login',
|
||||||
component: VaultLoginComponent,
|
component: VaultLoginComponent,
|
||||||
|
|||||||
@@ -1,32 +1,120 @@
|
|||||||
<div class="vertically-centered">
|
<div class="container">
|
||||||
<div class="sam-flex-column center">
|
<div class="logo-section">
|
||||||
<div class="sam-flex-column gap" style="align-items: center">
|
<div class="logo-frame">
|
||||||
<span class="title">Plebeian Signer</span>
|
<img src="logo.svg" height="80" width="80" alt="" />
|
||||||
|
</div>
|
||||||
|
<span class="title">Plebeian Signer</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="logo-frame">
|
<!-- New Identity Section -->
|
||||||
<img src="logo.svg" height="120" width="120" alt="" />
|
<div class="section">
|
||||||
</div>
|
<h2 class="section-heading">Restore or Create New Identity</h2>
|
||||||
|
|
||||||
|
<span class="section-note">Create a new nostr identity or paste in your current nsec.</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="nickname"
|
||||||
|
[(ngModel)]="nickname"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
#nsecInputElement
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="nsec or hex private key"
|
||||||
|
[(ngModel)]="nsecInput"
|
||||||
|
(ngModelChange)="validateNsec()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="toggleVisibility(nsecInputElement)"
|
||||||
|
title="toggle visibility"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bi"
|
||||||
|
[class.bi-eye]="nsecInputElement.type === 'password'"
|
||||||
|
[class.bi-eye-slash]="nsecInputElement.type === 'text'"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="copyToClipboard()"
|
||||||
|
title="copy to clipboard"
|
||||||
|
>
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="sam-mt-2 btn btn-primary"
|
class="btn btn-outline-secondary generate-btn"
|
||||||
(click)="router.navigateByUrl('/vault-create/new')"
|
(click)="generateKey()"
|
||||||
|
title="generate new key"
|
||||||
>
|
>
|
||||||
<div class="sam-flex-row gap-h">
|
<span>generate</span>
|
||||||
<i class="bi bi-plus-circle" style="height: 22px"></i>
|
<span>✨</span>
|
||||||
<span>Create a new vault</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="sam-text-muted">or</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-primary continue-btn"
|
||||||
(click)="router.navigateByUrl('/vault-import')"
|
[disabled]="!isNsecValid || !nickname"
|
||||||
|
(click)="onContinueWithNsec()"
|
||||||
>
|
>
|
||||||
<span>Import a vault</span>
|
<span>Continue</span>
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Section -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-heading">Import a Vault</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#fileInput
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept=".json"
|
||||||
|
(change)="onFileSelected($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="import-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary file-btn"
|
||||||
|
(click)="fileInput.click()"
|
||||||
|
>
|
||||||
|
<i class="bi bi-folder2-open"></i>
|
||||||
|
<span>Add vault file</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (snapshots.length > 0) {
|
||||||
|
<div class="import-row">
|
||||||
|
<select class="form-select" [(ngModel)]="selectedSnapshot">
|
||||||
|
@for (snapshot of snapshots; track snapshot.id) {
|
||||||
|
<option [ngValue]="snapshot">
|
||||||
|
{{ snapshot.fileName }} ({{ snapshot.identityCount }} identities)
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary icon-btn"
|
||||||
|
[disabled]="!selectedSnapshot"
|
||||||
|
(click)="onImport()"
|
||||||
|
title="import vault"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,18 +2,26 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.vertically-centered {
|
.container {
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
padding: var(--size);
|
||||||
|
gap: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--size-half);
|
||||||
|
padding-bottom: var(--size-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: var(--size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-frame {
|
.logo-frame {
|
||||||
@@ -21,8 +29,73 @@
|
|||||||
border-radius: 100%;
|
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 {
|
.file-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
imports: [],
|
imports: [FormsModule],
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrl: './home.component.scss',
|
styleUrl: './home.component.scss',
|
||||||
})
|
})
|
||||||
export class HomeComponent extends NavComponent {
|
export class HomeComponent extends NavComponent implements OnInit {
|
||||||
readonly router = inject(Router);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Component, inject, ViewChild } from '@angular/core';
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common';
|
import {
|
||||||
|
LoggerService,
|
||||||
|
NavComponent,
|
||||||
|
StorageService,
|
||||||
|
DerivingModalComponent,
|
||||||
|
} from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-new',
|
selector: 'app-new',
|
||||||
@@ -18,6 +23,15 @@ export class NewComponent extends NavComponent {
|
|||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #logger = inject(LoggerService);
|
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) {
|
toggleType(element: HTMLInputElement) {
|
||||||
if (element.type === 'password') {
|
if (element.type === 'password') {
|
||||||
element.type = 'text';
|
element.type = 'text';
|
||||||
@@ -35,9 +49,22 @@ export class NewComponent extends NavComponent {
|
|||||||
this.derivingModal.show('Creating secure vault');
|
this.derivingModal.show('Creating secure vault');
|
||||||
try {
|
try {
|
||||||
await this.#storage.createNewVault(this.password);
|
await this.#storage.createNewVault(this.password);
|
||||||
this.derivingModal.hide();
|
|
||||||
this.#logger.logVaultCreated();
|
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) {
|
} catch (error) {
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
console.error('Failed to create vault:', error);
|
console.error('Failed to create vault:', error);
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
<div class="sam-text-header sam-mb-h">
|
|
||||||
<span>Plebeian Signer Setup - Sync Preference</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
|
||||||
Plebeian Signer always encrypts sensitive data like private keys and site permissions
|
|
||||||
independent of the chosen sync mode.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="sam-mt sam-text-lg">Sync : Google Chrome</span>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
|
||||||
Your encrypted data is synced between browser instances. You need to be signed
|
|
||||||
in with your account.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sam-mt btn btn-primary"
|
|
||||||
(click)="onClickSync(true)"
|
|
||||||
>
|
|
||||||
<span> Sync ON</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="sam-mt sam-text-lg">Offline</span>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md">
|
|
||||||
Your encrypted data is never uploaded to any servers. It remains in your local
|
|
||||||
browser instance.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sam-mt sam-mb-2 btn btn-secondary"
|
|
||||||
(click)="onClickSync(false)"
|
|
||||||
>
|
|
||||||
<span> Sync OFF</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="storage-info">
|
|
||||||
<details>
|
|
||||||
<summary>Important for Cashu wallet users</summary>
|
|
||||||
<p>
|
|
||||||
Browser sync storage is limited to ~100KB shared across all data
|
|
||||||
(identities, permissions, relays, and Cashu tokens).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you plan to use the Cashu ecash wallet with significant balances,
|
|
||||||
choose <strong>"Sync OFF"</strong> which provides ~5MB of local storage
|
|
||||||
(enough for ~18,000+ tokens vs ~300-400 with sync).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Note:</strong> Cashu tokens are bearer assets. If you lose your
|
|
||||||
vault backup, you lose your tokens permanently. Make sure to configure
|
|
||||||
regular backups.
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sam-flex-grow"></div>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-mb">
|
|
||||||
Your preference can later be changed at any time.
|
|
||||||
</span>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { WelcomeComponent } from './welcome.component';
|
|
||||||
|
|
||||||
describe('WelcomeComponent', () => {
|
|
||||||
let component: WelcomeComponent;
|
|
||||||
let fixture: ComponentFixture<WelcomeComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [WelcomeComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(WelcomeComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
StorageService,
|
StorageService,
|
||||||
StorageServiceConfig,
|
StorageServiceConfig,
|
||||||
} from '../storage/storage.service';
|
} from '../storage/storage.service';
|
||||||
|
import { SyncFlow } from '../storage/types';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -25,8 +26,9 @@ export class StartupService {
|
|||||||
// Step 1: Load the user settings
|
// Step 1: Load the user settings
|
||||||
const signerMetaData = await this.#storage.loadSignerMetaData();
|
const signerMetaData = await this.#storage.loadSignerMetaData();
|
||||||
if (typeof signerMetaData?.syncFlow === 'undefined') {
|
if (typeof signerMetaData?.syncFlow === 'undefined') {
|
||||||
// Very first run. The user has not set up Plebeian Signer yet.
|
// Very first run - default to NO_SYNC (sync can be enabled later via export/import)
|
||||||
this.#router.navigateByUrl('/welcome');
|
await this.#storage.enableBrowserSyncFlow(SyncFlow.NO_SYNC);
|
||||||
|
this.#router.navigateByUrl('/vault-create/home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#storage.enableBrowserSyncFlow(signerMetaData.syncFlow);
|
this.#storage.enableBrowserSyncFlow(signerMetaData.syncFlow);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Plebeian Signer",
|
"name": "Plebeian Signer",
|
||||||
"description": "Nostr Identity Manager & Signer",
|
"description": "Nostr Identity Manager & Signer",
|
||||||
"version": "1.1.5",
|
"version": "1.1.6",
|
||||||
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
"homepage_url": "https://github.com/PlebeianApp/plebeian-signer",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -51,7 +51,13 @@
|
|||||||
],
|
],
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "plebian-signer@mleku.dev"
|
"id": "plebian-signer@mleku.dev",
|
||||||
|
"data_collection_permissions": {
|
||||||
|
"required": [
|
||||||
|
"none"
|
||||||
|
],
|
||||||
|
"optional": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { KeysComponent as EditIdentityKeysComponent } from './components/edit-id
|
|||||||
import { NcryptsecComponent as EditIdentityNcryptsecComponent } from './components/edit-identity/ncryptsec/ncryptsec.component';
|
import { NcryptsecComponent as EditIdentityNcryptsecComponent } from './components/edit-identity/ncryptsec/ncryptsec.component';
|
||||||
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
|
||||||
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.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 { VaultLoginComponent } from './components/vault-login/vault-login.component';
|
||||||
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
import { VaultCreateComponent } from './components/vault-create/vault-create.component';
|
||||||
import { VaultImportComponent } from './components/vault-import/vault-import.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';
|
import { ProfileEditComponent } from './components/profile-edit/profile-edit.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
|
||||||
path: 'welcome',
|
|
||||||
component: WelcomeComponent,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'vault-login',
|
path: 'vault-login',
|
||||||
component: VaultLoginComponent,
|
component: VaultLoginComponent,
|
||||||
|
|||||||
@@ -1,32 +1,120 @@
|
|||||||
<div class="vertically-centered">
|
<div class="container">
|
||||||
<div class="sam-flex-column center">
|
<div class="logo-section">
|
||||||
<div class="sam-flex-column gap" style="align-items: center">
|
<div class="logo-frame">
|
||||||
<span class="title">Plebeian Signer</span>
|
<img src="logo.svg" height="80" width="80" alt="" />
|
||||||
|
</div>
|
||||||
|
<span class="title">Plebeian Signer</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="logo-frame">
|
<!-- New Identity Section -->
|
||||||
<img src="logo.svg" height="120" width="120" alt="" />
|
<div class="section">
|
||||||
</div>
|
<h2 class="section-heading">Restore or Create New Identity</h2>
|
||||||
|
|
||||||
|
<span class="section-note">Create a new nostr identity or paste in your current nsec.</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="nickname"
|
||||||
|
[(ngModel)]="nickname"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
#nsecInputElement
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="nsec or hex private key"
|
||||||
|
[(ngModel)]="nsecInput"
|
||||||
|
(ngModelChange)="validateNsec()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="toggleVisibility(nsecInputElement)"
|
||||||
|
title="toggle visibility"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="bi"
|
||||||
|
[class.bi-eye]="nsecInputElement.type === 'password'"
|
||||||
|
[class.bi-eye-slash]="nsecInputElement.type === 'text'"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="copyToClipboard()"
|
||||||
|
title="copy to clipboard"
|
||||||
|
>
|
||||||
|
<i class="bi bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="sam-mt-2 btn btn-primary"
|
class="btn btn-outline-secondary generate-btn"
|
||||||
(click)="router.navigateByUrl('/vault-create/new')"
|
(click)="generateKey()"
|
||||||
|
title="generate new key"
|
||||||
>
|
>
|
||||||
<div class="sam-flex-row gap-h">
|
<span>generate</span>
|
||||||
<i class="bi bi-plus-circle" style="height: 22px"></i>
|
<span>✨</span>
|
||||||
<span>Create a new vault</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="sam-text-muted">or</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-primary continue-btn"
|
||||||
(click)="router.navigateByUrl('/vault-import')"
|
[disabled]="!isNsecValid || !nickname"
|
||||||
|
(click)="onContinueWithNsec()"
|
||||||
>
|
>
|
||||||
<span>Import a vault</span>
|
<span>Continue</span>
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Import Section -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-heading">Import a Vault</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
#fileInput
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept=".json"
|
||||||
|
(change)="onFileSelected($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="import-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary file-btn"
|
||||||
|
(click)="fileInput.click()"
|
||||||
|
>
|
||||||
|
<i class="bi bi-folder2-open"></i>
|
||||||
|
<span>Add vault file</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (snapshots.length > 0) {
|
||||||
|
<div class="import-row">
|
||||||
|
<select class="form-select" [(ngModel)]="selectedSnapshot">
|
||||||
|
@for (snapshot of snapshots; track snapshot.id) {
|
||||||
|
<option [ngValue]="snapshot">
|
||||||
|
{{ snapshot.fileName }} ({{ snapshot.identityCount }} identities)
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary icon-btn"
|
||||||
|
[disabled]="!selectedSnapshot"
|
||||||
|
(click)="onImport()"
|
||||||
|
title="import vault"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,18 +2,26 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
.vertically-centered {
|
.container {
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
padding: var(--size);
|
||||||
|
gap: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--size-half);
|
||||||
|
padding-bottom: var(--size-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: var(--size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-frame {
|
.logo-frame {
|
||||||
@@ -21,8 +29,73 @@
|
|||||||
border-radius: 100%;
|
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 {
|
.file-input {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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({
|
@Component({
|
||||||
selector: 'app-vault-create-home',
|
selector: 'app-vault-create-home',
|
||||||
imports: [],
|
imports: [FormsModule],
|
||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
styleUrl: './home.component.scss',
|
styleUrl: './home.component.scss',
|
||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent extends NavComponent implements OnInit {
|
||||||
readonly router = inject(Router);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Component, inject, ViewChild } from '@angular/core';
|
import { Component, inject, ViewChild } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LoggerService, NavComponent, StorageService, DerivingModalComponent } from '@common';
|
import {
|
||||||
|
LoggerService,
|
||||||
|
NavComponent,
|
||||||
|
StorageService,
|
||||||
|
DerivingModalComponent,
|
||||||
|
} from '@common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-new',
|
selector: 'app-new',
|
||||||
@@ -18,6 +23,15 @@ export class NewComponent extends NavComponent {
|
|||||||
readonly #storage = inject(StorageService);
|
readonly #storage = inject(StorageService);
|
||||||
readonly #logger = inject(LoggerService);
|
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) {
|
toggleType(element: HTMLInputElement) {
|
||||||
if (element.type === 'password') {
|
if (element.type === 'password') {
|
||||||
element.type = 'text';
|
element.type = 'text';
|
||||||
@@ -35,9 +49,22 @@ export class NewComponent extends NavComponent {
|
|||||||
this.derivingModal.show('Creating secure vault');
|
this.derivingModal.show('Creating secure vault');
|
||||||
try {
|
try {
|
||||||
await this.#storage.createNewVault(this.password);
|
await this.#storage.createNewVault(this.password);
|
||||||
this.derivingModal.hide();
|
|
||||||
this.#logger.logVaultCreated();
|
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) {
|
} catch (error) {
|
||||||
this.derivingModal.hide();
|
this.derivingModal.hide();
|
||||||
console.error('Failed to create vault:', error);
|
console.error('Failed to create vault:', error);
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
<div class="sam-text-header sam-mb-h">
|
|
||||||
<span>Plebeian Signer Setup - Sync Preference</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
|
||||||
Plebeian Signer always encrypts sensitive data like private keys and site permissions
|
|
||||||
independent of the chosen sync mode.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="sam-mt sam-text-lg">Sync : Mozilla Firefox</span>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-text-align-center2">
|
|
||||||
Your encrypted data is synced between browser instances. You
|
|
||||||
need to be signed in with your account.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sam-mt btn btn-primary"
|
|
||||||
(click)="onClickSync(true)"
|
|
||||||
>
|
|
||||||
<span> Sync ON</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<span class="sam-mt sam-text-lg">Offline</span>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md">
|
|
||||||
Your encrypted data is never uploaded to any servers. It remains in your local
|
|
||||||
browser instance.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sam-mt sam-mb-2 btn btn-secondary"
|
|
||||||
(click)="onClickSync(false)"
|
|
||||||
>
|
|
||||||
<span> Sync OFF</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="storage-info">
|
|
||||||
<details>
|
|
||||||
<summary>Important for Cashu wallet users</summary>
|
|
||||||
<p>
|
|
||||||
Browser sync storage is limited to ~100KB shared across all data
|
|
||||||
(identities, permissions, relays, and Cashu tokens).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you plan to use the Cashu ecash wallet with significant balances,
|
|
||||||
choose <strong>"Sync OFF"</strong> which provides ~5MB of local storage
|
|
||||||
(enough for ~18,000+ tokens vs ~300-400 with sync).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Note:</strong> Cashu tokens are bearer assets. If you lose your
|
|
||||||
vault backup, you lose your tokens permanently. Make sure to configure
|
|
||||||
regular backups.
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sam-flex-grow"></div>
|
|
||||||
|
|
||||||
<span class="sam-text-muted sam-text-md sam-mb">
|
|
||||||
Your preference can later be changed at any time.
|
|
||||||
</span>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { WelcomeComponent } from './welcome.component';
|
|
||||||
|
|
||||||
describe('WelcomeComponent', () => {
|
|
||||||
let component: WelcomeComponent;
|
|
||||||
let fixture: ComponentFixture<WelcomeComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [WelcomeComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(WelcomeComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user