feat: email notifications on claim approval + OG images for names
ClaimApproved mailable sent when admin approves a claim — dark
themed HTML email with name, next steps, and CTA links. Wrapped
in try/catch so email failure doesn't block approval.
Dynamic SVG OpenGraph images at /names/{name}/og.svg — shows name,
type badge (Registered/Reserved/Gateway/Available), and branding.
og:image meta tag added to name detail pages via @push('head').
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0241e1f4a6
commit
8f9c1f282e
6 changed files with 146 additions and 0 deletions
|
|
@ -497,6 +497,14 @@ class NamesController extends Controller
|
|||
'email' => $claim->email,
|
||||
]);
|
||||
|
||||
// Send approval notification email (queued, non-blocking)
|
||||
try {
|
||||
\Illuminate\Support\Facades\Mail::to($claim->email)
|
||||
->send(new \Mod\Names\Mail\ClaimApproved($claim));
|
||||
} catch (\Throwable $e) {
|
||||
// Email delivery failure shouldn't break approval
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'claim_id' => $claim->claim_id,
|
||||
'name' => $claim->name,
|
||||
|
|
|
|||
64
app/Mod/Names/Controllers/OgImageController.php
Normal file
64
app/Mod/Names/Controllers/OgImageController.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mod\Names\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Mod\Chain\Services\DaemonRpc;
|
||||
|
||||
/**
|
||||
* Dynamic OpenGraph image for .lthn names.
|
||||
*
|
||||
* GET /names/{name}/og.svg
|
||||
*
|
||||
* Returns an SVG that social platforms can render as preview image.
|
||||
*/
|
||||
class OgImageController extends Controller
|
||||
{
|
||||
public function __invoke(string $name, DaemonRpc $rpc): \Illuminate\Http\Response
|
||||
{
|
||||
$alias = $rpc->getAliasByName(strtolower(trim($name)));
|
||||
|
||||
$type = 'Available';
|
||||
$typeColour = '#34d399';
|
||||
|
||||
if ($alias) {
|
||||
$comment = $alias['comment'] ?? '';
|
||||
if (str_contains($comment, 'type=reserved')) {
|
||||
$type = 'Reserved';
|
||||
$typeColour = '#fbbf24';
|
||||
} elseif (str_contains($comment, 'type=gateway')) {
|
||||
$type = 'Gateway';
|
||||
$typeColour = '#34d399';
|
||||
} else {
|
||||
$type = 'Registered';
|
||||
$typeColour = '#818cf8';
|
||||
}
|
||||
}
|
||||
|
||||
$escapedName = htmlspecialchars($name, ENT_XML1);
|
||||
|
||||
$svg = <<<SVG
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||
<rect width="1200" height="630" fill="#0a0e17"/>
|
||||
<rect x="0" y="0" width="1200" height="4" fill="{$typeColour}"/>
|
||||
|
||||
<text x="80" y="200" font-family="Inter, -apple-system, sans-serif" font-size="72" font-weight="700" fill="#e5e7eb">
|
||||
<tspan fill="#818cf8">{$escapedName}</tspan><tspan fill="#6b7280">.lthn</tspan>
|
||||
</text>
|
||||
|
||||
<rect x="80" y="240" width="120" height="36" rx="18" fill="{$typeColour}" opacity="0.15"/>
|
||||
<text x="140" y="265" font-family="Inter, sans-serif" font-size="16" font-weight="600" fill="{$typeColour}" text-anchor="middle">{$type}</text>
|
||||
|
||||
<text x="80" y="340" font-family="Inter, sans-serif" font-size="24" fill="#6b7280">Blockchain domain name on the Lethean network</text>
|
||||
|
||||
<text x="80" y="540" font-family="Inter, sans-serif" font-size="20" fill="#374151">lthn.io</text>
|
||||
<text x="1120" y="540" font-family="Inter, sans-serif" font-size="18" fill="#374151" text-anchor="end">Lethean CIC</text>
|
||||
</svg>
|
||||
SVG;
|
||||
|
||||
return response($svg, 200)->header('Content-Type', 'image/svg+xml')
|
||||
->header('Cache-Control', 'public, max-age=3600');
|
||||
}
|
||||
}
|
||||
66
app/Mod/Names/Mail/ClaimApproved.php
Normal file
66
app/Mod/Names/Mail/ClaimApproved.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mod\Names\Mail;
|
||||
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Mod\Names\Models\NameClaim;
|
||||
|
||||
/**
|
||||
* Sent when a pre-registration claim is approved.
|
||||
*
|
||||
* Mail::to($claim->email)->send(new ClaimApproved($claim));
|
||||
*/
|
||||
class ClaimApproved extends Mailable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly NameClaim $claim,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: "{$this->claim->name}.lthn — Your Name Claim Has Been Approved",
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
htmlString: $this->buildHtml(),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildHtml(): string
|
||||
{
|
||||
$name = e($this->claim->name);
|
||||
$fqdn = "{$name}.lthn";
|
||||
|
||||
return <<<HTML
|
||||
<div style="font-family: -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 2rem;">
|
||||
<h1 style="color: #34d399; font-size: 1.5rem;">Your .lthn Name is Ready</h1>
|
||||
<p>Great news — your claim for <strong>{$fqdn}</strong> has been approved and registered on the Lethean blockchain.</p>
|
||||
|
||||
<div style="background: #111827; border: 1px solid #1f2937; border-radius: 0.5rem; padding: 1.5rem; margin: 1.5rem 0;">
|
||||
<p style="margin: 0 0 0.5rem; color: #9ca3af;">Name</p>
|
||||
<p style="margin: 0; font-size: 1.25rem; font-weight: 600; color: #e5e7eb;">{$fqdn}</p>
|
||||
</div>
|
||||
|
||||
<h2 style="font-size: 1.1rem; margin-top: 2rem;">What's Next?</h2>
|
||||
<ul style="line-height: 2;">
|
||||
<li><strong>Manage DNS</strong> — log in at <a href="https://order.lthn.ai">order.lthn.ai</a> to set up your DNS records</li>
|
||||
<li><strong>View your name</strong> — <a href="https://lthn.io/names/{$name}">lthn.io/names/{$name}</a></li>
|
||||
<li><strong>Get SSL</strong> — add a certificate from the services page</li>
|
||||
</ul>
|
||||
|
||||
<p style="color: #6b7280; font-size: 0.85rem; margin-top: 2rem;">
|
||||
Lethean CIC — Community Interest Company<br>
|
||||
<a href="https://lthn.io" style="color: #818cf8;">lthn.io</a>
|
||||
</p>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ use Mod\Names\Controllers\NamesWebController;
|
|||
|
||||
Route::get('/', [NamesWebController::class, 'index']);
|
||||
Route::get('/register', [NamesWebController::class, 'register']);
|
||||
Route::get('/{name}/og.svg', \Mod\Names\Controllers\OgImageController::class);
|
||||
Route::get('/{name}', [NamesWebController::class, 'show']);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
@section('title', $name . '.lthn')
|
||||
@section('meta_description', $name . '.lthn — blockchain domain name registered on the Lethean network. ' . ($alias['comment'] ?? ''))
|
||||
|
||||
@push('head')
|
||||
<meta property="og:image" content="{{ url('/names/' . $name . '/og.svg') }}">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="section" style="max-width: 900px; margin: 0 auto;">
|
||||
<h2 style="margin-bottom: 0.25rem;"><span style="color: var(--accent);">{{ $name }}</span>.lthn</h2>
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
code { background: var(--surface); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.85em; }
|
||||
</style>
|
||||
<script>window.LTHN_API = '{{ config('chain.api_url', '') }}';</script>
|
||||
@stack('head')
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue