feat(onboarding): auto-create a default workspace at signup

- 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.
This commit is contained in:
Paulo Castellano 2026-06-22 08:18:19 -03:00
parent 17449bfe2a
commit b7374c1adb
6 changed files with 24 additions and 16 deletions

View file

@ -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;
});

View file

@ -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');

View file

@ -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(

View file

@ -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 () {

View file

@ -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 () {

View file

@ -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);
});