Refactor ZapComponent and ProfileComponent: update snackbar duration, modify canUseZap method, and adjust ZapComponent's form validation, error handling, and LNURLPayRequest processing.

This commit is contained in:
Milad Raeisi
2024-11-13 17:14:34 +04:00
parent 8af3f27c56
commit 261a3b5a09
2 changed files with 133 additions and 96 deletions

View File

@@ -438,7 +438,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
}
openSnackBar(message: string, action: string = 'dismiss'): void {
this._snackBar.open(message, action, { duration: 1300 });
this._snackBar.open(message, action, { duration: 3000 });
}
async canUseZap(): Promise<boolean> {
@@ -446,14 +446,14 @@ export class ProfileComponent implements OnInit, OnDestroy {
if (canReceiveZap) {
return true;
} else {
this.openSnackBar("User can't receive zaps");
this.openSnackBar("Using Zap is not possible. Please complete your profile to include lud06 or lud16.");
return false;
}
}
openZapDialog(eventId: string = ""): void {
if (this.canUseZap()) {
async openZapDialog(eventId: string = ""): Promise<void> {
const canZap = await this.canUseZap();
if (canZap) {
const zapData: ZapDialogData = {
lud16: this.profileUser.lud16,
lud06: this.profileUser.lud06,
@@ -470,8 +470,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
}
}
toggleLike() {
this.isLiked = !this.isLiked;
@@ -518,6 +516,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
this._eventService
.sendTextEvent(this.eventInput.nativeElement.value)
.then(() => {
this.eventInput.nativeElement.value = "";
this._changeDetectorRef.markForCheck();
})
.catch((error) => {

View File

@@ -1,19 +1,26 @@
import { Component, OnInit, inject } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { TextFieldModule } from '@angular/cdk/text-field';
import { LNURLPayRequest, LNURLInvoice } from 'app/services/interfaces';
import { SignerService } from 'app/services/signer.service';
import { RelayService } from 'app/services/relay.service';
import { finalizeEvent, NostrEvent, UnsignedEvent } from 'nostr-tools';
import { AngorCardComponent } from "../../../@angor/components/card/card.component";
import { Utilities } from 'app/services/utilities';
import { CommonModule } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { hexToBytes } from '@noble/hashes/utils';
import { LNURLInvoice, LNURLPayRequest } from 'app/services/interfaces';
import { RelayService } from 'app/services/relay.service';
import { SignerService } from 'app/services/signer.service';
import { Utilities } from 'app/services/utilities';
import { finalizeEvent, NostrEvent, UnsignedEvent } from 'nostr-tools';
import { AngorCardComponent } from '../../../@angor/components/card/card.component';
@Component({
selector: 'app-zap',
@@ -29,22 +36,20 @@ import { hexToBytes } from '@noble/hashes/utils';
MatSelectModule,
TextFieldModule,
ReactiveFormsModule,
AngorCardComponent
AngorCardComponent,
],
})
export class ZapComponent implements OnInit {
private readonly formBuilder = inject(FormBuilder);
private readonly signerService = inject(SignerService);
private readonly relayService = inject(RelayService);
private readonly utilities = inject(Utilities);
constructor(private util: Utilities,
) {
}
sendZapForm!: FormGroup;
payRequest: LNURLPayRequest | null = null;
invoice: LNURLInvoice = { pr: '' };
invoice: LNURLInvoice = {
pr: '',
};
canZap = false;
loading = false;
error = '';
@@ -55,50 +60,79 @@ export class ZapComponent implements OnInit {
private initializeForm(): void {
this.sendZapForm = this.formBuilder.group({
lightningAddress: ['', [Validators.required, this.validateLightningAddress]],
lightningAddress: [
'',
[Validators.required, this.validateLightningAddress],
],
eventId: [''], // Optional for zapping specific events
amount: ['', [Validators.required, Validators.min(1)]],
comment: [''],
});
}
private validateLightningAddress(control: AbstractControl): ValidationErrors | null {
private validateLightningAddress(
control: AbstractControl
): ValidationErrors | null {
const value = control.value;
return value.includes('@') ? null : { invalidFormat: true };
return value.includes('@')
? null
: {
invalidFormat: true,
};
}
private getCallbackUrl(lightningAddress: string): string | null {
if (lightningAddress.includes('@')) {
const [username, domain] = lightningAddress.split('@');
return `https://${domain}/.well-known/lnurlp/${username}`;
} else if (lightningAddress.toLowerCase().startsWith('lnurl')) {
return this.util.convertBech32ToText(lightningAddress).toString();
try {
if (lightningAddress.includes('@')) {
const [username, domain] = lightningAddress.split('@');
return `https://${domain}/.well-known/lnurlp/${username}`;
} else if (lightningAddress.toLowerCase().startsWith('lnurl')) {
return this.utilities
.convertBech32ToText(lightningAddress)
.toString();
}
return null;
} catch (error) {
console.error('Error generating callback URL:', error);
return null;
}
return null;
}
async fetchPayRequest(): Promise<void> {
this.resetState();
const lightningAddress = this.sendZapForm.get('lightningAddress')?.value;
const lightningAddress =
this.sendZapForm.get('lightningAddress')?.value;
if (!lightningAddress) {
this.setError('Lightning Address is required.');
return;
}
const callbackUrl = this.getCallbackUrl(lightningAddress);
const callbackUrl = this.getCallbackUrl(lightningAddress);
if (!callbackUrl) {
this.setError('Invalid Lightning Address.');
return;
}
try {
const response = await fetch(callbackUrl);
if (!response.ok) throw new Error('Failed to fetch pay request.');
if (!response.ok) {
throw new Error(
`Failed to fetch pay request: ${response.statusText}`
);
}
const result = await response.json();
if (result.status === 'ERROR') {
this.setError(result.reason || 'Error fetching the pay request.');
return;
throw new Error(
result.reason || 'Error fetching the pay request.'
);
}
this.payRequest = result as LNURLPayRequest;
this.canZap = true;
this.configureAmountValidators();
} catch (error) {
} catch (error: any) {
this.setError(error.message || 'Error connecting to the server.');
} finally {
this.loading = false;
@@ -127,97 +161,103 @@ export class ZapComponent implements OnInit {
}
this.resetState();
const { lightningAddress, eventId, amount, comment } = this.sendZapForm.value;
const { lightningAddress, eventId, amount, comment } =
this.sendZapForm.value;
if (!this.payRequest) {
this.setError('Pay request is not loaded.');
return;
}
const callback = new URL(this.payRequest.callback);
const query = new URLSearchParams({
amount: (amount * 1000).toString(),
});
if (comment && this.payRequest.commentAllowed) {
query.set('comment', comment);
}
if (eventId) {
const zapRequest = await this.createAndSignZapRequest(eventId, comment);
query.set('nostr', JSON.stringify(zapRequest));
}
try {
const response = await fetch(`${callback.origin}${callback.pathname}?${query.toString()}`);
if (!response.ok) throw new Error('Failed to fetch invoice.');
const callback = new URL(this.payRequest.callback);
const query = new URLSearchParams({
amount: (amount * 1000).toString(),
});
if (comment && this.payRequest.commentAllowed) {
query.set('comment', comment);
}
if (eventId) {
const zapRequest = await this.createAndSignZapRequest(
eventId,
comment
);
query.set('nostr', JSON.stringify(zapRequest));
}
const response = await fetch(
`${callback.origin}${callback.pathname}?${query.toString()}`
);
if (!response.ok) {
throw new Error(
`Failed to fetch invoice: ${response.statusText}`
);
}
const result = await response.json();
if (result.status === 'ERROR') {
this.setError(result.reason || 'Error fetching the invoice.');
return;
throw new Error(result.reason || 'Error fetching the invoice.');
}
this.invoice = result;
} catch (error) {
this.setError(error.message || 'Error fetching the invoice.');
} catch (error: any) {
this.setError(error.message || 'Error processing the zap request.');
} finally {
this.loading = false;
}
}
private async createAndSignZapRequest(eventId: string, msg?: string): Promise<NostrEvent> {
private async createAndSignZapRequest(
eventId: string,
msg?: string
): Promise<NostrEvent> {
try {
const unsignedZapRequest = this.createZapRequestData(eventId, msg);
let signedEvent: NostrEvent;
if (this.signerService.isUsingSecretKey()) {
const privateKey = await this.signerService.getDecryptedSecretKey();
if (!privateKey) throw new Error('Private key could not be retrieved.');
const signedEvent = this.signerService.isUsingSecretKey()
? finalizeEvent(
unsignedZapRequest,
hexToBytes(
await this.signerService.getDecryptedSecretKey()
)
)
: await this.signerService.signEventWithExtension(
unsignedZapRequest
);
const privateKeyBytes = hexToBytes(privateKey);
signedEvent = finalizeEvent(unsignedZapRequest, privateKeyBytes);
} else {
signedEvent = await this.signerService.signEventWithExtension(unsignedZapRequest);
if (!signedEvent) {
throw new Error('Signing failed. Signed event is null.');
}
if (!signedEvent) throw new Error('Signing failed. Signed event is null.');
return signedEvent;
} catch (error) {
} catch (error: any) {
console.error('Error creating and signing zap request:', error);
throw new Error('Failed to create and sign zap request.');
}
}
private createZapRequestData(eventId: string, msg?: string): UnsignedEvent {
const tags = [
['e', eventId],
['p', this.payRequest?.nostrPubkey || ''],
['relays', ...this.relayService.getConnectedRelays()],
];
return {
kind: 9734,
content: msg || '',
tags: tags,
tags: [
['e', eventId],
['p', this.payRequest?.nostrPubkey || ''],
['relays', ...this.relayService.getConnectedRelays()],
],
pubkey: this.signerService.getPublicKey(),
created_at: Math.floor(Date.now() / 1000),
};
}
async sendZapToRelay(signedEvent: NostrEvent): Promise<void> {
try {
await this.relayService.publishEventToWriteRelays(signedEvent);
console.log('Zap event sent successfully');
} catch (error) {
this.setError('Failed to send zap event to relays');
console.error('Error sending zap event:', error);
}
}
private resetState(): void {
this.error = '';
this.loading = true;
this.invoice = { pr: '' };
this.invoice = {
pr: '',
};
}
private setError(message: string): void {
@@ -225,5 +265,3 @@ export class ZapComponent implements OnInit {
this.loading = false;
}
}