- Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env

hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads,
attach packages, and surface localized success/error states.
- Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/
PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent
creation, webhooks, and the wizard CTA.
- Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/
useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages,
Checkout) with localized copy and experiment tracking.
- Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing
localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations.
- Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke
test for the hero CTA while reconciling outstanding checklist items.
This commit is contained in:
Codex Agent
2025-10-19 11:41:03 +02:00
parent ae9b9160ac
commit a949c8d3af
113 changed files with 5169 additions and 712 deletions

View File

@@ -0,0 +1,211 @@
<?php
namespace App\Http\Controllers;
use App\Mail\Welcome;
use App\Models\Package;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;
class CheckoutGoogleController extends Controller
{
private const SESSION_KEY = 'checkout_google_payload';
public function redirect(Request $request): RedirectResponse
{
$validated = $request->validate([
'package_id' => ['required', 'exists:packages,id'],
'locale' => ['nullable', 'string'],
]);
$payload = [
'package_id' => (int) $validated['package_id'],
'locale' => $validated['locale'] ?? app()->getLocale(),
];
$request->session()->put(self::SESSION_KEY, $payload);
$request->session()->put('selected_package_id', $payload['package_id']);
return Socialite::driver('google')
->scopes(['email', 'profile'])
->with(['prompt' => 'select_account'])
->redirect();
}
public function callback(Request $request): RedirectResponse
{
$payload = $request->session()->get(self::SESSION_KEY, []);
$packageId = $payload['package_id'] ?? null;
try {
$googleUser = Socialite::driver('google')->user();
} catch (\Throwable $e) {
Log::warning('Google checkout login failed', ['message' => $e->getMessage()]);
$this->flashError($request, __('checkout.google_error_fallback'));
return $this->redirectBackToWizard($packageId);
}
$email = $googleUser->getEmail();
if (! $email) {
$this->flashError($request, __('checkout.google_missing_email'));
return $this->redirectBackToWizard($packageId);
}
$user = DB::transaction(function () use ($googleUser, $email) {
$existing = User::where('email', $email)->first();
if ($existing) {
$existing->forceFill([
'name' => $googleUser->getName() ?: $existing->name,
'pending_purchase' => true,
'email_verified_at' => $existing->email_verified_at ?? now(),
])->save();
if (! $existing->tenant) {
$this->createTenantForUser($existing, $googleUser->getName(), $email);
}
return $existing->fresh();
}
$user = User::create([
'name' => $googleUser->getName(),
'email' => $email,
'password' => Hash::make(Str::random(32)),
'pending_purchase' => true,
'email_verified_at' => now(),
]);
event(new Registered($user));
$tenant = $this->createTenantForUser($user, $googleUser->getName(), $email);
try {
Mail::to($user)->queue(new Welcome($user));
} catch (\Throwable $exception) {
Log::warning('Failed to queue welcome mail after Google signup', [
'user_id' => $user->id,
'error' => $exception->getMessage(),
]);
}
return tap($user)->setRelation('tenant', $tenant);
});
if (! $user->tenant) {
$this->createTenantForUser($user, $googleUser->getName(), $email);
}
Auth::login($user, true);
$request->session()->regenerate();
$request->session()->forget(self::SESSION_KEY);
$request->session()->put('checkout_google_status', 'success');
if ($packageId) {
$this->ensurePackageAttached($user, (int) $packageId);
}
return $this->redirectBackToWizard($packageId);
}
private function createTenantForUser(User $user, ?string $displayName, string $email): Tenant
{
$tenantName = trim($displayName ?: Str::before($email, '@')) ?: 'Fotospiel Tenant';
$slugBase = Str::slug($tenantName) ?: 'tenant';
$slug = $slugBase;
$counter = 1;
while (Tenant::where('slug', $slug)->exists()) {
$slug = $slugBase . '-' . $counter;
$counter++;
}
$tenant = Tenant::create([
'user_id' => $user->id,
'name' => $tenantName,
'slug' => $slug,
'email' => $email,
'contact_email' => $email,
'is_active' => true,
'is_suspended' => false,
'event_credits_balance' => 0,
'subscription_tier' => 'free',
'subscription_status' => 'free',
'subscription_expires_at' => null,
'settings' => json_encode([
'branding' => [
'logo_url' => null,
'primary_color' => '#3B82F6',
'secondary_color' => '#1F2937',
'font_family' => 'Inter, sans-serif',
],
'features' => [
'photo_likes_enabled' => false,
'event_checklist' => false,
'custom_domain' => false,
'advanced_analytics' => false,
],
'custom_domain' => null,
'contact_email' => $email,
'event_default_type' => 'general',
]),
]);
$user->forceFill(['tenant_id' => $tenant->id])->save();
return $tenant;
}
private function ensurePackageAttached(User $user, int $packageId): void
{
$tenant = $user->tenant;
if (! $tenant) {
return;
}
$package = Package::find($packageId);
if (! $package) {
return;
}
if ($tenant->packages()->where('package_id', $packageId)->exists()) {
return;
}
$tenant->packages()->attach($packageId, [
'price' => $package->price,
'purchased_at' => now(),
'expires_at' => now()->addYear(),
'active' => $package->price <= 0,
]);
}
private function redirectBackToWizard(?int $packageId): RedirectResponse
{
if ($packageId) {
return redirect()->route('purchase.wizard', ['package' => $packageId]);
}
$firstPackageId = Package::query()->orderBy('price')->value('id');
if ($firstPackageId) {
return redirect()->route('purchase.wizard', ['package' => $firstPackageId]);
}
return redirect()->route('packages');
}
private function flashError(Request $request, string $message): void
{
$request->session()->flash('checkout_google_error', $message);
}
}