Improve initial setup screen:

📊 Add relay log visibility during startup
💼 Implement automatic wallet configuration
🧭 Add progress indicator showing current setup step
🧹 Fix minor linting issues throughout codebase
This commit is contained in:
Danny Morabito 2025-04-02 11:51:45 +02:00
parent 295137b313
commit 9fe777abd9
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
7 changed files with 217 additions and 72 deletions

View file

@ -14,7 +14,7 @@
"email": "developers@arx-ccn.com" "email": "developers@arx-ccn.com"
}, },
"scripts": { "scripts": {
"build": "tsc && electron-vite build", "build": "electron-vite build",
"build:mac": "npm run build && ./node_modules/.bin/electron-rebuild -p darwin -a x64 && electron-builder --mac", "build:mac": "npm run build && ./node_modules/.bin/electron-rebuild -p darwin -a x64 && electron-builder --mac",
"build:linux": "bun run build && electron-builder --linux", "build:linux": "bun run build && electron-builder --linux",
"start": "electron-vite preview", "start": "electron-vite preview",

View file

@ -211,6 +211,7 @@ export class StyledButton extends LitElement {
composed: true, composed: true,
}), }),
); );
return;
} }
private _handleKeyDown(e: KeyboardEvent) { private _handleKeyDown(e: KeyboardEvent) {

View file

@ -10,11 +10,13 @@ import * as nostrTools from '@nostr/tools/pure';
import { encodeBase64 } from '@std/encoding/base64'; import { encodeBase64 } from '@std/encoding/base64';
import { LitElement, css, html } from 'lit'; import { LitElement, css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import '@components/General/Button'; import '@components/General/Button';
import '@components/General/Fieldset'; import '@components/General/Fieldset';
import '@components/General/Input'; import '@components/General/Input';
import '@components/LoadingView'; import '@components/LoadingView';
import '@components/ProgressSteps';
@customElement('arx-initial-setup') @customElement('arx-initial-setup')
export class InitialSetup extends LitElement { export class InitialSetup extends LitElement {
@ -24,6 +26,13 @@ export class InitialSetup extends LitElement {
@state() private userName = ''; @state() private userName = '';
@state() private profileImage = ''; @state() private profileImage = '';
@state() private lightningAddress = ''; @state() private lightningAddress = '';
@state() private relayStatus: { running: boolean; pid: number | null; logs: string[] } = {
running: false,
pid: null,
logs: [],
};
private readonly pageLabels = ['Welcome', 'Seed Phrase', 'Relay Setup', 'Profile', 'Complete'];
get encryptionPassphrase() { get encryptionPassphrase() {
let encryptionPassphrase = localStorage.getItem('encryption_key'); let encryptionPassphrase = localStorage.getItem('encryption_key');
@ -85,6 +94,7 @@ export class InitialSetup extends LitElement {
oklch(from var(--color-accent) l calc(c * 1.2) h) oklch(from var(--color-accent) l calc(c * 1.2) h)
); );
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text;
color: transparent; color: transparent;
margin-bottom: calc(var(--spacing-unit) * 8); margin-bottom: calc(var(--spacing-unit) * 8);
} }
@ -139,7 +149,6 @@ export class InitialSetup extends LitElement {
} }
pre { pre {
white-space: normal;
background-color: var(--color-base-200); background-color: var(--color-base-200);
padding: calc(var(--spacing-unit) * 4); padding: calc(var(--spacing-unit) * 4);
border-radius: var(--radius-field); border-radius: var(--radius-field);
@ -220,6 +229,10 @@ export class InitialSetup extends LitElement {
private renderPageOne() { private renderPageOne() {
return html` return html`
<main class="welcome-container" ${animate()}> <main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section> <section>
<h1>Welcome to Eve</h1> <h1>Welcome to Eve</h1>
<h2>Your Private Community Network</h2> <h2>Your Private Community Network</h2>
@ -270,6 +283,10 @@ export class InitialSetup extends LitElement {
private renderPageTwo() { private renderPageTwo() {
return html` return html`
<main class="welcome-container" ${animate()}> <main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section> <section>
<h1>Getting Started</h1> <h1>Getting Started</h1>
<h2>Creating a Community</h2> <h2>Creating a Community</h2>
@ -338,11 +355,21 @@ export class InitialSetup extends LitElement {
private async startRelay() { private async startRelay() {
await window.relay.writeSeed(this.seedPhrase); await window.relay.writeSeed(this.seedPhrase);
await window.relay.start(this.encryptionPassphrase); await window.relay.start(this.encryptionPassphrase);
this.updateRelayStatus();
}
private async updateRelayStatus() {
this.relayStatus = await window.relay.getStatus();
if (this.relayStatus.running) setTimeout(() => this.updateRelayStatus(), 2000);
} }
private renderPageThree() { private renderPageThree() {
return html` return html`
<main class="welcome-container" ${animate()}> <main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section> <section>
<h2>Configure Eve Relay</h2> <h2>Configure Eve Relay</h2>
<p> <p>
@ -352,11 +379,25 @@ export class InitialSetup extends LitElement {
<p>Please press the button below to start the relay.</p> <p>Please press the button below to start the relay.</p>
<arx-button <arx-button
variant="primary" variant="primary"
label="Start Relay" label=${this.relayStatus.running ? 'Relay Running' : 'Start Relay'}
?disabled=${this.relayStatus.running}
@click=${() => this.startRelay()} @click=${() => this.startRelay()}
> >
Start Relay ${when(
this.relayStatus.running,
() => html`<iconify-icon slot="prefix" icon="mdi:check-circle"></iconify-icon>`,
)}
</arx-button> </arx-button>
${when(
this.relayStatus.running,
() => html`
<p class="note">
<iconify-icon icon="mdi:information"></iconify-icon>
Relay is running with PID: ${this.relayStatus.pid}
</p>
<pre class="relay-logs">${this.relayStatus.logs.slice(-5).join('\n')}</pre>
`,
)}
<p> <p>
Having trouble? Our team is here to help if you encounter any Having trouble? Our team is here to help if you encounter any
issues. issues.
@ -374,6 +415,7 @@ export class InitialSetup extends LitElement {
<arx-button <arx-button
@click=${() => this.handleNavigation(4)} @click=${() => this.handleNavigation(4)}
variant="primary" variant="primary"
?disabled=${!this.relayStatus.running}
label="Continue" label="Continue"
> >
</arx-button> </arx-button>
@ -390,13 +432,13 @@ export class InitialSetup extends LitElement {
this.profileImage = e.detail.value; this.profileImage = e.detail.value;
} }
private onLightningAddressInput(e: CustomEvent<{ value: string }>) {
this.lightningAddress = e.detail.value;
}
private renderPageFour() { private renderPageFour() {
return html` return html`
<main class="welcome-container" ${animate()}> <main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section> <section>
<h2>Complete Your Profile</h2> <h2>Complete Your Profile</h2>
<p>Great progress! Let's set up your community profile.</p> <p>Great progress! Let's set up your community profile.</p>
@ -437,9 +479,9 @@ export class InitialSetup extends LitElement {
> >
</arx-button> </arx-button>
<arx-button <arx-button
@click=${() => this.handleNavigation(5)} @click=${() => this.goToFinalStep()}
variant="primary" variant="primary"
label="Final Step" label="Next"
> >
</arx-button> </arx-button>
</div> </div>
@ -447,53 +489,12 @@ export class InitialSetup extends LitElement {
`; `;
} }
private renderPageFive() {
return html`
<main class="welcome-container" ${animate()}>
<section>
<h2>Payment Setup</h2>
<p>Almost done! Let's set up your payment options.</p>
<p>
Enter your existing Lightning address below for payments within the
community. If you don't have one, leave this field blank and we'll
automatically generate one for you through
<a target="_blank" href="https://npub.cash" class="external-link"
>npub.cash</a
>.
</p>
<arx-input
id="lightning-address"
type="text"
.value=${this.lightningAddress}
@change=${this.onLightningAddressInput}
placeholder="your@lightning.address"
/></arx-input>
<small class="note">
Your Lightning address enables secure, instant payments within the
community.
</small>
</section>
<div class="navigation">
<arx-button
@click=${() => this.handleNavigation(4)}
variant="secondary"
label="Back"
>
</arx-button>
<arx-button variant="primary" label="Next" @click=${() => this.goToFinalStep()}>
</arx-button>
</div>
</main>
`;
}
private async goToFinalStep() { private async goToFinalStep() {
const randomPrivateKey = nostrTools.generateSecretKey(); const randomPrivateKey = nostrTools.generateSecretKey();
const encryptedNsec = nip49.encrypt(randomPrivateKey, this.encryptionPassphrase); const encryptedNsec = nip49.encrypt(randomPrivateKey, this.encryptionPassphrase);
const npub = nip19.npubEncode(nostrTools.getPublicKey(randomPrivateKey)); const npub = nip19.npubEncode(nostrTools.getPublicKey(randomPrivateKey));
if (!this.lightningAddress) this.lightningAddress = `${npub}@npub.cash`; this.lightningAddress = `${npub}@npub.cash`;
localStorage.setItem('ncryptsec', encryptedNsec); localStorage.setItem('ncryptsec', encryptedNsec);
@ -510,12 +511,16 @@ export class InitialSetup extends LitElement {
await event.sign(); await event.sign();
await event.publish(); await event.publish();
this.handleNavigation(6); this.handleNavigation(5);
} }
private renderPageSix() { private renderPageFive() {
return html` return html`
<main class="welcome-container" ${animate()}> <main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section> <section>
<h2>Done!</h2> <h2>Done!</h2>
<p> <p>
@ -528,19 +533,13 @@ export class InitialSetup extends LitElement {
required to invite new members to the community. required to invite new members to the community.
</p> </p>
<p>Your username is: <b>${this.userName}</b></p> <p>Your username is: <b>${this.userName}</b></p>
${ ${when(this.profileImage, () => html`<p>Your profile image is: <img .src=${this.profileImage} /></p>`)}
this.profileImage
? html` <p>
Your profile image is: <img .src=${this.profileImage} />
</p>`
: ''
}
<p>Your lightning address is: <b>${this.lightningAddress}</b></p> <p>Your lightning address is: <b>${this.lightningAddress}</b></p>
</section> </section>
<div class="navigation"> <div class="navigation">
<arx-button <arx-button
@click=${() => this.handleNavigation(5)} @click=${() => this.handleNavigation(4)}
variant="secondary" variant="secondary"
label="Back" label="Back"
> >
@ -581,8 +580,6 @@ export class InitialSetup extends LitElement {
return this.renderPageFour(); return this.renderPageFour();
case 5: case 5:
return this.renderPageFive(); return this.renderPageFive();
case 6:
return this.renderPageSix();
default: default:
return html`<div class="welcome-container"> return html`<div class="welcome-container">
<arx-loading-view></arx-loading-view> <arx-loading-view></arx-loading-view>

View file

@ -0,0 +1,114 @@
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('arx-progress-steps')
export class ProgressSteps extends LitElement {
@property({ type: Number }) currentPage = 1;
@property({ type: Array }) pageLabels: string[] = [];
static override styles = css`
:host {
display: block;
width: 100%;
margin-bottom: calc(var(--spacing-unit) * 8);
}
.progress-container {
display: flex;
flex-direction: column;
gap: calc(var(--spacing-unit) * 2);
}
.progress-bar {
width: 100%;
height: 4px;
background: var(--color-base-200);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-accent);
transition: width 0.3s ease;
}
.page-indicators {
display: flex;
justify-content: space-between;
padding: 0 calc(var(--spacing-unit) * 2);
}
.page-indicator {
display: flex;
flex-direction: column;
align-items: center;
gap: calc(var(--spacing-unit) * 1);
}
.page-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-base-300);
transition: all 0.3s ease;
}
.page-dot.active {
background: var(--color-accent);
transform: scale(1.2);
}
.page-dot.completed {
background: var(--color-accent);
}
.page-label {
font-size: 0.875rem;
color: var(--color-secondary);
transition: color 0.3s ease;
}
.page-label.active {
color: var(--color-accent);
font-weight: 500;
}
.page-label.completed {
color: var(--color-accent);
}
`;
override render() {
const progress = ((this.currentPage - 1) / (this.pageLabels.length - 1)) * 100;
return html`
<div class="progress-container">
<div class="progress-bar">
<div
class="progress-fill"
style="width: ${progress}%"
></div>
</div>
<div class="page-indicators">
${this.pageLabels.map((label, index) => {
const pageNum = index + 1;
const isActive = pageNum === this.currentPage;
const isCompleted = pageNum < this.currentPage;
return html`
<div class="page-indicator">
<div
class="page-dot ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
></div>
<span
class="page-label ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
>${label}</span>
</div>
`;
})}
</div>
</div>
`;
}
}

View file

@ -15,6 +15,7 @@ export class RelayManager {
private relayLogs: string[]; private relayLogs: string[];
private readonly maxLogs: number; private readonly maxLogs: number;
private encryptionKey: string | null; private encryptionKey: string | null;
private devRunning: boolean;
/** /**
* Checks if the relay is currently running. * Checks if the relay is currently running.
@ -22,7 +23,9 @@ export class RelayManager {
* @returns {boolean} True if the relay is running, otherwise false. * @returns {boolean} True if the relay is running, otherwise false.
*/ */
get isRunning(): boolean { get isRunning(): boolean {
return !!this.process; if (!this.process) return false;
if (is.dev) return this.devRunning;
return true;
} }
/** /**
@ -46,6 +49,7 @@ export class RelayManager {
this.relayLogs = []; this.relayLogs = [];
this.maxLogs = maxLogs; this.maxLogs = maxLogs;
this.encryptionKey = null; this.encryptionKey = null;
this.devRunning = false;
} }
private detectEnvironment(): PackageEnvironment { private detectEnvironment(): PackageEnvironment {
@ -123,10 +127,39 @@ export class RelayManager {
this.encryptionKey = encryptionKey; this.encryptionKey = encryptionKey;
if (is.dev) { if (is.dev) {
this.relayLogs = [];
this.process = spawn('nc', ['localhost', '6942']); this.process = spawn('nc', ['localhost', '6942']);
this.process.on('exit', () => { this.devRunning = true;
if (this.process.stdout) {
this.process.stdout.on('data', (data: Buffer) => {
const logLine = data.toString().trim();
this.addLog(`[DEV] ${logLine}`);
});
}
if (this.process.stderr) {
this.process.stderr.on('data', (data: Buffer) => {
const logLine = data.toString().trim();
this.addLog(`[DEV] ERROR: ${logLine}`);
});
}
this.process.on('error', (err: Error) => {
this.addLog(`[DEV] Failed to start netcat: ${err.message}`);
this.process = null; this.process = null;
this.start(encryptionKey); this.devRunning = false;
this.restartProcess();
});
this.process.on('exit', (code: number | null) => {
this.addLog(`[DEV] Netcat exited with code ${code}`);
this.process = null;
this.devRunning = false;
if (code !== 1) this.start(encryptionKey);
else this.addLog('[DEV] Connection failed - target port may not be open - relay is not running');
}); });
return; return;
} }

View file

@ -124,7 +124,7 @@ export class EveSettings extends LitElement {
</arx-fieldset> </arx-fieldset>
<arx-fieldset legend="Profile"> <arx-fieldset legend="Profile">
${when( ${when(
this.profile.picture, this.profile!.picture,
() => html` () => html`
<div class="profile-header"> <div class="profile-header">
<img <img
@ -143,7 +143,7 @@ export class EveSettings extends LitElement {
label="Name" label="Name"
type="text" type="text"
name="name" name="name"
.value=${this.profile.name} .value=${this.profile!.name}
@change=${(e: ArxInputChangeEvent) => this.handleInputChange('name', e)} @change=${(e: ArxInputChangeEvent) => this.handleInputChange('name', e)}
placeholder="Your display name" placeholder="Your display name"
></arx-input> ></arx-input>
@ -152,7 +152,7 @@ export class EveSettings extends LitElement {
label="Profile Image URL" label="Profile Image URL"
type="text" type="text"
name="image" name="image"
.value=${this.profile.picture} .value=${this.profile!.picture}
@change=${(e: ArxInputChangeEvent) => this.handleInputChange('picture', e)} @change=${(e: ArxInputChangeEvent) => this.handleInputChange('picture', e)}
placeholder="https://example.com/your-image.jpg" placeholder="https://example.com/your-image.jpg"
></arx-input> ></arx-input>
@ -161,7 +161,7 @@ export class EveSettings extends LitElement {
label="Banner URL" label="Banner URL"
type="text" type="text"
name="banner" name="banner"
.value=${this.profile.banner} .value=${this.profile!.banner}
@change=${(e: ArxInputChangeEvent) => this.handleInputChange('banner', e)} @change=${(e: ArxInputChangeEvent) => this.handleInputChange('banner', e)}
placeholder="https://example.com/your-image.jpg" placeholder="https://example.com/your-image.jpg"
></arx-input> ></arx-input>

View file

@ -32,5 +32,5 @@ export default function formatDateTime(date: Date | string | number): string {
minute: formattingOptions.minute === '' ? undefined : formattingOptions.minute, minute: formattingOptions.minute === '' ? undefined : formattingOptions.minute,
second: formattingOptions.second === '' ? undefined : formattingOptions.second, second: formattingOptions.second === '' ? undefined : formattingOptions.second,
hour12: formattingOptions.hour12, hour12: formattingOptions.hour12,
}); } as unknown as Intl.DateTimeFormatOptions);
} }