From 287c27b79243ae973155dc9ee0cb77e26aacf87a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 16 Apr 2026 11:00:16 -0300 Subject: [PATCH] feat: add brand configuration step to onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/Enums/User/Setup.php | 9 +- .../Controllers/App/OnboardingController.php | 45 ++++- .../App/Onboarding/StoreBrandRequest.php | 26 +++ lang/en/onboarding.php | 8 + lang/es/onboarding.php | 8 + lang/pt-BR/onboarding.php | 8 + resources/js/pages/onboarding/Brand.vue | 154 ++++++++++++++++++ routes/app.php | 3 + tests/Feature/OnboardingControllerTest.php | 119 +++++++++++++- tests/Unit/Enums/UserSetupTest.php | 9 +- 10 files changed, 379 insertions(+), 10 deletions(-) create mode 100644 app/Http/Requests/App/Onboarding/StoreBrandRequest.php create mode 100644 resources/js/pages/onboarding/Brand.vue diff --git a/app/Enums/User/Setup.php b/app/Enums/User/Setup.php index f8bb1e42..c0b1451b 100644 --- a/app/Enums/User/Setup.php +++ b/app/Enums/User/Setup.php @@ -8,6 +8,7 @@ enum Setup: string { case Registering = 'registering'; case Role = 'role'; + case Brand = 'brand'; case Connections = 'connections'; case Subscription = 'subscription'; case Completed = 'completed'; @@ -17,6 +18,7 @@ public function label(): string return match ($this) { self::Registering => 'Registering', self::Role => 'Select Role', + self::Brand => 'Configure Brand', self::Connections => 'Connect Accounts', self::Subscription => 'Start Subscription', self::Completed => 'Completed', @@ -28,9 +30,10 @@ public function stepNumber(): int return match ($this) { self::Registering => 0, self::Role => 1, - self::Connections => 2, - self::Subscription => 3, - self::Completed => 4, + self::Brand => 2, + self::Connections => 3, + self::Subscription => 4, + self::Completed => 5, }; } } diff --git a/app/Http/Controllers/App/OnboardingController.php b/app/Http/Controllers/App/OnboardingController.php index 0653b2a9..18d6457e 100644 --- a/app/Http/Controllers/App/OnboardingController.php +++ b/app/Http/Controllers/App/OnboardingController.php @@ -7,6 +7,7 @@ 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; @@ -36,9 +37,50 @@ public function storeRole(Request $request): RedirectResponse $request->user()->update([ 'persona' => data_get($validated, 'persona'), - 'setup' => Setup::Connections, + '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'); } @@ -98,6 +140,7 @@ private function enforceStep(User $user, Setup $expectedStep): ?RedirectResponse 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'), }; diff --git a/app/Http/Requests/App/Onboarding/StoreBrandRequest.php b/app/Http/Requests/App/Onboarding/StoreBrandRequest.php new file mode 100644 index 00000000..43c9113c --- /dev/null +++ b/app/Http/Requests/App/Onboarding/StoreBrandRequest.php @@ -0,0 +1,26 @@ + ['nullable', 'url', 'max:255'], + 'brand_description' => ['nullable', 'string', 'max:2000'], + 'brand_tone' => ['required', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'], + 'brand_voice_notes' => ['nullable', 'string', 'max:2000'], + 'content_language' => ['required', 'string', 'in:en,pt-BR,es'], + ]; + } +} diff --git a/lang/en/onboarding.php b/lang/en/onboarding.php index aa095997..33898d4a 100644 --- a/lang/en/onboarding.php +++ b/lang/en/onboarding.php @@ -37,6 +37,14 @@ ], ], + 'brand' => [ + 'page_title' => 'Tell us about your brand', + 'title' => 'Tell us about your brand', + 'description' => 'These defaults power every AI-generated post in this workspace. You can change them later in Settings.', + 'submit' => 'Continue', + 'skip' => 'Skip for now', + ], + 'connect' => [ 'page_title' => 'Connect your accounts', 'title' => 'Connect your accounts', diff --git a/lang/es/onboarding.php b/lang/es/onboarding.php index a3795f59..cfb601e1 100644 --- a/lang/es/onboarding.php +++ b/lang/es/onboarding.php @@ -37,6 +37,14 @@ ], ], + 'brand' => [ + 'page_title' => 'Cuéntanos sobre tu marca', + 'title' => 'Cuéntanos sobre tu marca', + 'description' => 'Estos valores predeterminados se usarán en todos los posts generados por IA en este workspace. Puedes cambiarlos después en Configuración.', + 'submit' => 'Continuar', + 'skip' => 'Omitir por ahora', + ], + 'connect' => [ 'page_title' => 'Conecta tus cuentas', 'title' => 'Conecta tus cuentas', diff --git a/lang/pt-BR/onboarding.php b/lang/pt-BR/onboarding.php index 388bbd64..07aab435 100644 --- a/lang/pt-BR/onboarding.php +++ b/lang/pt-BR/onboarding.php @@ -37,6 +37,14 @@ ], ], + 'brand' => [ + 'page_title' => 'Conte sobre sua marca', + 'title' => 'Conte sobre sua marca', + 'description' => 'Esses valores padrão serão usados em todos os posts gerados por AI neste workspace. Você pode alterar depois em Configurações.', + 'submit' => 'Continuar', + 'skip' => 'Pular por enquanto', + ], + 'connect' => [ 'page_title' => 'Conecte suas contas', 'title' => 'Conecte suas contas', diff --git a/resources/js/pages/onboarding/Brand.vue b/resources/js/pages/onboarding/Brand.vue new file mode 100644 index 00000000..4270bf6f --- /dev/null +++ b/resources/js/pages/onboarding/Brand.vue @@ -0,0 +1,154 @@ + + +