From b7374c1adb225bb6c38cf4e28343d0c88b6293e9 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 22 Jun 2026 08:18:19 -0300 Subject: [PATCH] feat(onboarding): auto-create a default workspace at signup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateUser now provisions a default "{name}'s Workspace" for non-invite signups (email, Google, GitHub, seeder), with the owner as Admin and current_workspace_id set. Invite signups still skip it (they join the inviter's workspace on acceptance). - UserSeeder drops its now-redundant explicit CreateWorkspace call. - After Stripe checkout, billing/processing lands the user on the social accounts screen with the connect dialog open (was: calendar), so onboarding flows straight into connecting an account. Quantity stays correct (1 workspace → checkout quantity 1). The first workspace is created without brand details; brand setup remains available in Settings and on the create-workspace screen for additional workspaces. --- app/Actions/User/CreateUser.php | 5 +++++ database/seeders/UserSeeder.php | 5 +---- resources/js/pages/billing/Processing.vue | 6 +++--- tests/Feature/Actions/User/CreateUserTest.php | 10 +++++++--- tests/Feature/Auth/RegistrationTest.php | 9 +++++---- tests/Feature/SocialLoginControllerTest.php | 5 +++-- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index f6c0c1eb..ead5eca0 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -4,6 +4,7 @@ namespace App\Actions\User; +use App\Actions\Workspace\CreateWorkspace; use App\Enums\Plan\Slug; use App\Jobs\PostHog\SyncUser; use App\Models\Account; @@ -48,6 +49,10 @@ public static function execute(array $data, array $utmParameters = []): User $account->update(['owner_id' => $user->id]); + if (! $isInviteRegistration) { + CreateWorkspace::execute($user, ['name' => data_get($data, 'name')."'s Workspace"]); + } + return $user; }); diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index a3a28b5d..68c7ca28 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -5,7 +5,6 @@ namespace Database\Seeders; use App\Actions\User\CreateUser; -use App\Actions\Workspace\CreateWorkspace; use App\Models\User; use Illuminate\Database\Seeder; @@ -19,7 +18,7 @@ public function run(): void return; } - $user = CreateUser::execute([ + CreateUser::execute([ 'name' => 'Admin', 'email' => 'admin@trypost.it', 'password' => 'password', @@ -27,8 +26,6 @@ public function run(): void 'timezone' => 'UTC', ]); - CreateWorkspace::execute($user, ['name' => 'My Workspace']); - $this->command->info('Admin account created — change the password on first login:'); $this->command->line(' email: admin@trypost.it'); $this->command->line(' password: password'); diff --git a/resources/js/pages/billing/Processing.vue b/resources/js/pages/billing/Processing.vue index 5220708f..96673402 100644 --- a/resources/js/pages/billing/Processing.vue +++ b/resources/js/pages/billing/Processing.vue @@ -4,7 +4,7 @@ import { IconLoader2 } from '@tabler/icons-vue'; import { onMounted, ref, watch } from 'vue'; import { useTracking } from '@/composables/useTracking'; -import { home } from '@/routes/app'; +import { accounts } from '@/routes/app'; import type { Auth } from '@/types'; const props = defineProps<{ @@ -27,7 +27,7 @@ const { trackPurchase } = useTracking(); const tracked = ref(false); -const goHome = () => router.visit(home.url()); +const goToAccounts = () => router.visit(accounts.url({ query: { openDialog: true } })); // Fires `checkout.completed` exactly once for a real checkout. A trial-with-card // subscription is already `subscribed()` (status `trialing`) by the time the @@ -53,7 +53,7 @@ const completePurchase = () => { } } - goHome(); + goToAccounts(); }; watch( diff --git a/tests/Feature/Actions/User/CreateUserTest.php b/tests/Feature/Actions/User/CreateUserTest.php index 353f47cc..570ea539 100644 --- a/tests/Feature/Actions/User/CreateUserTest.php +++ b/tests/Feature/Actions/User/CreateUserTest.php @@ -8,7 +8,7 @@ use App\Models\Workspace; use Illuminate\Support\Facades\Bus; -test('CreateUser creates user with account but no workspace and no current_workspace_id', function () { +test('CreateUser creates the owner a default workspace and sets it as current', function () { $user = CreateUser::execute([ 'name' => 'Jane Doe', 'email' => 'jane@example.com', @@ -17,10 +17,14 @@ expect($user->name)->toBe('Jane Doe'); expect($user->email)->toBe('jane@example.com'); - expect($user->current_workspace_id)->toBeNull(); expect($user->account_id)->not->toBeNull(); - expect(Workspace::count())->toBe(0); expect(Account::find($user->account_id))->not->toBeNull(); + + $workspace = $user->workspaces()->first(); + expect($user->workspaces()->count())->toBe(1); + expect($workspace->name)->toBe("Jane Doe's Workspace"); + expect($workspace->account_id)->toBe($user->account_id); + expect($user->fresh()->current_workspace_id)->toBe($workspace->id); }); test('CreateUser sets account owner_id to the new user', function () { diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 495791ca..39aae50d 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -24,7 +24,7 @@ $response->assertRedirect(route('register.success', absolute: false)); }); -test('new users do not get a default workspace on registration', function () { +test('new users get a default workspace on registration', function () { $this->post(route('register.store'), [ 'name' => 'Test User', 'email' => 'test@example.com', @@ -35,8 +35,9 @@ expect($user)->not->toBeNull(); expect($user->account_id)->not->toBeNull(); - expect($user->workspaces()->count())->toBe(0); - expect($user->current_workspace_id)->toBeNull(); + expect($user->workspaces()->count())->toBe(1); + expect($user->workspaces()->first()->name)->toBe("Test User's Workspace"); + expect($user->current_workspace_id)->toBe($user->workspaces()->first()->id); }); test('new users can register with a timezone preference', function () { @@ -50,7 +51,7 @@ $user = User::where('email', 'test@example.com')->first(); expect($user)->not->toBeNull(); - expect($user->workspaces()->count())->toBe(0); + expect($user->workspaces()->count())->toBe(1); }); test('new users do not have verified email by default', function () { diff --git a/tests/Feature/SocialLoginControllerTest.php b/tests/Feature/SocialLoginControllerTest.php index b0df0284..9fb08ccd 100644 --- a/tests/Feature/SocialLoginControllerTest.php +++ b/tests/Feature/SocialLoginControllerTest.php @@ -66,8 +66,9 @@ expect($user->name)->toBe('New User'); expect($user->email_verified_at)->not->toBeNull(); expect($user->account_id)->not->toBeNull(); - expect($user->workspaces()->count())->toBe(0); - expect($user->current_workspace_id)->toBeNull(); + expect($user->workspaces()->count())->toBe(1); + expect($user->workspaces()->first()->name)->toBe("New User's Workspace"); + expect($user->current_workspace_id)->toBe($user->workspaces()->first()->id); $this->assertAuthenticatedAs($user); });