feat: add QR code generator and Nostr event handling

- Added the `simplesoftwareio/simple-qrcode` package to the project
- Created a new JavaScript file `nostrZap.js` to handle Nostr events
- Added the `nostrZap` function to the Alpine.js data property in `app.js`
- Updated the `services.php` configuration file to include the `nostr` environment variable
- Created a new database migration to add a `payment_event` field to the `einundzwanzig_plebs` table
- Made adjustments in the `Election:year.blade.php` view file to handle potential null values
- Updated `composer.lock` and `package.json` with the new dependencies
This commit is contained in:
fsociety
2024-09-30 14:46:30 +02:00
parent ee86f2ec37
commit e0a34d9376
19 changed files with 599 additions and 35 deletions

View File

@@ -28,7 +28,7 @@
}
</script>
<div x-data="nostrLogin"
class="flex h-[100dvh] overflow-hidden">
class="flex h-[100dvh] overflow-hidden">
<livewire:layout.sidebar/>
<div
class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
@@ -102,7 +102,9 @@
<ul>
<li>
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
target="_blank" href="https://gitworkshop.dev/r/naddr1qvzqqqrhnypzqzklvar4enzu53t06vpzu3h465nwkzhk9p9ls4y5crwhs3lnu5pnqy88wumn8ghj7mn0wvhxcmmv9uqpxetfde6kuer6wasku7nfvukkummnw3eqdgsn8w/issues" @click="open = false" @focus="open = true"
target="_blank"
href="https://gitworkshop.dev/r/naddr1qvzqqqrhnypzqzklvar4enzu53t06vpzu3h465nwkzhk9p9ls4y5crwhs3lnu5pnqy88wumn8ghj7mn0wvhxcmmv9uqpxetfde6kuer6wasku7nfvukkummnw3eqdgsn8w/issues"
@click="open = false" @focus="open = true"
@focusout="open = false">
<i class="fa-sharp-duotone fa-solid fa-code w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
<span>Issues/Feedback</span>
@@ -118,7 +120,8 @@
</li>
<li>
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
href="https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr" target="_blank" @click="open = false" @focus="open = true"
href="https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr"
target="_blank" @click="open = false" @focus="open = true"
@focusout="open = false">
<i class="fa-brands fa-github w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
<span>Github</span>
@@ -126,7 +129,8 @@
</li>
<li>
<a class="font-medium text-sm text-amber-500 hover:text-amber-600 dark:hover:text-amber-400 flex items-center py-1 px-3"
href="https://einundzwanzig.space/kontakt/" target="_blank" @click="open = false" @focus="open = true"
href="https://einundzwanzig.space/kontakt/" target="_blank"
@click="open = false" @focus="open = true"
@focusout="open = false">
<i class="fa-sharp-duotone fa-solid fa-info w-3 h-3 fill-current text-amber-500 shrink-0 mr-2"></i>
<span>Impressum</span>

View File

@@ -507,13 +507,13 @@ $signEvent = function ($event) {
<div>{{ $event['kind'] }}</div>
</td>
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
<div>{{ $event['profile']['name'] }}</div>
<div>{{ $event['profile']['name'] ?? '' }}</div>
</td>
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
<div>{{ $event['created_at'] }}</div>
</td>
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
<div>{{ $event['votedFor']['name'] ?? 'error' }}</div>
<div>{{ $event['votedFor']['name'] ?? '' }}</div>
</td>
<td class="px-2 first:pl-5 last:pr-5 py-3 whitespace-nowrap">
<div>{{ $event['type'] }}</div>

View File

@@ -2,28 +2,125 @@
use Livewire\Volt\Component;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Key\Key;
use swentel\nostr\Message\EventMessage;
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\Relay\RelaySet;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;
use swentel\nostr\Event\Event as NostrEvent;
use swentel\nostr\Sign\Sign;
use function Livewire\Volt\computed;
use function Livewire\Volt\mount;
use function Livewire\Volt\state;
use function Livewire\Volt\with;
use function Laravel\Folio\{middleware};
use function Laravel\Folio\name;
use function Livewire\Volt\{on, form};
use function Livewire\Volt\{on, form, updated};
name('association.profile');
state(['yearsPaid' => []]);
state(['events' => []]);
state(['payments' => []]);
state(['invoice' => null]);
state(['qrCode' => null]);
state(['paid' => false]);
state(['amountToPay' => 1]);
state(['currentYearIsPaid' => false]);
state(['currentPubkey' => null]);
state(['currentPleb' => null]);
form(\App\Livewire\Forms\ApplicationForm::class);
updated([
'invoice' => function () {
$this->qrCode = base64_encode(
QrCode::format('png')
->size(300)
->merge('/public/android-chrome-192x192.png', .3)
->errorCorrection('H')
->generate($this->invoice),
);
},
]);
on([
'nostrLoggedIn' => function ($pubkey) {
$this->currentPubkey = $pubkey;
$this->currentPleb = \App\Models\EinundzwanzigPleb::query()->where('pubkey', $pubkey)->first();
if ($this->currentPleb->association_status === \App\Enums\AssociationStatus::ACTIVE) {
$this->amountToPay = 21;
}
if (!$this->currentPleb->payment_event) {
$this->createPaymentEvent();
}
$this->loadEvents();
$this->searchPaymentEvent();
},
]);
$listenForPayment = function () {
if (!$this->paid) {
$this->searchPaymentEvent();
}
};
$searchPaymentEvent = function () {
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setKinds([9735]);
$filters = [$filter1];
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relays = [
new Relay(config('services.relay')),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);
$request = new Request($relaySet, $requestMessage);
$response = $request->send();
if (count($response[config('services.relay')]) > 0) {
$this->payments = collect($response[config('services.relay')])->map(fn($event)
=> [
'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,
])->toArray();
$this->yearsPaid = collect($this->payments)->map(fn($payment)
=> [
'year' => $payment['content'],
'amount' => collect(
json_decode(
collect($payment['tags'])->firstWhere('0', 'description')[1],
true,
512,
JSON_THROW_ON_ERROR
)['tags']
)->firstWhere('0', 'amount')[1] / 1000,
]);
$this->currentYearIsPaid = collect($this->yearsPaid)->contains(
fn($yearPaid) => $yearPaid['year'] == date('Y') && $yearPaid['amount'] == $this->amountToPay
);
$this->paid = true;
}
};
$save = function ($type) {
$this->form->validate();
$this->currentPleb
@@ -33,11 +130,90 @@ $save = function ($type) {
]);
};
$createKind0 = function () {
$note = new NostrEvent();
$note->setKind(0);
$note->setContent('');
$note->setTags([
['display_name', 'Einundzwanzig Portal'],
['lud16', 'portaleinundzwanzig@getalby.com'],
['pubkey', 'daf83d92768b5d0005373f83e30d4203c0b747c170449e02fea611a0da125ee6'],
]);
$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();
};
$createPaymentEvent = function () {
$note = new NostrEvent();
$note->setKind(32121);
$note->setContent(
'Dieses Event dient der Zahlung des Mitgliedsbeitrags für das Jahr ' . date(
'Y',
) . '. Bitte zappe den Betrag von 1 Satoshi.',
);
$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();
$this->currentPleb->update([
'payment_event' => $result->eventId,
]);
};
$loadEvents = function () {
$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(config('services.relay')),
];
$relaySet = new RelaySet();
$relaySet->setRelays($relays);
$request = new Request($relaySet, $requestMessage);
$response = $request->send();
$this->events = collect($response[config('services.relay')])
->map(fn($event)
=> [
'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,
])
->unique('id')
->toArray();
};
?>
<x-layouts.app title="{{ __('Wahl') }}">
@volt
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto">
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="nostrZap(@this)">
<!-- Page header -->
<div class="mb-8">
@@ -157,7 +333,8 @@ $save = function ($type) {
<div class="text-sm">
<x-textarea
corner="Beschreibe deine Motivation, passives Mitglied zu werden."
label="Warum möchtest du passives Mitglied werden?" wire:model="form.reason"/>
label="Warum möchtest du passives Mitglied werden?"
wire:model="form.reason"/>
</div>
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4 mt-5">
<div class="sm:w-1/3 flex flex-col space-y-2">
@@ -180,7 +357,8 @@ $save = function ($type) {
<x-textarea
corner="Woher kennen wir dich? Was möchtest du einbringen?"
description="Wir bitten dich mindestens von 3 aktiven Mitgliedern auf Nostr gefolgt zu werden."
label="Warum möchtest du aktives Mitglied werden?" wire:model="form.reason"/>
label="Warum möchtest du aktives Mitglied werden?"
wire:model="form.reason"/>
</div>
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4 mt-5">
<div class="sm:w-1/3 flex flex-col space-y-2">
@@ -205,8 +383,8 @@ $save = function ($type) {
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div>
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">Du hast
dich erfolgreich mit folgendem Grund beworben:
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
Du hast dich erfolgreich mit folgendem Grund beworben:
</div>
<div>{{ $currentPleb->application_text }}</div>
</div>
@@ -228,9 +406,117 @@ $save = function ($type) {
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div>
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">Dein
aktueller
Status: {{ $currentPleb->association_status->label() }}</div>
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
Dein aktueller
Status: {{ $currentPleb->association_status->label() }}
</div>
</div>
</div>
</div>
</div>
@endif
</section>
<section>
@if($currentPleb && $currentPleb->association_status->value > 1)
<div
class="inline-flex flex-col w-full px-4 py-2 rounded-lg text-sm bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400">
<div class="flex w-full justify-between items-start">
<div class="flex">
<svg class="shrink-0 fill-current text-yellow-500 mt-[3px] mr-3" width="16"
height="16" viewBox="0 0 16 16">
<path
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div>
<div
class="font-medium text-gray-800 dark:text-gray-100 mb-1 space-y-2">
<p>Nostr Event für die Zahlung des
Mitgliedsbeitrags: {{ $currentPleb->payment_event }}</p>
<div>
@if(isset($events[0]))
<p>{{ $events[0]['content'] }}</p>
<div class="mt-8">
@if(!$invoice && !$currentYearIsPaid)
<div class="flex justify-center">
<button
@click="zap('{{ date('Y') }}', '{{ $currentPubkey }}', {{ $amountToPay }})"
class="btn text-2xl dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-green-500"
>
<i class="fa-sharp-duotone fa-solid fa-bolt-lightning mr-2"></i>
Zap
</button>
</div>
@else
@if(!$currentYearIsPaid && $qrCode)
<div class="flex justify-center"
wire:key="qrcode"
wire:poll="listenForPayment">
<a href="lightning:{{ $invoice }}">
<img
class="p-12 bg-white"
src="{{ 'data:image/png;base64, '. $qrCode }}"
alt="qrcode">
</a>
</div>
@else
@if($currentYearIsPaid)
<div class="flex justify-center">
<button
class="btn text-2xl dark:bg-gray-800 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-green-500"
>
<i class="fa-sharp-duotone fa-solid fa-check-circle mr-2"></i>
aktuelles Jahr bezahlt
</button>
</div>
@endif
@endif
@endif
</div>
@endif
</div>
<section>
<h3 class="text-xl leading-snug text-gray-800 dark:text-gray-100 font-bold mb-1">
bisherige Zahlungen</h3>
<!-- Table -->
<table class="table-auto w-full dark:text-gray-400">
<!-- Table header -->
<thead
class="text-xs uppercase text-gray-400 dark:text-gray-500">
<tr class="flex flex-wrap md:table-row md:flex-no-wrap">
<th class="w-full block md:w-auto md:table-cell py-2">
<div class="font-semibold text-left">Satoshis</div>
</th>
<th class="w-full hidden md:w-auto md:table-cell py-2">
<div class="font-semibold text-left">Jahr</div>
</th>
<th class="w-full hidden md:w-auto md:table-cell py-2">
<div class="font-semibold text-left">Event-ID</div>
</th>
</tr>
</thead>
<!-- Table body -->
<tbody class="text-sm">
@foreach($payments as $payment)
<tr class="flex flex-wrap md:table-row md:flex-no-wrap border-b border-gray-200 dark:border-gray-700/60 py-2 md:py-0">
<td class="w-full block md:w-auto md:table-cell py-0.5 md:py-2">
<div
class="text-left font-medium text-gray-800 dark:text-gray-100">{{ collect(json_decode(collect($payment['tags'])->firstWhere('0', 'description')[1], true, 512, JSON_THROW_ON_ERROR)['tags'])->firstWhere('0', 'amount')[1] / 1000 }}</div>
</td>
<td class="w-full block md:w-auto md:table-cell py-0.5 md:py-2">
<div
class="text-left">{{ $payment['content'] }}</div>
</td>
<td class="w-full block md:w-auto md:table-cell py-0.5 md:py-2">
<div
class="text-left font-medium">{{ $payment['id'] }}</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</section>
</div>
</div>
</div>
</div>
@@ -240,22 +526,6 @@ $save = function ($type) {
</div>
<!-- Panel footer -->
{{--<footer>
<div class="flex flex-col px-6 py-5 border-t border-gray-200 dark:border-gray-700/60">
<div class="flex self-end">
<button
class="btn dark:bg-[#1B1B1B] border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 text-[#1B1B1B] dark:text-gray-300">
Cancel
</button>
<button
class="btn bg-gray-900 text-gray-100 hover:bg-[#1B1B1B] dark:bg-gray-100 dark:text-[#1B1B1B] dark:hover:bg-white ml-3">
Save Changes
</button>
</div>
</div>
</footer>--}}
</div>
</div>