Add photobooth uploader download email
This commit is contained in:
@@ -3,12 +3,16 @@
|
||||
namespace App\Http\Controllers\Api\Tenant;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Photobooth\PhotoboothSendUploaderDownloadRequest;
|
||||
use App\Http\Resources\Tenant\PhotoboothStatusResource;
|
||||
use App\Mail\PhotoboothUploaderDownload;
|
||||
use App\Models\Event;
|
||||
use App\Models\PhotoboothSetting;
|
||||
use App\Services\Photobooth\PhotoboothProvisioner;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PhotoboothController extends Controller
|
||||
{
|
||||
@@ -69,6 +73,39 @@ class PhotoboothController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendUploaderDownloadEmail(PhotoboothSendUploaderDownloadRequest $request, Event $event): JsonResponse
|
||||
{
|
||||
$this->assertEventBelongsToTenant($request, $event);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user || ! $user->email) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('No email address is configured for this account.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$locale = $user->preferred_locale ?: app()->getLocale();
|
||||
$eventName = $this->resolveEventName($event, $locale);
|
||||
$recipientName = $user->fullName ?? $user->name ?? $user->email;
|
||||
|
||||
Mail::to($user->email)
|
||||
->locale($locale)
|
||||
->queue(new PhotoboothUploaderDownload(
|
||||
recipientName: $recipientName,
|
||||
eventName: $eventName,
|
||||
links: [
|
||||
'windows' => url('/downloads/PhotoboothUploader-win-x64.exe'),
|
||||
'macos' => url('/downloads/PhotoboothUploader-macos-x64'),
|
||||
'linux' => url('/downloads/PhotoboothUploader-linux-x64'),
|
||||
],
|
||||
));
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Download links sent via email.'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function resource(Event $event): PhotoboothStatusResource
|
||||
{
|
||||
return PhotoboothStatusResource::make([
|
||||
@@ -92,4 +129,30 @@ class PhotoboothController extends Controller
|
||||
|
||||
return in_array($mode, ['sparkbooth', 'ftp'], true) ? $mode : 'ftp';
|
||||
}
|
||||
|
||||
protected function resolveEventName(Event $event, ?string $locale = null): string
|
||||
{
|
||||
$name = $event->name;
|
||||
|
||||
if (is_string($name) && trim($name) !== '') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
if (is_array($name)) {
|
||||
$locale = $locale ?: app()->getLocale();
|
||||
$localized = $name[$locale] ?? null;
|
||||
|
||||
if (is_string($localized) && trim($localized) !== '') {
|
||||
return $localized;
|
||||
}
|
||||
|
||||
foreach ($name as $value) {
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $event->slug ?: __('emails.photobooth_uploader.event_fallback');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Photobooth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PhotoboothSendUploaderDownloadRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
50
app/Mail/PhotoboothUploaderDownload.php
Normal file
50
app/Mail/PhotoboothUploaderDownload.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PhotoboothUploaderDownload extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @param array{windows:string, macos:string, linux:string} $links
|
||||
*/
|
||||
public function __construct(
|
||||
public string $recipientName,
|
||||
public string $eventName,
|
||||
public array $links,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: __('emails.photobooth_uploader.subject', [
|
||||
'event' => $this->eventName,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.photobooth-uploader-download',
|
||||
with: [
|
||||
'recipientName' => $this->recipientName,
|
||||
'eventName' => $this->eventName,
|
||||
'links' => $this->links,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,25 @@ return [
|
||||
'benefit4' => 'Unterstuetzung, wenn du sie brauchst',
|
||||
'footer' => 'Brauchst du Hilfe? Antworte einfach auf diese E-Mail.',
|
||||
],
|
||||
'photobooth_uploader' => [
|
||||
'subject' => 'Fotospiel Uploader App fuer :event',
|
||||
'preheader' => 'Download-Links fuer die Fotospiel Photobooth Uploader App.',
|
||||
'hero_title' => 'Hallo :name,',
|
||||
'hero_subtitle' => 'Deine Uploader App fuer :event ist bereit.',
|
||||
'body' => 'Hier findest du die Download-Links fuer die Fotospiel Photobooth Uploader App. Installiere die passende Version auf dem Photobooth-PC, bevor dein Event startet.',
|
||||
'downloads_title' => 'Download-Links',
|
||||
'downloads' => [
|
||||
'windows' => 'Windows (x64)',
|
||||
'macos' => 'macOS (x64)',
|
||||
'linux' => 'Linux (x64)',
|
||||
],
|
||||
'cta_windows' => 'Download fuer Windows',
|
||||
'cta_macos' => 'Download fuer macOS',
|
||||
'cta_linux' => 'Download fuer Linux',
|
||||
'credentials_hint' => 'Die Zugangsdaten bleiben im Admin-Dashboard. Erstelle einen Verbindungscode, sobald du die App koppeln moechtest.',
|
||||
'footer' => 'Fragen? Antworte einfach auf diese E-Mail.',
|
||||
'event_fallback' => 'dein Event',
|
||||
],
|
||||
'package_limits' => [
|
||||
'package_fallback' => 'Paket',
|
||||
'team_fallback' => 'dein Team',
|
||||
|
||||
@@ -65,6 +65,25 @@ return [
|
||||
'benefit4' => 'Friendly support whenever you need help',
|
||||
'footer' => 'Need help? Reply to this email.',
|
||||
],
|
||||
'photobooth_uploader' => [
|
||||
'subject' => 'Fotospiel Uploader App for :event',
|
||||
'preheader' => 'Download links for the Fotospiel Photobooth Uploader.',
|
||||
'hero_title' => 'Hi :name,',
|
||||
'hero_subtitle' => 'Your uploader app for :event is ready.',
|
||||
'body' => 'Here are the download links for the Fotospiel Photobooth Uploader. Install the right version on the photobooth PC before your event starts.',
|
||||
'downloads_title' => 'Download links',
|
||||
'downloads' => [
|
||||
'windows' => 'Windows (x64)',
|
||||
'macos' => 'macOS (x64)',
|
||||
'linux' => 'Linux (x64)',
|
||||
],
|
||||
'cta_windows' => 'Download for Windows',
|
||||
'cta_macos' => 'Download for macOS',
|
||||
'cta_linux' => 'Download for Linux',
|
||||
'credentials_hint' => 'Connection credentials stay in the admin dashboard. Generate a connect code when you are ready to pair the app.',
|
||||
'footer' => 'Questions? Reply to this email and we will help.',
|
||||
'event_fallback' => 'your event',
|
||||
],
|
||||
'package_limits' => [
|
||||
'package_fallback' => 'package',
|
||||
'team_fallback' => 'your team',
|
||||
|
||||
@@ -2068,6 +2068,13 @@ export async function createEventPhotoboothConnectCode(
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendEventPhotoboothUploaderEmail(slug: string): Promise<void> {
|
||||
const response = await authorizedFetch(`${photoboothEndpoint(slug)}/uploader-email`, {
|
||||
method: 'POST',
|
||||
});
|
||||
await jsonOrThrow<{ message?: string }>(response, 'Failed to send photobooth uploader email');
|
||||
}
|
||||
|
||||
export async function submitTenantFeedback(payload: {
|
||||
category: string;
|
||||
sentiment?: 'positive' | 'neutral' | 'negative';
|
||||
|
||||
@@ -1211,6 +1211,9 @@
|
||||
"uploaderDownload": {
|
||||
"title": "Fotospiel Uploader App",
|
||||
"description": "Die Fotospiel Uploader App wird benötigt, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.",
|
||||
"emailAction": "Download-Links per E-Mail senden",
|
||||
"emailSuccess": "Download-Links wurden per E-Mail gesendet.",
|
||||
"emailFailed": "E-Mail konnte nicht gesendet werden.",
|
||||
"actionWindows": "Uploader herunterladen (Windows)",
|
||||
"actionMac": "Uploader herunterladen (macOS)",
|
||||
"actionLinux": "Uploader herunterladen (Linux)"
|
||||
|
||||
@@ -924,6 +924,9 @@
|
||||
"uploaderDownload": {
|
||||
"title": "Fotospiel Uploader App",
|
||||
"description": "The Fotospiel Uploader App is required so uploads stay stable, credentials remain protected, and no files are lost.",
|
||||
"emailAction": "Send download links by email",
|
||||
"emailSuccess": "Download links were sent by email.",
|
||||
"emailFailed": "Email could not be sent.",
|
||||
"actionWindows": "Download uploader (Windows)",
|
||||
"actionMac": "Download uploader (macOS)",
|
||||
"actionLinux": "Download uploader (Linux)"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3 } from 'lucide-react';
|
||||
import { RefreshCcw, PlugZap, RefreshCw, ShieldCheck, Copy, Power, Clock3, Mail } from 'lucide-react';
|
||||
import { YStack, XStack } from '@tamagui/stacks';
|
||||
import { SizableText as Text } from '@tamagui/text';
|
||||
import { Switch } from '@tamagui/switch';
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
disableEventPhotobooth,
|
||||
rotateEventPhotobooth,
|
||||
createEventPhotoboothConnectCode,
|
||||
sendEventPhotoboothUploaderEmail,
|
||||
PhotoboothStatus,
|
||||
TenantEvent,
|
||||
} from '../api';
|
||||
@@ -41,6 +42,7 @@ export default function MobileEventPhotoboothPage() {
|
||||
const [connectCode, setConnectCode] = React.useState<string | null>(null);
|
||||
const [connectExpiresAt, setConnectExpiresAt] = React.useState<string | null>(null);
|
||||
const [connectLoading, setConnectLoading] = React.useState(false);
|
||||
const [sendingEmail, setSendingEmail] = React.useState(false);
|
||||
const [showCredentials, setShowCredentials] = React.useState(false);
|
||||
const back = useBackNavigation(slug ? adminPath(`/mobile/events/${slug}`) : adminPath('/mobile/events'));
|
||||
|
||||
@@ -138,6 +140,23 @@ export default function MobileEventPhotoboothPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendDownloadEmail = async () => {
|
||||
if (!slug) return;
|
||||
setSendingEmail(true);
|
||||
try {
|
||||
await sendEventPhotoboothUploaderEmail(slug);
|
||||
toast.success(t('photobooth.uploaderDownload.emailSuccess', 'Download-Links wurden per E-Mail gesendet.'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
toast.error(
|
||||
getApiErrorMessage(err, t('photobooth.uploaderDownload.emailFailed', 'E-Mail konnte nicht gesendet werden.'))
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setSendingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const spark = status?.sparkbooth ?? null;
|
||||
const metrics = spark?.metrics ?? null;
|
||||
const expiresAt = spark?.expires_at ?? status?.expires_at;
|
||||
@@ -261,6 +280,17 @@ export default function MobileEventPhotoboothPage() {
|
||||
'Die Fotospiel Uploader App ist verpflichtend, damit Uploads stabil laufen, die Zugangsdaten geschützt bleiben und keine Dateien verloren gehen.'
|
||||
)}
|
||||
</Text>
|
||||
<CTAButton
|
||||
label={
|
||||
sendingEmail
|
||||
? t('common.processing', '...')
|
||||
: t('photobooth.uploaderDownload.emailAction', 'Download-Links per E-Mail senden')
|
||||
}
|
||||
tone="ghost"
|
||||
onPress={handleSendDownloadEmail}
|
||||
iconLeft={<Mail size={14} color={text} />}
|
||||
disabled={sendingEmail}
|
||||
/>
|
||||
<CTAButton
|
||||
label={t('photobooth.uploaderDownload.actionWindows', 'Uploader herunterladen (Windows)')}
|
||||
onPress={() => {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
@extends('emails.partials.layout')
|
||||
|
||||
@section('title', __('emails.photobooth_uploader.subject', ['event' => $eventName]))
|
||||
@section('preheader', __('emails.photobooth_uploader.preheader', ['event' => $eventName]))
|
||||
@section('hero_title', __('emails.photobooth_uploader.hero_title', ['name' => $recipientName]))
|
||||
@section('hero_subtitle', __('emails.photobooth_uploader.hero_subtitle', ['event' => $eventName]))
|
||||
|
||||
@section('content')
|
||||
<p style="margin:0 0 16px; font-size:15px; color:#1f2937;">
|
||||
{{ __('emails.photobooth_uploader.body', ['event' => $eventName]) }}
|
||||
</p>
|
||||
<p style="margin:0 0 12px; font-size:14px; color:#6b7280;">
|
||||
{{ __('emails.photobooth_uploader.downloads_title') }}
|
||||
</p>
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="margin-bottom:12px;">
|
||||
<tr>
|
||||
<td style="padding:4px 0; font-size:14px; color:#1f2937;">
|
||||
<strong>{{ __('emails.photobooth_uploader.downloads.windows') }}</strong>
|
||||
</td>
|
||||
<td align="right" style="padding:4px 0; font-size:14px;">
|
||||
<a href="{{ $links['windows'] }}" style="color:#1d4ed8; text-decoration:none;">
|
||||
{{ $links['windows'] }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px 0; font-size:14px; color:#1f2937;">
|
||||
<strong>{{ __('emails.photobooth_uploader.downloads.macos') }}</strong>
|
||||
</td>
|
||||
<td align="right" style="padding:4px 0; font-size:14px;">
|
||||
<a href="{{ $links['macos'] }}" style="color:#1d4ed8; text-decoration:none;">
|
||||
{{ $links['macos'] }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px 0; font-size:14px; color:#1f2937;">
|
||||
<strong>{{ __('emails.photobooth_uploader.downloads.linux') }}</strong>
|
||||
</td>
|
||||
<td align="right" style="padding:4px 0; font-size:14px;">
|
||||
<a href="{{ $links['linux'] }}" style="color:#1d4ed8; text-decoration:none;">
|
||||
{{ $links['linux'] }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:0; font-size:14px; color:#6b7280;">
|
||||
{{ __('emails.photobooth_uploader.credentials_hint') }}
|
||||
</p>
|
||||
@endsection
|
||||
|
||||
@section('cta')
|
||||
<a href="{{ $links['windows'] }}" style="display:inline-block; background-color:#111827; color:#ffffff; text-decoration:none; padding:12px 20px; border-radius:999px; font-weight:600; font-size:14px; margin-right:8px;">
|
||||
{{ __('emails.photobooth_uploader.cta_windows') }}
|
||||
</a>
|
||||
<a href="{{ $links['macos'] }}" style="display:inline-block; background-color:#f3f4f6; color:#111827; text-decoration:none; padding:12px 18px; border-radius:999px; font-weight:600; font-size:14px; margin-right:8px;">
|
||||
{{ __('emails.photobooth_uploader.cta_macos') }}
|
||||
</a>
|
||||
<a href="{{ $links['linux'] }}" style="display:inline-block; background-color:#f3f4f6; color:#111827; text-decoration:none; padding:12px 18px; border-radius:999px; font-weight:600; font-size:14px;">
|
||||
{{ __('emails.photobooth_uploader.cta_linux') }}
|
||||
</a>
|
||||
@endsection
|
||||
|
||||
@section('footer')
|
||||
{!! __('emails.photobooth_uploader.footer') !!}
|
||||
@endsection
|
||||
@@ -270,6 +270,8 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
|
||||
Route::post('/disable', [PhotoboothController::class, 'disable'])->name('tenant.events.photobooth.disable');
|
||||
Route::post('/connect-codes', [PhotoboothConnectCodeController::class, 'store'])
|
||||
->name('tenant.events.photobooth.connect-codes.store');
|
||||
Route::post('/uploader-email', [PhotoboothController::class, 'sendUploaderDownloadEmail'])
|
||||
->name('tenant.events.photobooth.uploader-email');
|
||||
});
|
||||
|
||||
Route::get('members', [EventMemberController::class, 'index'])
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Photobooth;
|
||||
|
||||
use App\Mail\PhotoboothUploaderDownload;
|
||||
use App\Models\Event;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\Feature\Tenant\TenantTestCase;
|
||||
|
||||
class PhotoboothUploaderDownloadEmailTest extends TenantTestCase
|
||||
{
|
||||
#[Test]
|
||||
public function it_sends_the_photobooth_uploader_download_email(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$event = Event::factory()->for($this->tenant)->create([
|
||||
'slug' => 'photobooth-email',
|
||||
]);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/photobooth/uploader-email");
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
Mail::assertQueued(PhotoboothUploaderDownload::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user