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",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plebeian-signer",
|
||||
"version": "1.1.6",
|
||||
"version": "1.2.0",
|
||||
"custom": {
|
||||
"chrome": {
|
||||
"version": "v1.1.6"
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,32 +1,120 @@
|
||||
<div class="vertically-centered">
|
||||
<div class="sam-flex-column center">
|
||||
<div class="sam-flex-column gap" style="align-items: center">
|
||||
<span class="title">Plebeian Signer</span>
|
||||
<div class="container">
|
||||
<div class="logo-section">
|
||||
<div class="logo-frame">
|
||||
<img src="logo.svg" height="80" width="80" alt="" />
|
||||
</div>
|
||||
<span class="title">Plebeian Signer</span>
|
||||
</div>
|
||||
|
||||
<div class="logo-frame">
|
||||
<img src="logo.svg" height="120" width="120" alt="" />
|
||||
</div>
|
||||
<!-- New Identity Section -->
|
||||
<div class="section">
|
||||
<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
|
||||
type="button"
|
||||
class="sam-mt-2 btn btn-primary"
|
||||
(click)="router.navigateByUrl('/vault-create/new')"
|
||||
class="btn btn-outline-secondary generate-btn"
|
||||
(click)="generateKey()"
|
||||
title="generate new key"
|
||||
>
|
||||
<div class="sam-flex-row gap-h">
|
||||
<i class="bi bi-plus-circle" style="height: 22px"></i>
|
||||
<span>Create a new vault</span>
|
||||
</div>
|
||||
<span>generate</span>
|
||||
<span>✨</span>
|
||||
</button>
|
||||
|
||||
<span class="sam-text-muted">or</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
(click)="router.navigateByUrl('/vault-import')"
|
||||
class="btn btn-primary continue-btn"
|
||||
[disabled]="!isNsecValid || !nickname"
|
||||
(click)="onContinueWithNsec()"
|
||||
>
|
||||
<span>Import a vault</span>
|
||||
<span>Continue</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
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);
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,32 +1,120 @@
|
||||
<div class="vertically-centered">
|
||||
<div class="sam-flex-column center">
|
||||
<div class="sam-flex-column gap" style="align-items: center">
|
||||
<span class="title">Plebeian Signer</span>
|
||||
<div class="container">
|
||||
<div class="logo-section">
|
||||
<div class="logo-frame">
|
||||
<img src="logo.svg" height="80" width="80" alt="" />
|
||||
</div>
|
||||
<span class="title">Plebeian Signer</span>
|
||||
</div>
|
||||
|
||||
<div class="logo-frame">
|
||||
<img src="logo.svg" height="120" width="120" alt="" />
|
||||
</div>
|
||||
<!-- New Identity Section -->
|
||||
<div class="section">
|
||||
<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
|
||||
type="button"
|
||||
class="sam-mt-2 btn btn-primary"
|
||||
(click)="router.navigateByUrl('/vault-create/new')"
|
||||
class="btn btn-outline-secondary generate-btn"
|
||||
(click)="generateKey()"
|
||||
title="generate new key"
|
||||
>
|
||||
<div class="sam-flex-row gap-h">
|
||||
<i class="bi bi-plus-circle" style="height: 22px"></i>
|
||||
<span>Create a new vault</span>
|
||||
</div>
|
||||
<span>generate</span>
|
||||
<span>✨</span>
|
||||
</button>
|
||||
|
||||
<span class="sam-text-muted">or</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
(click)="router.navigateByUrl('/vault-import')"
|
||||
class="btn btn-primary continue-btn"
|
||||
[disabled]="!isNsecValid || !nickname"
|
||||
(click)="onContinueWithNsec()"
|
||||
>
|
||||
<span>Import a vault</span>
|
||||
<span>Continue</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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