diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts index 947e089..69b55b8 100644 --- a/src/app/components/profile/profile.component.ts +++ b/src/app/components/profile/profile.component.ts @@ -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 { @@ -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 { + 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) => { diff --git a/src/app/components/zap/zap.component.ts b/src/app/components/zap/zap.component.ts index 1719d79..e2ab6e6 100644 --- a/src/app/components/zap/zap.component.ts +++ b/src/app/components/zap/zap.component.ts @@ -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 { 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 { + private async createAndSignZapRequest( + eventId: string, + msg?: string + ): Promise { 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 { - 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; } } - -