After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
148 lines
4.6 KiB
PHP
148 lines
4.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\App;
|
|
|
|
use App\Enums\SocialAccount\Platform as SocialPlatform;
|
|
use App\Enums\User\Persona;
|
|
use App\Enums\User\Setup;
|
|
use App\Http\Requests\App\Onboarding\StoreBrandRequest;
|
|
use App\Models\User;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class OnboardingController extends Controller
|
|
{
|
|
public function role(Request $request): Response|RedirectResponse
|
|
{
|
|
$redirect = $this->enforceStep($request->user(), Setup::Role);
|
|
if ($redirect) {
|
|
return $redirect;
|
|
}
|
|
|
|
return Inertia::render('onboarding/Role', [
|
|
'personas' => Persona::toSelectArray(),
|
|
]);
|
|
}
|
|
|
|
public function storeRole(Request $request): RedirectResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'persona' => ['required', Rule::enum(Persona::class)],
|
|
]);
|
|
|
|
$request->user()->update([
|
|
'persona' => data_get($validated, 'persona'),
|
|
'setup' => Setup::Brand,
|
|
]);
|
|
|
|
return redirect()->route('app.onboarding.brand');
|
|
}
|
|
|
|
public function brand(Request $request): Response|RedirectResponse
|
|
{
|
|
$redirect = $this->enforceStep($request->user(), Setup::Brand);
|
|
if ($redirect) {
|
|
return $redirect;
|
|
}
|
|
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
return Inertia::render('onboarding/Brand', [
|
|
'workspace' => [
|
|
'name' => $workspace?->name ?? '',
|
|
'brand_website' => $workspace?->brand_website ?? '',
|
|
'brand_description' => $workspace?->brand_description ?? '',
|
|
'brand_tone' => $workspace?->brand_tone ?? 'professional',
|
|
'brand_voice_notes' => $workspace?->brand_voice_notes ?? '',
|
|
'content_language' => $workspace?->content_language ?? 'en',
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function storeBrand(StoreBrandRequest $request): RedirectResponse
|
|
{
|
|
$workspace = $request->user()->currentWorkspace;
|
|
|
|
if ($workspace) {
|
|
$workspace->update($request->validated());
|
|
}
|
|
|
|
$request->user()->update(['setup' => Setup::Connections]);
|
|
|
|
return redirect()->route('app.onboarding.account');
|
|
}
|
|
|
|
public function skipBrand(Request $request): RedirectResponse
|
|
{
|
|
$request->user()->update(['setup' => Setup::Connections]);
|
|
|
|
return redirect()->route('app.onboarding.account');
|
|
}
|
|
|
|
public function account(Request $request): Response|RedirectResponse
|
|
{
|
|
$redirect = $this->enforceStep($request->user(), Setup::Connections);
|
|
if ($redirect) {
|
|
return $redirect;
|
|
}
|
|
|
|
$user = $request->user();
|
|
$workspace = $user->currentWorkspace;
|
|
|
|
$platforms = collect();
|
|
|
|
if ($workspace) {
|
|
$connectedAccounts = $workspace->socialAccounts;
|
|
|
|
$platforms = collect(SocialPlatform::enabled())->map(fn ($platform) => [
|
|
'value' => $platform->value,
|
|
'label' => $platform->label(),
|
|
'color' => $platform->color(),
|
|
'connected' => $connectedAccounts->firstWhere('platform', $platform) !== null,
|
|
'account' => $connectedAccounts->firstWhere('platform', $platform),
|
|
])->values();
|
|
}
|
|
|
|
return Inertia::render('onboarding/Account', [
|
|
'platforms' => $platforms,
|
|
'hasWorkspace' => $workspace !== null,
|
|
]);
|
|
}
|
|
|
|
public function storeAccount(Request $request): RedirectResponse
|
|
{
|
|
$request->user()->update(['setup' => Setup::Completed]);
|
|
|
|
if (config('trypost.self_hosted')) {
|
|
session()->flash('flash.banner', __('auth.flash.welcome'));
|
|
session()->flash('flash.bannerStyle', 'success');
|
|
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
return redirect()->route('app.subscribe');
|
|
}
|
|
|
|
private function enforceStep(User $user, Setup $expectedStep): ?RedirectResponse
|
|
{
|
|
if ($user->setup === $expectedStep) {
|
|
return null;
|
|
}
|
|
|
|
if ($user->setup === Setup::Completed) {
|
|
return redirect()->route('app.calendar');
|
|
}
|
|
|
|
return match ($user->setup) {
|
|
Setup::Role => redirect()->route('app.onboarding.role'),
|
|
Setup::Brand => redirect()->route('app.onboarding.brand'),
|
|
Setup::Connections => redirect()->route('app.onboarding.account'),
|
|
default => redirect()->route('app.onboarding.role'),
|
|
};
|
|
}
|
|
}
|