From 4e80fd54b7bacbf76af8b47683e746899e5e97d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20S=C3=A9rgio=20Dantas?= Date: Wed, 1 Jul 2026 12:16:15 -0300 Subject: [PATCH 1/3] fix(media): don't 500 when the Unsplash/Giphy key is set to null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `config('services.unsplash.access_key', '')` only falls back to '' when the key is *absent* — if the env var is present but empty, config returns null, and assigning null to the `string` property throws a TypeError, taking the whole request down with a 500 during media search. Cast to `(string)` so a null/empty key degrades gracefully to '' and the service returns an empty result set (as it already does for a missing key) instead of throwing. Covered by a regression test for both services. --- app/Services/GiphyService.php | 2 +- app/Services/UnsplashService.php | 2 +- .../Services/MediaSearchKeyFallbackTest.php | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/Services/MediaSearchKeyFallbackTest.php diff --git a/app/Services/GiphyService.php b/app/Services/GiphyService.php index e60ff667..f5e674d4 100644 --- a/app/Services/GiphyService.php +++ b/app/Services/GiphyService.php @@ -15,7 +15,7 @@ class GiphyService public function __construct() { - $this->apiKey = config('services.giphy.api_key', ''); + $this->apiKey = (string) config('services.giphy.api_key', ''); } /** diff --git a/app/Services/UnsplashService.php b/app/Services/UnsplashService.php index 5ac1ad9a..db16237e 100644 --- a/app/Services/UnsplashService.php +++ b/app/Services/UnsplashService.php @@ -15,7 +15,7 @@ class UnsplashService public function __construct() { - $this->accessKey = config('services.unsplash.access_key', ''); + $this->accessKey = (string) config('services.unsplash.access_key', ''); } /** diff --git a/tests/Unit/Services/MediaSearchKeyFallbackTest.php b/tests/Unit/Services/MediaSearchKeyFallbackTest.php new file mode 100644 index 00000000..6fa85c47 --- /dev/null +++ b/tests/Unit/Services/MediaSearchKeyFallbackTest.php @@ -0,0 +1,22 @@ + null]); + + $result = (new UnsplashService)->search('cats'); + + expect($result)->toBe(['results' => [], 'total' => 0, 'total_pages' => 0]); +}); + +test('giphy service tolerates a null api key without throwing', function () { + config(['services.giphy.api_key' => null]); + + $result = (new GiphyService)->search('cats'); + + expect($result)->toBe(['results' => [], 'total' => 0, 'total_pages' => 0]); +}); From 7dab106f063b2e1e9e0dbfc50e4bdc9c56030e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20S=C3=A9rgio=20Dantas?= Date: Wed, 1 Jul 2026 13:49:48 -0300 Subject: [PATCH 2/3] fix(workspace): default content_language to the app locale, not 'en' When a workspace is created without an explicit content_language, `data_get($data, 'content_language')` returns null, array_filter drops the key, and the column falls back to its DB default of 'en'. So AI content and notifications came out in English even when the app was running in another locale. Default to `app()->getLocale()` instead, so a new workspace inherits the user's current language. An explicit content_language still wins. --- app/Actions/Workspace/CreateWorkspace.php | 2 +- .../Actions/Workspace/CreateWorkspaceTest.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/Actions/Workspace/CreateWorkspace.php b/app/Actions/Workspace/CreateWorkspace.php index 4c42fe67..3c7c2df0 100644 --- a/app/Actions/Workspace/CreateWorkspace.php +++ b/app/Actions/Workspace/CreateWorkspace.php @@ -24,7 +24,7 @@ public static function execute(User $user, array $data): Workspace 'brand_color' => data_get($data, 'brand_color'), 'background_color' => data_get($data, 'background_color'), 'text_color' => data_get($data, 'text_color'), - 'content_language' => data_get($data, 'content_language'), + 'content_language' => data_get($data, 'content_language', app()->getLocale()), ], static fn ($value): bool => $value !== null); $workspace = DB::transaction(function () use ($user, $attributes): Workspace { diff --git a/tests/Feature/Actions/Workspace/CreateWorkspaceTest.php b/tests/Feature/Actions/Workspace/CreateWorkspaceTest.php index a31bf419..7c57efd3 100644 --- a/tests/Feature/Actions/Workspace/CreateWorkspaceTest.php +++ b/tests/Feature/Actions/Workspace/CreateWorkspaceTest.php @@ -42,6 +42,28 @@ expect($member?->pivot->role)->toBe(Role::Admin->value); }); +test('CreateWorkspace inherits the app locale as content_language when none is given', function () { + app()->setLocale('pt-BR'); + + $account = Account::factory()->create(); + $user = User::factory()->create(['account_id' => $account->id]); + + $workspace = CreateWorkspace::execute($user, ['name' => 'Acme']); + + expect($workspace->content_language)->toBe('pt-BR'); +}); + +test('CreateWorkspace keeps an explicit content_language over the app locale', function () { + app()->setLocale('pt-BR'); + + $account = Account::factory()->create(); + $user = User::factory()->create(['account_id' => $account->id]); + + $workspace = CreateWorkspace::execute($user, ['name' => 'Acme', 'content_language' => 'es']); + + expect($workspace->content_language)->toBe('es'); +}); + test('CreateWorkspace ignores unknown extra keys like logo_url', function () { $account = Account::factory()->create(); $user = User::factory()->create(['account_id' => $account->id]); From 974c076e8654a4be58142c5acdebe70d53be7358 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 2 Jul 2026 09:21:31 -0300 Subject: [PATCH 3/3] fix(tracking): fire begin_checkout when onboarding checkout starts trackBeginCheckout was defined but never called, so the begin_checkout (GTM/dataLayer) and checkout.started (PostHog) events never fired. Wire it into the onboarding Connect submit handler, before the redirect to Stripe, and pass the workspace plan from the controller so the event carries plan name + interval. --- .../Controllers/App/OnboardingController.php | 6 ++++++ resources/js/pages/onboarding/Connect.vue | 6 ++++++ .../Onboarding/OnboardingControllerTest.php | 16 ++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/app/Http/Controllers/App/OnboardingController.php b/app/Http/Controllers/App/OnboardingController.php index 80475a77..e78a502f 100644 --- a/app/Http/Controllers/App/OnboardingController.php +++ b/app/Http/Controllers/App/OnboardingController.php @@ -150,9 +150,15 @@ public function connect(Request $request): Response|RedirectResponse 'network' => $platform->network(), ])->values(); + $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); + return Inertia::render('onboarding/Connect', [ 'platforms' => $platforms, 'accounts' => SocialAccountResource::collection($accounts)->resolve(), + 'plan' => [ + 'name' => $plan->name, + 'interval' => 'monthly', + ], ]); } diff --git a/resources/js/pages/onboarding/Connect.vue b/resources/js/pages/onboarding/Connect.vue index 4b9e914a..80ae728a 100644 --- a/resources/js/pages/onboarding/Connect.vue +++ b/resources/js/pages/onboarding/Connect.vue @@ -9,14 +9,18 @@ import NetworkConnectGrid, { type ConnectedAccount, } from '@/components/accounts/NetworkConnectGrid.vue'; import { Button } from '@/components/ui/button'; +import { useTracking } from '@/composables/useTracking'; const props = defineProps<{ platforms: AvailablePlatform[]; accounts: ConnectedAccount[]; + plan: { name: string; interval: string }; }>(); const form = useForm({}); +const { trackBeginCheckout } = useTracking(); + const hasConnected = computed((): boolean => props.accounts.length > 0); const submit = (): void => { @@ -24,6 +28,8 @@ const submit = (): void => { return; } + trackBeginCheckout({ name: props.plan.name, interval: props.plan.interval }); + form.post(checkout.url()); }; diff --git a/tests/Feature/Onboarding/OnboardingControllerTest.php b/tests/Feature/Onboarding/OnboardingControllerTest.php index 5f25e6aa..21c00f74 100644 --- a/tests/Feature/Onboarding/OnboardingControllerTest.php +++ b/tests/Feature/Onboarding/OnboardingControllerTest.php @@ -263,6 +263,22 @@ function subscribeOnboardingAccount(Account $account): void ); }); +test('connect passes the workspace plan so the client can fire begin_checkout', function () { + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + onboardingWorkspace($this->user); + + $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); + + $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('onboarding/Connect') + ->where('plan.name', $plan->name) + ->where('plan.interval', 'monthly') + ); +}); + test('connect offers a single linkedin card and no standalone linkedin page card', function () { $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); onboardingWorkspace($this->user);