Gate Events-linked joins behind registration and badge check-in.
Deploy Ladill Meet / deploy (push) Successful in 43s
Deploy Ladill Meet / deploy (push) Successful in 43s
Attendees register or sign in with a badge before the display-name screen; email is no longer collected on the Meet join form. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,11 +6,13 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Participant;
|
||||
use App\Models\Room;
|
||||
use App\Models\Session;
|
||||
use App\Services\Integrations\EventsClient;
|
||||
use App\Services\Meet\ConferenceService;
|
||||
use App\Services\Meet\InvitationService;
|
||||
use App\Services\Meet\SecurityPolicyService;
|
||||
use App\Services\Meet\SessionService;
|
||||
use App\Services\Meet\SpaceService;
|
||||
use App\Support\EventsRegistrationSession;
|
||||
use App\Support\EventsSourceLink;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -25,6 +27,7 @@ class JoinController extends Controller
|
||||
protected SecurityPolicyService $security,
|
||||
protected SpaceService $spaces,
|
||||
protected ConferenceService $conferences,
|
||||
protected EventsClient $events,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, Room $room): View|RedirectResponse
|
||||
@@ -74,11 +77,38 @@ class JoinController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return view('meet.join.show', [
|
||||
'room' => $room,
|
||||
'organization' => $room->organization,
|
||||
'user' => $request->user(),
|
||||
'isWebinar' => $room->isWebinar(),
|
||||
$badge = trim((string) $request->query('badge', ''));
|
||||
if ($badge !== '' && $this->requiresEventsRegistration($room, $request)) {
|
||||
if ($this->verifyAndStoreBadge($request, $room, $badge)) {
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
return $this->gateView($request, $room)->withErrors([
|
||||
'badge_code' => 'Badge code not found or registration is not confirmed.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->requiresEventsRegistration($room, $request)) {
|
||||
return $this->gateView($request, $room);
|
||||
}
|
||||
|
||||
return $this->displayNameView($request, $room);
|
||||
}
|
||||
|
||||
public function verifyBadge(Request $request, Room $room): RedirectResponse
|
||||
{
|
||||
if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) {
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
$request->validate(['badge_code' => ['required', 'string', 'max:16']]);
|
||||
|
||||
if ($this->verifyAndStoreBadge($request, $room, $request->input('badge_code'))) {
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'badge_code' => 'Badge code not found or registration is not confirmed.',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -101,6 +131,10 @@ class JoinController extends Controller
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
if ($this->requiresEventsRegistration($room, $request)) {
|
||||
return redirect()->route('meet.join', $room);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'email' => ['nullable', 'email'],
|
||||
];
|
||||
@@ -118,10 +152,11 @@ class JoinController extends Controller
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$displayName = $user?->name ?? ($validated['display_name'] ?? 'Guest');
|
||||
$email = $user?->email ?? ($validated['email'] ?? null);
|
||||
$registration = EventsRegistrationSession::get($request, $room);
|
||||
$displayName = $user?->name ?? ($validated['display_name'] ?? ($registration['name'] ?? 'Guest'));
|
||||
$email = $user?->email ?? ($registration['email'] ?? null);
|
||||
|
||||
$kind = $room->isConference() ? 'conference' : 'meeting';
|
||||
$kind = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting');
|
||||
|
||||
[$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local');
|
||||
if (! $allowed && $reason !== 'passcode_required') {
|
||||
@@ -164,30 +199,9 @@ class JoinController extends Controller
|
||||
$role = $this->spaces->resolveJoinRole($room, $user, $role);
|
||||
} elseif ($room->isConference() && $role !== 'host') {
|
||||
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
|
||||
|
||||
if ($role === 'attendee' && $room->isEventsLinked()) {
|
||||
$joinEmail = $email ?? $user?->email;
|
||||
if (! $room->hasEventsRegistrationInvite($joinEmail)) {
|
||||
$registrationUrl = EventsSourceLink::eventRegistrationUrl($room);
|
||||
|
||||
return $registrationUrl
|
||||
? redirect()->away($registrationUrl)
|
||||
: back()->withErrors(['join' => 'Register for this conference in Ladill Events before joining.']);
|
||||
}
|
||||
}
|
||||
} elseif ($room->isWebinar() && $role !== 'host') {
|
||||
if ($room->isEventsLinked()) {
|
||||
$joinEmail = $email ?? $user?->email;
|
||||
if (! $room->hasEventsRegistrationInvite($joinEmail)) {
|
||||
$registrationUrl = EventsSourceLink::eventRegistrationUrl($room);
|
||||
|
||||
return $registrationUrl
|
||||
? redirect()->away($registrationUrl)
|
||||
: back()->withErrors(['join' => 'Register for this webinar in Ladill Events before joining.']);
|
||||
}
|
||||
|
||||
$role = 'attendee';
|
||||
$displayName = $displayName ?? $user?->name;
|
||||
} else {
|
||||
return back()->withErrors([
|
||||
'join' => 'This webinar must be linked to a Ladill Events event before attendees can register and join.',
|
||||
@@ -301,4 +315,79 @@ class JoinController extends Controller
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function isRoomHost(Request $request, Room $room): bool
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return $user && $user->ownerRef() === $room->host_user_ref;
|
||||
}
|
||||
|
||||
protected function requiresEventsRegistration(Room $room, Request $request): bool
|
||||
{
|
||||
if (! $room->isEventsLinked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isRoomHost($request, $room)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $room->isWebinar() && ! $room->isConference()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! EventsRegistrationSession::isVerified($request, $room);
|
||||
}
|
||||
|
||||
protected function verifyAndStoreBadge(Request $request, Room $room, string $badgeCode): bool
|
||||
{
|
||||
$eventId = EventsSourceLink::eventId($room);
|
||||
if (! $eventId || ! $this->events->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$registration = $this->events->verifyRegistration($eventId, $room->owner_ref, $badgeCode);
|
||||
if (! $registration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EventsRegistrationSession::store($request, $room, [
|
||||
'email' => $registration['attendee_email'] ?? null,
|
||||
'name' => $registration['attendee_name'] ?? null,
|
||||
'badge_code' => $registration['badge_code'] ?? $badgeCode,
|
||||
]);
|
||||
|
||||
return EventsRegistrationSession::isVerified($request, $room);
|
||||
}
|
||||
|
||||
protected function gateView(Request $request, Room $room): View
|
||||
{
|
||||
$meetReturn = EventsSourceLink::meetReturnUrl($room);
|
||||
$registrationUrl = EventsSourceLink::eventRegistrationUrl($room, $meetReturn);
|
||||
$sessionNoun = $room->isConference() ? 'conference' : 'webinar';
|
||||
|
||||
return view('meet.join.gate', [
|
||||
'room' => $room,
|
||||
'organization' => $room->organization,
|
||||
'eventTitle' => EventsSourceLink::eventTitle($room),
|
||||
'registrationUrl' => $registrationUrl,
|
||||
'sessionNoun' => $sessionNoun,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function displayNameView(Request $request, Room $room): View
|
||||
{
|
||||
$registration = EventsRegistrationSession::get($request, $room);
|
||||
$sessionNoun = $room->isConference() ? 'conference' : ($room->isWebinar() ? 'webinar' : 'meeting');
|
||||
|
||||
return view('meet.join.show', [
|
||||
'room' => $room,
|
||||
'organization' => $room->organization,
|
||||
'user' => $request->user(),
|
||||
'isWebinar' => $room->isWebinar(),
|
||||
'sessionNoun' => $sessionNoun,
|
||||
'defaultDisplayName' => $registration['name'] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,31 @@ class EventsClient
|
||||
return (array) ($response['event'] ?? []);
|
||||
}
|
||||
|
||||
/** @return array<string, mixed>|null */
|
||||
public function verifyRegistration(int $eventId, string $ownerRef, string $badgeCode): ?array
|
||||
{
|
||||
try {
|
||||
$response = $this->client()->post("events/{$eventId}/verify-registration", [
|
||||
'owner_ref' => $ownerRef,
|
||||
'badge_code' => $badgeCode,
|
||||
]);
|
||||
} catch (ConnectionException) {
|
||||
throw new \RuntimeException('Could not reach Ladill Events.');
|
||||
}
|
||||
|
||||
if ($response->status() === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new \RuntimeException('Events service error: '.($response->json('message') ?? $response->body()));
|
||||
}
|
||||
|
||||
$registration = $response->json('registration');
|
||||
|
||||
return is_array($registration) ? $registration : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Room;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EventsRegistrationSession
|
||||
{
|
||||
private static function key(Room $room): string
|
||||
{
|
||||
return "meet.events.registration.{$room->uuid}";
|
||||
}
|
||||
|
||||
/** @return array{email: ?string, name: ?string, badge_code: ?string}|null */
|
||||
public static function get(Request $request, Room $room): ?array
|
||||
{
|
||||
$data = $request->session()->get(self::key($room));
|
||||
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
public static function isVerified(Request $request, Room $room): bool
|
||||
{
|
||||
$data = self::get($request, $room);
|
||||
|
||||
return filled($data['badge_code'] ?? null) && filled($data['email'] ?? null);
|
||||
}
|
||||
|
||||
/** @param array{email?: ?string, name?: ?string, badge_code?: ?string} $data */
|
||||
public static function store(Request $request, Room $room, array $data): void
|
||||
{
|
||||
$request->session()->put(self::key($room), [
|
||||
'email' => $data['email'] ?? null,
|
||||
'name' => $data['name'] ?? null,
|
||||
'badge_code' => $data['badge_code'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function forget(Request $request, Room $room): void
|
||||
{
|
||||
$request->session()->forget(self::key($room));
|
||||
}
|
||||
}
|
||||
@@ -51,18 +51,32 @@ class EventsSourceLink
|
||||
return $eventId ? self::baseUrl().'/events/'.$eventId : null;
|
||||
}
|
||||
|
||||
public static function eventRegistrationUrl(Room $room): ?string
|
||||
public static function meetReturnUrl(Room $room): string
|
||||
{
|
||||
return route('meet.join', $room, absolute: true);
|
||||
}
|
||||
|
||||
public static function eventRegistrationUrl(Room $room, ?string $meetReturn = null): ?string
|
||||
{
|
||||
$source = (array) ($room->source ?? []);
|
||||
$stored = trim((string) ($source['registration_url'] ?? ''));
|
||||
|
||||
if ($stored !== '') {
|
||||
return $stored;
|
||||
$url = $stored !== ''
|
||||
? $stored
|
||||
: (($eventId = self::eventId($room))
|
||||
? self::baseUrl().'/q/'.($source['short_code'] ?? '')
|
||||
: null);
|
||||
|
||||
if ($url === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$eventId = self::eventId($room);
|
||||
$meetReturn = trim((string) ($meetReturn ?? ''));
|
||||
if ($meetReturn === '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
return $eventId ? self::baseUrl().'/q/'.($source['short_code'] ?? '') : null;
|
||||
return $url.(str_contains($url, '?') ? '&' : '?').'meet_return='.urlencode($meetReturn);
|
||||
}
|
||||
|
||||
public static function eventQrUrl(Room $room): ?string
|
||||
|
||||
@@ -71,5 +71,72 @@ Alpine.data('leaveFeedback', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.data('meetBadgeScanner', () => ({
|
||||
scanning: false,
|
||||
scanError: '',
|
||||
scanner: null,
|
||||
|
||||
toggle() {
|
||||
if (this.scanning) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanning = true;
|
||||
this.scanError = '';
|
||||
this.$nextTick(() => this.start());
|
||||
},
|
||||
|
||||
start() {
|
||||
if (typeof Html5Qrcode === 'undefined') {
|
||||
this.scanError = 'QR scanner could not load. Enter your badge code manually.';
|
||||
this.scanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const el = document.getElementById('meet-badge-qr-reader');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanner = new Html5Qrcode('meet-badge-qr-reader');
|
||||
this.scanner.start(
|
||||
{ facingMode: 'environment' },
|
||||
{ fps: 10, qrbox: { width: 220, height: 220 } },
|
||||
(decoded) => {
|
||||
const code = (decoded || '').trim().toUpperCase();
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
const input = document.querySelector('input[name="badge_code"]');
|
||||
if (input) {
|
||||
input.value = code;
|
||||
}
|
||||
|
||||
input?.closest('form')?.requestSubmit();
|
||||
},
|
||||
() => {},
|
||||
).catch(() => {
|
||||
this.scanError = 'Camera access was denied or unavailable.';
|
||||
this.scanning = false;
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (!this.scanner) {
|
||||
this.scanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanner.stop().catch(() => {}).finally(() => {
|
||||
this.scanner.clear().catch(() => {});
|
||||
this.scanner = null;
|
||||
this.scanning = false;
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
window.Alpine = Alpine;
|
||||
Alpine.start();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<x-meet-kiosk-layout :title="'Register · '.$room->title" :organization="$organization">
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-lg">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">{{ $room->title }}</h1>
|
||||
|
||||
@if ($eventTitle)
|
||||
<p class="mt-2 text-sm font-medium text-indigo-600">{{ $eventTitle }}</p>
|
||||
@endif
|
||||
|
||||
@if ($room->scheduled_at)
|
||||
<p class="mt-3 text-base text-slate-500">
|
||||
{{ $room->scheduled_at->timezone($room->timezone)->format('l, j F Y · g:i A T') }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<p class="mt-6 text-sm leading-relaxed text-slate-600">
|
||||
Register for this event before joining the {{ $sessionNoun }}. Paid tickets must be completed before you can enter.
|
||||
</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mt-6 rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-left text-sm text-red-700">
|
||||
{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($registrationUrl)
|
||||
<a href="{{ $registrationUrl }}"
|
||||
class="btn-primary btn-primary-lg mt-8 inline-flex w-full items-center justify-center py-4 text-base font-semibold">
|
||||
Register for event
|
||||
</a>
|
||||
@else
|
||||
<p class="mt-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
Event registration is not available. Contact the organizer.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="relative my-10">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-slate-200"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="bg-slate-100 px-3 text-sm font-medium text-slate-500">Already registered?</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('meet.join.verify-badge', $room) }}" class="text-left">
|
||||
@csrf
|
||||
|
||||
<label class="block text-sm font-medium text-slate-700">Badge code</label>
|
||||
<input type="text" name="badge_code" required placeholder="Enter your badge code"
|
||||
value="{{ old('badge_code') }}"
|
||||
autocomplete="off"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 font-mono uppercase tracking-widest text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:normal-case placeholder:tracking-normal placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
|
||||
<button type="submit" class="btn-primary mt-4 w-full py-3.5 text-sm font-semibold">
|
||||
Sign in with badge
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6" x-data="meetBadgeScanner" x-cloak>
|
||||
<button type="button" @click="toggle()"
|
||||
class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50">
|
||||
<span x-text="scanning ? 'Stop scanning' : 'Scan badge QR code'"></span>
|
||||
</button>
|
||||
|
||||
<div x-show="scanning" class="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-black">
|
||||
<div id="meet-badge-qr-reader" class="w-full"></div>
|
||||
</div>
|
||||
|
||||
<p x-show="scanError" x-text="scanError" class="mt-3 text-left text-sm text-red-600"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('head')
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
|
||||
@endpush
|
||||
</x-meet-kiosk-layout>
|
||||
@@ -1,7 +1,4 @@
|
||||
<x-meet-kiosk-layout :title="'Join · '.$room->title" :organization="$organization">
|
||||
@php
|
||||
$sessionNoun = $room->isConference() ? 'conference' : 'meeting';
|
||||
@endphp
|
||||
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||
<div class="w-full max-w-lg">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">{{ $room->title }}</h1>
|
||||
@@ -32,15 +29,8 @@
|
||||
@guest
|
||||
<label class="block text-sm font-medium text-slate-700">Your name</label>
|
||||
<input type="text" name="display_name" required placeholder="How should we show your name?"
|
||||
value="{{ old('display_name') }}"
|
||||
value="{{ old('display_name', $defaultDisplayName ?? '') }}"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
|
||||
@if ($isWebinar ?? false)
|
||||
<label class="mt-4 block text-sm font-medium text-slate-700">Email</label>
|
||||
<input type="email" name="email" required placeholder="you@example.com"
|
||||
value="{{ old('email') }}"
|
||||
class="mt-2 w-full rounded-2xl border-slate-200 bg-white px-4 py-3.5 text-slate-900 shadow-sm ring-1 ring-slate-200 placeholder:text-slate-400 focus:border-indigo-500 focus:ring-indigo-500">
|
||||
@endif
|
||||
@endguest
|
||||
|
||||
@auth
|
||||
@@ -50,11 +40,11 @@
|
||||
@endauth
|
||||
|
||||
@if ($room->setting('waiting_room', true) && ! ($user && $user->ownerRef() === $room->host_user_ref))
|
||||
<p class="mt-4 text-xs text-slate-500">The host will admit you when the {{ $sessionNoun }} is ready.</p>
|
||||
<p class="mt-4 text-xs text-slate-500">The host will admit you when the {{ $sessionNoun ?? 'session' }} is ready.</p>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="btn-primary btn-primary-lg mt-8 w-full py-4 text-base font-semibold">
|
||||
Join {{ $sessionNoun }}
|
||||
Join {{ $sessionNoun ?? 'session' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ Route::get('/r/{room}', [JoinController::class, 'show'])->name('meet.join');
|
||||
Route::get('/r/{room}/waiting', [JoinController::class, 'waiting'])->name('meet.join.waiting');
|
||||
Route::get('/r/{room}/waiting/status', [JoinController::class, 'waitingStatus'])->name('meet.join.waiting.status');
|
||||
Route::post('/r/{room}/passcode', [JoinController::class, 'verifyPasscode'])->name('meet.join.passcode');
|
||||
Route::post('/r/{room}/verify-badge', [JoinController::class, 'verifyBadge'])->name('meet.join.verify-badge');
|
||||
Route::post('/r/{room}/enter', [JoinController::class, 'enter'])->name('meet.join.enter');
|
||||
|
||||
Route::get('/room/{session}/left', [LeaveController::class, 'show'])->name('meet.left');
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Room;
|
||||
use App\Models\Session;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventsJoinGateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Organization $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||
|
||||
config([
|
||||
'meet.events_api_url' => 'https://events.test/api/service/v1',
|
||||
'meet.events_api_key' => 'test-events-key',
|
||||
]);
|
||||
|
||||
$this->user = User::create([
|
||||
'public_id' => 'host-events-001',
|
||||
'name' => 'Event Host',
|
||||
'email' => 'host@example.com',
|
||||
]);
|
||||
|
||||
$this->organization = Organization::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'name' => 'Events Org',
|
||||
'slug' => 'events-org',
|
||||
'timezone' => 'UTC',
|
||||
'settings' => ['onboarded' => true],
|
||||
]);
|
||||
|
||||
Member::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$base = rtrim((string) config('billing.api_url'), '/');
|
||||
Http::fake([
|
||||
$base.'/can-afford*' => Http::response(['affordable' => true]),
|
||||
$base.'/debit' => Http::response(['ok' => true]),
|
||||
$base.'/balance*' => Http::response(['balance_minor' => 100000]),
|
||||
'https://events.test/api/service/v1/events/*/verify-registration' => Http::response([
|
||||
'registration' => [
|
||||
'attendee_name' => 'Registered Guest',
|
||||
'attendee_email' => 'guest@example.com',
|
||||
'badge_code' => 'BADGE123',
|
||||
],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_events_linked_webinar_shows_registration_gate_for_guest(): void
|
||||
{
|
||||
$room = $this->createEventsLinkedRoom('webinar');
|
||||
|
||||
$this->get('/r/'.$room->uuid)
|
||||
->assertOk()
|
||||
->assertSee('Register for event', false)
|
||||
->assertSee('Already registered?', false)
|
||||
->assertDontSee('How should we show your name?', false);
|
||||
}
|
||||
|
||||
public function test_badge_verification_leads_to_display_name_screen(): void
|
||||
{
|
||||
$room = $this->createEventsLinkedRoom('webinar');
|
||||
|
||||
$this->post('/r/'.$room->uuid.'/verify-badge', ['badge_code' => 'BADGE123'])
|
||||
->assertRedirect(route('meet.join', $room));
|
||||
|
||||
$this->get('/r/'.$room->uuid)
|
||||
->assertOk()
|
||||
->assertSee('How should we show your name?', false)
|
||||
->assertSee('value="Registered Guest"', false)
|
||||
->assertDontSee('Register for event', false);
|
||||
}
|
||||
|
||||
public function test_verified_guest_can_join_live_webinar_with_display_name_only(): void
|
||||
{
|
||||
$room = $this->createEventsLinkedRoom('webinar', [
|
||||
'settings' => array_merge(config('meet.default_settings'), [
|
||||
'join_before_host' => true,
|
||||
'waiting_room' => false,
|
||||
]),
|
||||
]);
|
||||
|
||||
$session = Session::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'room_id' => $room->id,
|
||||
'media_room_name' => 'room-'.$room->uuid,
|
||||
'status' => 'live',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
$room->update(['status' => 'live']);
|
||||
|
||||
$this->post('/r/'.$room->uuid.'/verify-badge', ['badge_code' => 'BADGE123'])
|
||||
->assertRedirect(route('meet.join', $room));
|
||||
|
||||
$this->post('/r/'.$room->uuid.'/enter', [
|
||||
'display_name' => 'Registered Guest',
|
||||
])->assertRedirect(route('meet.room', $session));
|
||||
|
||||
$this->assertDatabaseHas('meet_participants', [
|
||||
'session_id' => $session->id,
|
||||
'display_name' => 'Registered Guest',
|
||||
'email' => 'guest@example.com',
|
||||
'role' => 'attendee',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_badge_query_param_auto_verifies_on_join_url(): void
|
||||
{
|
||||
$room = $this->createEventsLinkedRoom('webinar');
|
||||
|
||||
$this->get('/r/'.$room->uuid.'?badge=BADGE123')
|
||||
->assertRedirect(route('meet.join', $room));
|
||||
|
||||
$this->get('/r/'.$room->uuid)
|
||||
->assertOk()
|
||||
->assertSee('How should we show your name?', false);
|
||||
}
|
||||
|
||||
public function test_host_skips_registration_gate(): void
|
||||
{
|
||||
$room = $this->createEventsLinkedRoom('webinar');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get('/r/'.$room->uuid)
|
||||
->assertOk()
|
||||
->assertSee('Joining as', false)
|
||||
->assertDontSee('Register for event', false);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $overrides */
|
||||
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
|
||||
{
|
||||
return Room::create(array_merge([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'host_user_ref' => $this->user->public_id,
|
||||
'title' => 'Linked session',
|
||||
'type' => $type,
|
||||
'status' => 'scheduled',
|
||||
'scheduled_at' => now()->addDay(),
|
||||
'timezone' => 'UTC',
|
||||
'settings' => config('meet.default_settings'),
|
||||
'source' => [
|
||||
'app' => 'events',
|
||||
'entity_type' => 'virtual_session',
|
||||
'entity_id' => 'vs-test',
|
||||
'event_id' => '42',
|
||||
'event_title' => 'Summit 2026',
|
||||
'short_code' => 'summit26',
|
||||
'registration_url' => 'https://ladl.link/summit26',
|
||||
],
|
||||
], $overrides));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user