-
- {(
- [
- { key: 'all', label: t('header.notifications.scope.all', 'Alle') },
- { key: 'tips', label: t('header.notifications.scope.tips', 'Tipps & Achievements') },
- { key: 'general', label: t('header.notifications.scope.general', 'Allgemein') },
- ] as const
- ).map((option) => (
-
- ))}
-
+
+
+ {(
+ [
+ { key: 'all', label: t('header.notifications.scope.all', 'Alle') },
+ { key: 'tips', label: t('header.notifications.scope.tips', 'Tipps & Achievements') },
+ { key: 'general', label: t('header.notifications.scope.general', 'Allgemein') },
+ ] as const
+ ).map((option) => (
+
+ ))}
- )}
- {activeTab === 'uploads' && (center.pendingCount > 0 || center.queueCount > 0) && (
+
+
+ {center.loading ? (
+
+ ) : scopedNotifications.length === 0 ? (
+
+ ) : (
+ scopedNotifications.map((item) => (
+ center.markAsRead(item.id)}
+ onDismiss={() => center.dismiss(item.id)}
+ t={t}
+ />
+ ))
+ )}
+
+ {activeTab === 'status' && (
{center.pendingCount > 0 && (
@@ -447,32 +478,30 @@ function NotificationButton({ center, eventToken, open, onToggle, panelRef, butt
)}
)}
-
- {center.loading ? (
-
- ) : scopedNotifications.length === 0 ? (
-
- ) : (
- scopedNotifications.map((item) => (
-
center.markAsRead(item.id)}
- onDismiss={() => center.dismiss(item.id)}
- t={t}
+ {taskProgress && (
+
+
+
+
{t('header.notifications.badgeLabel', 'Badge-Fortschritt')}
+
+ {taskProgress.completedCount}/{TASK_BADGE_TARGET}
+
+
+
+ {t('header.notifications.tasksCta', 'Weiter')}
+
+
+
+
+
+ )}
({
queueItems: [],
queueCount: 0,
pendingCount: 0,
+ totalCount: 0,
loading: false,
pendingLoading: false,
refresh: vi.fn(),
@@ -96,10 +97,10 @@ describe('Header notifications toggle', () => {
const bellButton = screen.getByLabelText('Benachrichtigungen anzeigen');
fireEvent.click(bellButton);
- expect(screen.getByText('Updates')).toBeInTheDocument();
+ expect(screen.getByText('Benachrichtigungen')).toBeInTheDocument();
fireEvent.click(bellButton);
- expect(screen.queryByText('Updates')).not.toBeInTheDocument();
+ expect(screen.queryByText('Benachrichtigungen')).not.toBeInTheDocument();
});
});
diff --git a/resources/js/guest/context/NotificationCenterContext.tsx b/resources/js/guest/context/NotificationCenterContext.tsx
index ef023c0..817b8f0 100644
--- a/resources/js/guest/context/NotificationCenterContext.tsx
+++ b/resources/js/guest/context/NotificationCenterContext.tsx
@@ -16,6 +16,7 @@ export type NotificationCenterValue = {
queueItems: QueueItem[];
queueCount: number;
pendingCount: number;
+ totalCount: number;
loading: boolean;
pendingLoading: boolean;
refresh: () => Promise;
@@ -263,9 +264,11 @@ export function NotificationCenterProvider({ eventToken, children }: { eventToke
}, [loadNotifications, refreshQueue, loadPendingUploads]);
const loading = loadingNotifications || queueLoading || pendingLoading;
+ const totalCount = unreadCount + queueCount + pendingCount;
+
React.useEffect(() => {
- void updateAppBadge(unreadCount);
- }, [unreadCount]);
+ void updateAppBadge(totalCount);
+ }, [totalCount]);
const value: NotificationCenterValue = {
notifications,
@@ -273,6 +276,7 @@ export function NotificationCenterProvider({ eventToken, children }: { eventToke
queueItems: items,
queueCount,
pendingCount,
+ totalCount,
loading,
pendingLoading,
refresh,
diff --git a/resources/js/guest/i18n/messages.ts b/resources/js/guest/i18n/messages.ts
index 83b15ec..6a8291f 100644
--- a/resources/js/guest/i18n/messages.ts
+++ b/resources/js/guest/i18n/messages.ts
@@ -42,13 +42,7 @@ export const messages: Record = {
},
helpGallery: 'Hilfe zu Galerie & Teilen',
notifications: {
- title: 'Updates',
- unread: '{count} neu',
- allRead: 'Alles gelesen',
- tabUnread: 'Nachrichten',
- tabUploads: 'Uploads',
- tabAll: 'Alle Updates',
- emptyStatus: 'Keine Upload-Hinweise oder Wartungen aktiv.',
+ tabStatus: 'Upload-Status',
},
},
liveShowPlayer: {
@@ -780,13 +774,7 @@ export const messages: Record = {
},
helpGallery: 'Help: Gallery & sharing',
notifications: {
- title: 'Updates',
- unread: '{count} new',
- allRead: 'All read',
- tabUnread: 'Messages',
- tabUploads: 'Uploads',
- tabAll: 'All updates',
- emptyStatus: 'No upload status or maintenance active.',
+ tabStatus: 'Upload status',
},
},
liveShowPlayer: {
diff --git a/routes/api.php b/routes/api.php
index ffa82fc..e766285 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -6,7 +6,6 @@ use App\Http\Controllers\Api\LegalController;
use App\Http\Controllers\Api\LiveShowController;
use App\Http\Controllers\Api\Marketing\CouponPreviewController;
use App\Http\Controllers\Api\PackageController;
-use App\Http\Controllers\Api\PhotoboothConnectController;
use App\Http\Controllers\Api\SparkboothUploadController;
use App\Http\Controllers\Api\Tenant\AdminPushSubscriptionController;
use App\Http\Controllers\Api\Tenant\DashboardController;
@@ -25,7 +24,6 @@ use App\Http\Controllers\Api\Tenant\LiveShowLinkController;
use App\Http\Controllers\Api\Tenant\LiveShowPhotoController;
use App\Http\Controllers\Api\Tenant\NotificationLogController;
use App\Http\Controllers\Api\Tenant\OnboardingController;
-use App\Http\Controllers\Api\Tenant\PhotoboothConnectCodeController;
use App\Http\Controllers\Api\Tenant\PhotoboothController;
use App\Http\Controllers\Api\Tenant\PhotoController;
use App\Http\Controllers\Api\Tenant\ProfileController;
@@ -155,9 +153,6 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
Route::post('/photobooth/sparkbooth/upload', [SparkboothUploadController::class, 'store'])
->name('photobooth.sparkbooth.upload');
- Route::post('/photobooth/connect', [PhotoboothConnectController::class, 'store'])
- ->middleware('throttle:photobooth-connect')
- ->name('photobooth.connect');
Route::get('/tenant/events/{event:slug}/photos/{photo}/{variant}/asset', [PhotoController::class, 'asset'])
->whereNumber('photo')
@@ -268,8 +263,6 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
Route::post('/enable', [PhotoboothController::class, 'enable'])->name('tenant.events.photobooth.enable');
Route::post('/rotate', [PhotoboothController::class, 'rotate'])->name('tenant.events.photobooth.rotate');
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::get('members', [EventMemberController::class, 'index'])
@@ -360,8 +353,6 @@ Route::prefix('v1')->name('api.v1.')->group(function () {
Route::post('/complete', [PackageController::class, 'completePurchase'])->name('packages.complete');
Route::post('/free', [PackageController::class, 'assignFree'])->name('packages.free');
Route::post('/paddle-checkout', [PackageController::class, 'createPaddleCheckout'])->name('packages.paddle-checkout');
- Route::get('/checkout-session/{session}/status', [PackageController::class, 'checkoutSessionStatus'])
- ->name('packages.checkout-session.status');
});
Route::get('addons/catalog', [EventAddonCatalogController::class, 'index'])
diff --git a/tests/Feature/Photobooth/PhotoboothConnectCodeTest.php b/tests/Feature/Photobooth/PhotoboothConnectCodeTest.php
deleted file mode 100644
index 68bdb6b..0000000
--- a/tests/Feature/Photobooth/PhotoboothConnectCodeTest.php
+++ /dev/null
@@ -1,100 +0,0 @@
-for($this->tenant)->create([
- 'slug' => 'connect-code-event',
- ]);
-
- EventPhotoboothSetting::factory()
- ->for($event)
- ->activeSparkbooth()
- ->create([
- 'username' => 'pbconnect',
- 'password' => 'SECRET12',
- ]);
-
- $response = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/photobooth/connect-codes");
-
- $response->assertOk()
- ->assertJsonPath('data.code', fn ($value) => is_string($value) && strlen($value) === 6)
- ->assertJsonPath('data.expires_at', fn ($value) => is_string($value) && $value !== '');
-
- $this->assertDatabaseCount('photobooth_connect_codes', 1);
- }
-
- #[Test]
- public function it_redeems_a_connect_code_and_returns_upload_credentials(): void
- {
- $event = Event::factory()->for($this->tenant)->create([
- 'slug' => 'connect-code-redeem',
- ]);
-
- EventPhotoboothSetting::factory()
- ->for($event)
- ->activeSparkbooth()
- ->create([
- 'username' => 'pbconnect',
- 'password' => 'SECRET12',
- ]);
-
- $codeResponse = $this->authenticatedRequest('POST', "/api/v1/tenant/events/{$event->slug}/photobooth/connect-codes");
- $codeResponse->assertOk();
-
- $code = (string) $codeResponse->json('data.code');
-
- $redeem = $this->postJson('/api/v1/photobooth/connect', [
- 'code' => $code,
- ]);
-
- $redeem->assertOk()
- ->assertJsonPath('data.upload_url', fn ($value) => is_string($value) && $value !== '')
- ->assertJsonPath('data.username', 'pbconnect')
- ->assertJsonPath('data.password', 'SECRET12');
-
- $this->assertDatabaseHas('photobooth_connect_codes', [
- 'event_id' => $event->id,
- ]);
- }
-
- #[Test]
- public function it_rejects_expired_connect_codes(): void
- {
- $event = Event::factory()->for($this->tenant)->create([
- 'slug' => 'connect-code-expired',
- ]);
-
- EventPhotoboothSetting::factory()
- ->for($event)
- ->activeSparkbooth()
- ->create([
- 'username' => 'pbconnect',
- 'password' => 'SECRET12',
- ]);
-
- $code = '123456';
-
- PhotoboothConnectCode::query()->create([
- 'event_id' => $event->id,
- 'code_hash' => hash('sha256', $code),
- 'expires_at' => now()->subMinute(),
- ]);
-
- $response = $this->postJson('/api/v1/photobooth/connect', [
- 'code' => $code,
- ]);
-
- $response->assertStatus(422);
- }
-}
diff --git a/tests/Feature/Tenant/PhotoModerationControllerTest.php b/tests/Feature/Tenant/PhotoModerationControllerTest.php
deleted file mode 100644
index 567125e..0000000
--- a/tests/Feature/Tenant/PhotoModerationControllerTest.php
+++ /dev/null
@@ -1,26 +0,0 @@
-for($this->tenant)->create([
- 'slug' => 'moderation-event',
- ]);
- $photo = Photo::factory()->for($event)->create([
- 'status' => 'pending',
- ]);
-
- $response = $this->authenticatedRequest('PATCH', "/api/v1/tenant/events/{$event->slug}/photos/{$photo->id}", [
- 'status' => 'approved',
- ]);
-
- $response->assertOk();
- $this->assertSame('approved', $photo->refresh()->status);
- }
-}
diff --git a/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php b/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php
deleted file mode 100644
index 77e38de..0000000
--- a/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php
+++ /dev/null
@@ -1,46 +0,0 @@
-create([
- 'price' => 129,
- ]);
-
- $session = CheckoutSession::create([
- 'id' => (string) Str::uuid(),
- 'user_id' => $this->tenantUser->id,
- 'tenant_id' => $this->tenant->id,
- 'package_id' => $package->id,
- 'status' => CheckoutSession::STATUS_FAILED,
- 'provider' => CheckoutSession::PROVIDER_PADDLE,
- 'provider_metadata' => [
- 'paddle_checkout_url' => 'https://checkout.paddle.test/checkout/123',
- ],
- 'status_history' => [
- [
- 'status' => CheckoutSession::STATUS_FAILED,
- 'reason' => 'paddle_failed',
- 'at' => now()->toIso8601String(),
- ],
- ],
- ]);
-
- $response = $this->authenticatedRequest(
- 'GET',
- "/api/v1/tenant/packages/checkout-session/{$session->id}/status"
- );
-
- $response->assertOk()
- ->assertJsonPath('status', CheckoutSession::STATUS_FAILED)
- ->assertJsonPath('reason', 'paddle_failed')
- ->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123');
- }
-}
diff --git a/tests/Feature/Tenant/TenantPaddleCheckoutTest.php b/tests/Feature/Tenant/TenantPaddleCheckoutTest.php
index 76fc651..4156b20 100644
--- a/tests/Feature/Tenant/TenantPaddleCheckoutTest.php
+++ b/tests/Feature/Tenant/TenantPaddleCheckoutTest.php
@@ -29,10 +29,7 @@ class TenantPaddleCheckoutTest extends TenantTestCase
return $tenant->is($this->tenant)
&& $payloadPackage->is($package)
&& array_key_exists('success_url', $payload)
- && array_key_exists('return_url', $payload)
- && array_key_exists('metadata', $payload)
- && is_array($payload['metadata'])
- && ! empty($payload['metadata']['checkout_session_id']);
+ && array_key_exists('return_url', $payload);
})
->andReturn([
'checkout_url' => 'https://checkout.paddle.test/checkout/123',
@@ -45,8 +42,7 @@ class TenantPaddleCheckoutTest extends TenantTestCase
]);
$response->assertOk()
- ->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123')
- ->assertJsonStructure(['checkout_session_id']);
+ ->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123');
}
public function test_paddle_checkout_requires_paddle_price_id(): void
diff --git a/tests/Unit/RateLimitConfigTest.php b/tests/Unit/RateLimitConfigTest.php
deleted file mode 100644
index 5f4a14d..0000000
--- a/tests/Unit/RateLimitConfigTest.php
+++ /dev/null
@@ -1,67 +0,0 @@
- '10.0.0.1',
- ]);
- $request->attributes->set('tenant_id', 42);
-
- $limiter = RateLimiter::limiter('tenant-api');
-
- $this->assertNotNull($limiter);
-
- $limit = $limiter($request);
-
- $this->assertInstanceOf(Limit::class, $limit);
- $this->assertSame(600, $limit->maxAttempts);
- }
-
- public function test_guest_api_rate_limiter_allows_higher_throughput(): void
- {
- $request = Request::create('/api/v1/events/sample', 'GET', [], [], [], [
- 'REMOTE_ADDR' => '10.0.0.2',
- ]);
-
- $limiter = RateLimiter::limiter('guest-api');
-
- $this->assertNotNull($limiter);
-
- $limit = $limiter($request);
-
- $this->assertInstanceOf(Limit::class, $limit);
- $this->assertSame(300, $limit->maxAttempts);
- }
-
- public function test_guest_policy_defaults_follow_join_token_limits(): void
- {
- $accessLimit = 300;
- $downloadLimit = 120;
-
- config([
- 'join_tokens.access_limit' => $accessLimit,
- 'join_tokens.download_limit' => $downloadLimit,
- ]);
-
- GuestPolicySetting::query()->delete();
- GuestPolicySetting::flushCache();
-
- $settings = GuestPolicySetting::current();
-
- $this->assertSame($accessLimit, $settings->join_token_access_limit);
- $this->assertSame($downloadLimit, $settings->join_token_download_limit);
- }
-}
diff --git a/tests/Unit/SendPhotoUploadedNotificationTest.php b/tests/Unit/SendPhotoUploadedNotificationTest.php
deleted file mode 100644
index 2fc38bf..0000000
--- a/tests/Unit/SendPhotoUploadedNotificationTest.php
+++ /dev/null
@@ -1,144 +0,0 @@
-create();
- $listener = $this->app->make(SendPhotoUploadedNotification::class);
-
- GuestNotification::factory()->create([
- 'tenant_id' => $event->tenant_id,
- 'event_id' => $event->id,
- 'type' => GuestNotificationType::PHOTO_ACTIVITY,
- 'title' => 'Fotospiel-Test hat gerade ein Foto gemacht 🎉',
- 'payload' => [
- 'photo_id' => 123,
- 'photo_ids' => [123],
- 'count' => 1,
- ],
- 'created_at' => now()->subSeconds(5),
- 'updated_at' => now()->subSeconds(5),
- ]);
-
- $listener->handle(new GuestPhotoUploaded(
- $event,
- 123,
- 'device-123',
- 'Fotospiel-Test'
- ));
-
- $notification = GuestNotification::query()
- ->where('event_id', $event->id)
- ->where('type', GuestNotificationType::PHOTO_ACTIVITY)
- ->first();
-
- $this->assertSame(1, GuestNotification::query()
- ->where('event_id', $event->id)
- ->where('type', GuestNotificationType::PHOTO_ACTIVITY)
- ->count());
- $this->assertSame(1, (int) ($notification?->payload['count'] ?? 0));
- }
-
- public function test_it_groups_recent_photo_activity_notifications(): void
- {
- Carbon::setTestNow('2026-01-12 13:48:01');
-
- $event = Event::factory()->create();
- $listener = $this->app->make(SendPhotoUploadedNotification::class);
-
- GuestNotification::factory()->create([
- 'tenant_id' => $event->tenant_id,
- 'event_id' => $event->id,
- 'type' => GuestNotificationType::PHOTO_ACTIVITY,
- 'title' => 'Fotospiel-Test hat gerade ein Foto gemacht 🎉',
- 'payload' => [
- 'photo_id' => 122,
- 'photo_ids' => [122],
- 'count' => 1,
- ],
- 'created_at' => now()->subMinutes(5),
- 'updated_at' => now()->subMinutes(5),
- ]);
-
- $listener->handle(new GuestPhotoUploaded(
- $event,
- 123,
- 'device-123',
- 'Fotospiel-Test'
- ));
-
- $this->assertSame(1, GuestNotification::query()
- ->where('event_id', $event->id)
- ->where('type', GuestNotificationType::PHOTO_ACTIVITY)
- ->count());
-
- $notification = GuestNotification::query()
- ->where('event_id', $event->id)
- ->where('type', GuestNotificationType::PHOTO_ACTIVITY)
- ->first();
-
- $this->assertSame('Es gibt 2 neue Fotos!', $notification?->title);
- $this->assertSame(2, (int) ($notification?->payload['count'] ?? 0));
-
- $this->assertSame(1, GuestNotificationReceipt::query()
- ->where('guest_identifier', 'device-123')
- ->where('status', 'read')
- ->count());
- }
-
- public function test_it_creates_notification_outside_group_window(): void
- {
- Carbon::setTestNow('2026-01-12 13:48:01');
-
- $event = Event::factory()->create();
- $listener = $this->app->make(SendPhotoUploadedNotification::class);
-
- GuestNotification::factory()->create([
- 'tenant_id' => $event->tenant_id,
- 'event_id' => $event->id,
- 'type' => GuestNotificationType::PHOTO_ACTIVITY,
- 'title' => 'Fotospiel-Test hat gerade ein Foto gemacht 🎉',
- 'payload' => [
- 'photo_id' => 122,
- 'photo_ids' => [122],
- 'count' => 1,
- ],
- 'created_at' => now()->subMinutes(20),
- 'updated_at' => now()->subMinutes(20),
- ]);
-
- $listener->handle(new GuestPhotoUploaded(
- $event,
- 123,
- 'device-123',
- 'Fotospiel-Test'
- ));
-
- $this->assertSame(2, GuestNotification::query()
- ->where('event_id', $event->id)
- ->where('type', GuestNotificationType::PHOTO_ACTIVITY)
- ->count());
-
- $this->assertSame(1, GuestNotificationReceipt::query()
- ->where('guest_identifier', 'device-123')
- ->where('status', 'read')
- ->count());
- }
-}