'handleNostrLoggedIn', 'nostrLoggedOut' => 'handleNostrLoggedOut', ]; public function mount(): void { if (!NostrAuth::check()) { return; } $this->currentPubkey = NostrAuth::pubkey(); $this->currentPleb = EinundzwanzigPleb::query() ->with([ 'paymentEvents' => fn($query) => $query->where('year', date('Y')), 'profile', ]) ->where('pubkey', $this->currentPubkey)->first(); if (!$this->currentPleb) { return; } $this->profileForm->setPleb($this->currentPleb); $this->form->setPleb($this->currentPleb); if ($this->currentPleb->nip05_handle) { $this->nip05VerifiedHandles = $this->getNip05HandlesForPubkey($this->currentPubkey); if (count($this->nip05VerifiedHandles) > 0) { $this->nip05Verified = true; $this->nip05VerifiedHandle = $this->nip05VerifiedHandles[0]; if (!in_array($this->profileForm->nip05Handle, $this->nip05VerifiedHandles, true)) { $this->nip05HandleMismatch = true; } } } $this->no = $this->currentPleb->no_email; $this->showEmail = !$this->no; $this->amountToPay = config('app.env') === 'production' ? 21000 : 1; $this->resolveCurrentPaymentEvent(); $this->loadEvents(); $this->listenForPayment(); } public function updatedNo(): void { $this->showEmail = !$this->no; $this->currentPleb->update([ 'no_email' => $this->no, ]); } public function updatingFax(mixed $value): void { if (! is_string($value)) { $this->skipRender(); abort(422); } } public function updatedFax(): void { $this->js('alert("Markus Turm wird sich per Fax melden!")'); } public function updatedProfileFormNip05Handle(): void { $this->profileForm->nip05Handle = strtolower($this->profileForm->nip05Handle); } public function handleNostrLoggedIn($signedEvent = null): void { $pubkey = NostrAuth::loginWithSignedEvent($signedEvent); $this->currentPubkey = $pubkey; $this->currentPleb = EinundzwanzigPleb::query() ->with([ 'paymentEvents' => fn($query) => $query->where('year', date('Y')), 'profile', ]) ->where('pubkey', $pubkey)->first(); if (!$this->currentPleb) { return; } $this->profileForm->setPleb($this->currentPleb); $this->form->setPleb($this->currentPleb); $this->no = $this->currentPleb->no_email; $this->showEmail = !$this->no; if ($this->currentPleb->nip05_handle) { $this->nip05VerifiedHandles = $this->getNip05HandlesForPubkey($this->currentPubkey); if (count($this->nip05VerifiedHandles) > 0) { $this->nip05Verified = true; $this->nip05VerifiedHandle = $this->nip05VerifiedHandles[0]; if (!in_array($this->profileForm->nip05Handle, $this->nip05VerifiedHandles, true)) { $this->nip05HandleMismatch = true; } } } $this->amountToPay = config('app.env') === 'production' ? 21000 : 1; $this->resolveCurrentPaymentEvent(); $this->loadEvents(); $this->listenForPayment(); } public function handleNostrLoggedOut(): void { $this->currentPubkey = null; $this->currentPleb = null; } public function saveEmail(): void { $this->profileForm->saveEmail(); } public function saveNip05Handle(): void { $this->profileForm->saveNip05Handle(); // Refresh NIP-05 verification status after saving if ($this->currentPleb->nip05_handle) { $this->nip05VerifiedHandles = $this->getNip05HandlesForPubkey($this->currentPubkey); if (count($this->nip05VerifiedHandles) > 0) { $this->nip05Verified = true; $this->nip05VerifiedHandle = $this->nip05VerifiedHandles[0]; if (!in_array($this->profileForm->nip05Handle, $this->nip05VerifiedHandles, true)) { $this->nip05HandleMismatch = true; } else { $this->nip05HandleMismatch = false; } } } } public function pay($comment): mixed { if (!$this->currentPleb) { return redirect()->route('association.profile'); } $paymentEvent = $this->resolveCurrentPaymentEvent(); $this->resetInvoiceMeta(); $paymentEvent = $this->syncPaymentEventStatus($paymentEvent); if ($paymentEvent->btc_pay_invoice) { return redirect()->away('https://pay.einundzwanzig.space/i/'.$paymentEvent->btc_pay_invoice); } try { $response = \Illuminate\Support\Facades\Http::withHeaders([ 'Authorization' => 'token '.config('services.btc_pay.api_key'), ])->post( 'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices', [ 'amount' => $this->amountToPay, 'metadata' => [ 'orderId' => $comment, 'orderUrl' => url()->route('association.profile'), 'itemDesc' => 'Mitgliedsbeitrag '.date('Y').' von nostr:'.$this->currentPleb->npub, 'posData' => [ 'event' => $paymentEvent->event_id, 'pubkey' => $this->currentPleb->pubkey, 'npub' => $this->currentPleb->npub, ], ], 'checkout' => [ 'expirationMinutes' => 60 * 24, 'redirectURL' => url()->route('association.profile'), 'redirectAutomatically' => true, 'defaultLanguage' => 'de', ], ], )->throw(); $invoice = $response->json(); $paymentEvent->btc_pay_invoice = $invoice['id']; $paymentEvent->save(); $this->applyInvoiceMeta($invoice); $this->invoiceStatusVariant = 'info'; $this->invoiceStatusMessage = 'Rechnung erstellt. Bitte bezahle sie vor Ablauf.'; return redirect()->away($invoice['checkoutLink']); } catch (\Throwable $e) { Flux::toast( 'Fehler beim Erstellen der Rechnung. Bitte versuche es später erneut: '.$e->getMessage(), variant: 'danger', ); return redirect()->route('association.profile'); } } public function listenForPayment(): void { if (!$this->currentPleb) { return; } $paymentEvent = $this->resolveCurrentPaymentEvent(); $this->resetInvoiceMeta(); $paymentEvent = $this->syncPaymentEventStatus($paymentEvent); $this->currentYearIsPaid = (bool) $paymentEvent->paid; $this->payments = $this->currentPleb ->paymentEvents() ->where('paid', true) ->get(); } protected function resolveCurrentPaymentEvent(): PaymentEvent { $paymentEvents = $this->currentPleb ->paymentEvents() ->where('year', date('Y')) ->orderByDesc('id') ->get(); if ($paymentEvents->count() > 1) { $this->pruneDuplicatePaymentEvents($paymentEvents); $paymentEvents = $this->currentPleb ->paymentEvents() ->where('year', date('Y')) ->orderByDesc('id') ->get(); } if ($paymentEvents->isEmpty()) { $paymentEvent = $this->createPaymentEvent(); } else { $paymentEvent = $paymentEvents->first(); } $this->currentPleb->setRelation( 'paymentEvents', $this->currentPleb ->paymentEvents() ->where('year', date('Y')) ->orderBy('id') ->get(), ); return $paymentEvent; } protected function pruneDuplicatePaymentEvents(Collection $paymentEvents): void { $eventToKeep = $paymentEvents ->sortByDesc(fn (PaymentEvent $event) => [ (int) $event->paid, $event->updated_at?->timestamp ?? 0, ]) ->first(); $idsToDelete = $paymentEvents ->where('id', '!=', $eventToKeep?->id) ->pluck('id'); if ($idsToDelete->isNotEmpty()) { PaymentEvent::query() ->whereIn('id', $idsToDelete) ->delete(); } } protected function syncPaymentEventStatus(PaymentEvent $paymentEvent): PaymentEvent { if (!$paymentEvent->btc_pay_invoice) { $this->invoiceStatusVariant = 'info'; $this->invoiceStatusMessage = 'Noch keine Rechnung gestartet. Klicke auf „Pay“, um eine neue Invoice zu erstellen.'; $this->invoiceStatus = null; $this->invoiceStatusLabel = 'Bereit für neue Rechnung'; $this->invoiceExpiresAt = null; $this->invoiceExpiresAtDisplay = null; $this->invoiceExpiresIn = null; $this->currentYearIsPaid = (bool) $paymentEvent->paid; return $paymentEvent; } try { $invoice = $this->fetchInvoice($paymentEvent->btc_pay_invoice); $this->applyInvoiceMeta($invoice); $status = $invoice['status'] ?? null; $this->invoiceStatus = $status; $this->invoiceStatusLabel = $this->statusLabel($status); if ($this->invoiceIsExpired($status)) { $paymentEvent->delete(); $this->currentYearIsPaid = false; $paymentEvent = $this->createPaymentEvent(); $this->loadEvents(); $this->invoiceStatusVariant = 'warning'; $this->invoiceStatusMessage = 'Die Rechnung ist abgelaufen und wurde entfernt. Starte eine neue Zahlung.'; } elseif ($status === 'Settled') { $paymentEvent->update(['paid' => true]); $this->currentYearIsPaid = true; $this->invoiceStatusVariant = 'success'; $this->invoiceStatusMessage = 'Zahlung bestätigt. Danke!'; } elseif ($status === 'Processing') { $this->currentYearIsPaid = $paymentEvent->paid; $this->invoiceStatusVariant = 'info'; $this->invoiceStatusMessage = 'Zahlung eingegangen, wartet auf Bestätigung.'; } else { $this->currentYearIsPaid = $paymentEvent->paid; $this->invoiceStatusVariant = 'info'; $this->invoiceStatusMessage = $this->statusMessage($status); } } catch (\Throwable $e) { $this->resetInvoiceMeta(); $this->invoiceStatusVariant = 'danger'; $this->invoiceStatusLabel = 'Status unbekannt'; $this->invoiceStatusMessage = 'Die Rechnung konnte nicht überprüft werden. Bitte versuche es später erneut.'; $this->currentYearIsPaid = (bool) $paymentEvent->paid; } $this->currentPleb->load([ 'paymentEvents' => fn($query) => $query->where('year', date('Y')), ]); return $paymentEvent->refresh(); } protected function fetchInvoice(string $invoiceId): array { return \Illuminate\Support\Facades\Http::withHeaders([ 'Authorization' => 'token '.config('services.btc_pay.api_key'), ]) ->get( 'https://pay.einundzwanzig.space/api/v1/stores/98PF86BoMd3C8P1nHHyFdoeznCwtcm5yehcAgoCYDQ2a/invoices/'.$invoiceId, )->throw()->json(); } protected function applyInvoiceMeta(array $invoice): void { $this->invoiceStatus = $invoice['status'] ?? null; $this->invoiceStatusLabel = $this->statusLabel($this->invoiceStatus); $this->invoiceExpiresAt = $invoice['expirationTime'] ?? null; [$this->invoiceExpiresAtDisplay, $this->invoiceExpiresIn] = $this->formatExpiration($this->invoiceExpiresAt); } protected function formatExpiration(?string $timestamp): array { if (!$timestamp) { return [null, null]; } $expiresAt = is_numeric($timestamp) ? Carbon::createFromTimestamp((int) $timestamp, config('app.timezone', 'UTC')) : Carbon::parse($timestamp)->setTimezone(config('app.timezone', 'UTC')); return [ $expiresAt->format('d.m.Y H:i'), $expiresAt->diffForHumans(null, true, true, 2), ]; } protected function invoiceIsExpired(?string $status): bool { return in_array($status, ['Expired', 'Invalid'], true); } protected function statusLabel(?string $status): ?string { return match ($status) { 'New' => 'Offene Rechnung', 'Processing' => 'Zahlung in Bestätigung', 'Settled' => 'Bezahlt', 'Expired' => 'Abgelaufen', 'Invalid' => 'Ungültig', default => null, }; } protected function statusMessage(?string $status): string { return match ($status) { 'Processing' => 'Zahlung eingegangen, warte auf Bestätigung.', 'Settled' => 'Zahlung bestätigt.', 'Expired', 'Invalid' => 'Rechnung abgelaufen oder ungültig.', default => 'Rechnung ist aktiv. Bitte vor Ablauf bezahlen.', }; } protected function resetInvoiceMeta(): void { $this->invoiceStatus = null; $this->invoiceStatusLabel = null; $this->invoiceStatusMessage = null; $this->invoiceStatusVariant = 'info'; $this->invoiceExpiresAt = null; $this->invoiceExpiresAtDisplay = null; $this->invoiceExpiresIn = null; } public function save($type): void { try { $this->form->apply($type); Flux::toast('Mitgliedschaft erfolgreich beantragt!', variant: 'success'); } catch (\Illuminate\Validation\ValidationException $e) { if (!$this->form->check) { $this->js('alert("Du musst den Statuten zustimmen.")'); } throw $e; } } public function createPaymentEvent(): PaymentEvent { $existing = $this->currentPleb ->paymentEvents() ->where('year', date('Y')) ->first(); if ($existing) { return $existing; } if (app()->environment('testing')) { try { return $this->currentPleb->paymentEvents()->create([ 'year' => date('Y'), 'event_id' => 'test_event_'.Str::uuid(), 'amount' => $this->amountToPay, ]); } catch (UniqueConstraintViolationException) { return $this->currentPleb->paymentEvents()->where('year', date('Y'))->firstOrFail(); } } $note = new NostrEvent; $note->setKind(32121); $note->setContent( 'Dieses Event dient der Zahlung des Mitgliedsbeitrags für das Jahr '.date( 'Y', ).'. Bitte bezahle den Betrag von '.number_format($this->amountToPay, 0, ',', '.').' Satoshis.', ); $note->setTags([ ['d', $this->currentPleb->pubkey.','.date('Y')], ['zap', 'daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6', config('services.relay'), '1'], ]); $signer = new Sign; $signer->signEvent($note, config('services.nostr')); $eventMessage = new EventMessage($note); $relayUrl = config('services.relay'); $relay = new Relay($relayUrl); $relay->setMessage($eventMessage); $result = $relay->send(); try { return $this->currentPleb->paymentEvents()->create([ 'year' => date('Y'), 'event_id' => $result->eventId, 'amount' => $this->amountToPay, ]); } catch (UniqueConstraintViolationException) { return $this->currentPleb->paymentEvents()->where('year', date('Y'))->firstOrFail(); } } public function loadEvents(): void { $relayUrl = config('services.relay'); if (! $relayUrl) { $this->events = []; return; } $subscription = new Subscription; $subscriptionId = $subscription->setId(); $filter1 = new Filter; $filter1->setKinds([32121]); $filter1->setAuthors(['daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6']); $filters = [$filter1]; $requestMessage = new RequestMessage($subscriptionId, $filters); $relays = [ new Relay($relayUrl), ]; $relaySet = new RelaySet; $relaySet->setRelays($relays); $request = new Request($relaySet, $requestMessage); $response = $request->send(); $this->events = collect($response[$relayUrl] ?? []) ->map(function ($event) { if (!isset($event->event)) { return false; } return [ 'id' => $event->event->id, 'kind' => $event->event->kind, 'content' => $event->event->content, 'pubkey' => $event->event->pubkey, 'tags' => $event->event->tags, 'created_at' => $event->event->created_at, ]; }) ->filter() ->unique('id') ->toArray(); } public function copyRelayUrl(): void { $relayUrl = 'wss://nostr.einundzwanzig.space'; $this->js("navigator.clipboard.writeText('{$relayUrl}')"); Flux::toast('Relay-Adresse in die Zwischenablage kopiert!'); } public function copyWatchtowerUrl(): void { $watchtowerUrl = '03a09f56bba3d2c200cc55eda2f1f069564a97c1fb74345e1560e2868a8ab3d7d0@62.171.139.240:9911'; $this->js("navigator.clipboard.writeText('{$watchtowerUrl}')"); Flux::toast('Watchtower-Adresse in die Zwischenablage kopiert!'); } } ?>
Premium Outbox-Relay von Einundzwanzig.
Menschenlesbarer, verifizierter Nostr-Name.
Schutz für deine Lightning Channel.
Eigener Speicher für Bilder & Videos auf Nostr.
@if($currentPleb->profile?->name) {{ $currentPleb->profile->name }} @endif
{{ $currentPleb->pubkey }}
{{ $currentPleb->npub }}
{{ $currentPleb->nip05_handle }}
Perfekt für mobile Android Geräte. Eine App, in der man alle Keys/nsecs verwalten kann.
Browser-Erweiterung in die man seinen Key/nsec eingeben kann. Pro Alby-Konto ein nsec.
Browser-Erweiterung für Chrome Browser. Multi-Key fähig.
Browser-Erweiterung für Firefox Browser. Multi-Key fähig.
Profil in der Datenbank vorhanden.
Nur Personen können Mitglied werden und zahlen 21.000 Satoshis im Jahr. Firmen melden sich bitte direkt an den Vorstand.
Falls du möchtest, kannst du hier eine E-Mail Adresse hinterlegen, damit der Verein dich darüber informieren kann, wenn es Neuigkeiten gibt.
Am besten eine anonymisierte E-Mail Adresse verwenden. Wir sichern diese Adresse AES-256 verschlüsselt in der Datenbank ab.
@if($currentYearIsPaid) Du bist derzeit ein Mitglied des Vereins. Das aktuelle Jahr ist bezahlt. @else Du wirst nach Zahlung des Vereinsbeitrages zum Mitglied. Das aktuelle Jahr ist noch nicht bezahlt. @endif
Nostr Event für die Zahlung des Mitgliedsbeitrags: {{ $currentPleb->paymentEvents->last()->event_id }}
{{ $invoiceStatusLabel ?? 'Rechnungsstatus' }} @if($invoiceStatus) ({{ $invoiceStatus }}) @endif
{{ $invoiceStatusMessage }}
@if($invoiceExpiresIn && in_array($invoiceStatus, ['New', 'Processing'], true))Gültig noch {{ $invoiceExpiresIn }} @if($invoiceExpiresAtDisplay) (bis {{ $invoiceExpiresAtDisplay }}) @endif
@endif{{ $latestEvent['content'] }}
Unser Nostr-Relay konnte derzeit nicht erreicht werden, um eine Zahlung zu initialisieren. Bitte versuche es später noch einmal.
|
Satoshis
|
Jahr
|
Event-ID
|
Quittung
|
|---|---|---|---|
|
{{ $payment->amount }}
|
{{ $payment->year }}
|
{{ $payment->event_id }}
|
@if($payment->btc_pay_invoice)
|