diff --git a/app/Enums/User/ReferralSource.php b/app/Enums/User/ReferralSource.php new file mode 100644 index 00000000..11f3b9df --- /dev/null +++ b/app/Enums/User/ReferralSource.php @@ -0,0 +1,21 @@ + $goals, ]); + return redirect()->route('app.onboarding.referral-source'); + } + + public function referralSource(Request $request): Response|RedirectResponse + { + if (config('trypost.self_hosted')) { + return redirect()->route('app.calendar'); + } + + $user = $request->user(); + + if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { + return redirect()->route('app.calendar'); + } + + if (! $user->persona) { + return redirect()->route('app.onboarding'); + } + + if (! $user->goals) { + return redirect()->route('app.onboarding.goals'); + } + + return Inertia::render('onboarding/ReferralSource', [ + 'sources' => array_map(fn (ReferralSource $source): string => $source->value, ReferralSource::cases()), + 'selected' => $user->referral_source?->value, + ]); + } + + public function storeReferralSource(StoreOnboardingReferralSourceRequest $request, PostHogService $postHog): RedirectResponse + { + if (config('trypost.self_hosted')) { + return redirect()->route('app.calendar'); + } + + $user = $request->user(); + + if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { + return redirect()->route('app.calendar'); + } + + if (! $user->persona) { + return redirect()->route('app.onboarding'); + } + + if (! $user->goals) { + return redirect()->route('app.onboarding.goals'); + } + + $referralSource = (string) $request->validated('referral_source'); + + $user->update(['referral_source' => $referralSource]); + + $postHog->identify($user->id, [ + 'referral_source' => $referralSource, + ]); + return redirect()->route('app.onboarding.connect'); } @@ -133,6 +192,10 @@ public function connect(Request $request): Response|RedirectResponse return redirect()->route('app.onboarding.goals'); } + if (! $user->referral_source) { + return redirect()->route('app.onboarding.referral-source'); + } + $workspace = $user->currentWorkspace; if (! $workspace) { diff --git a/app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php b/app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php new file mode 100644 index 00000000..47c9a5ae --- /dev/null +++ b/app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php @@ -0,0 +1,27 @@ + + */ + public function rules(): array + { + return [ + 'referral_source' => ['required', Rule::enum(ReferralSource::class)], + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index d8b11ebe..dc06e3e9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,7 @@ use App\Enums\Notification\Type as NotificationType; use App\Enums\User\Persona; +use App\Enums\User\ReferralSource; use App\Models\Traits\HasMedia; use App\Models\Traits\HasWorkspace; use Database\Factories\UserFactory; @@ -45,6 +46,7 @@ class User extends Authenticatable implements MustVerifyEmail, OAuthenticatable 'registration_ip', 'persona', 'goals', + 'referral_source', ]; /** @@ -80,6 +82,7 @@ protected function casts(): array 'two_factor_confirmed_at' => 'datetime', 'persona' => Persona::class, 'goals' => 'array', + 'referral_source' => ReferralSource::class, ]; } diff --git a/database/migrations/2026_07_18_184130_add_referral_source_to_users_table.php b/database/migrations/2026_07_18_184130_add_referral_source_to_users_table.php new file mode 100644 index 00000000..bc97aab6 --- /dev/null +++ b/database/migrations/2026_07_18_184130_add_referral_source_to_users_table.php @@ -0,0 +1,28 @@ +string('referral_source')->nullable()->after('goals'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('referral_source'); + }); + } +}; diff --git a/lang/ar/onboarding.php b/lang/ar/onboarding.php index 1eb9d9d6..5103f1d3 100644 --- a/lang/ar/onboarding.php +++ b/lang/ar/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'مجرد استكشاف في الوقت الحالي', 'other' => 'شيء آخر', ], + 'referral_source_title' => 'كيف وجدتنا؟', + 'referral_source_description' => 'يساعدنا هذا على فهم كيفية اكتشاف الأشخاص لـ TryPost.', + 'referral_source' => [ + 'google' => 'Google أو البحث', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram أو Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'مساعد ذكاء اصطناعي (ChatGPT، Claude…)', + 'friend' => 'صديق أو زميل', + 'blog' => 'مدونة أو نشرة إخبارية أو مقال', + 'other' => 'شيء آخر', + ], 'connect' => [ 'title' => 'اربط شبكتك الأولى', 'description' => 'اربط حسابًا اجتماعيًا واحدًا على الأقل لبدء الجدولة. يمكنك إضافة المزيد في أي وقت.', diff --git a/lang/de/onboarding.php b/lang/de/onboarding.php index a3b93c47..9ccf8ad7 100644 --- a/lang/de/onboarding.php +++ b/lang/de/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Ich schaue mich vorerst nur um', 'other' => 'Etwas anderes', ], + 'referral_source_title' => 'Wie hast du uns gefunden?', + 'referral_source_description' => 'Das hilft uns zu verstehen, wie Menschen TryPost entdecken.', + 'referral_source' => [ + 'google' => 'Google oder Suche', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram oder Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'KI-Assistent (ChatGPT, Claude…)', + 'friend' => 'Freund oder Kollege', + 'blog' => 'Blog, Newsletter oder Artikel', + 'other' => 'Etwas anderes', + ], 'connect' => [ 'title' => 'Verbinde dein erstes Netzwerk', 'description' => 'Verknüpfe mindestens ein Social-Media-Konto, um mit der Planung zu beginnen. Du kannst jederzeit weitere hinzufügen.', diff --git a/lang/el/onboarding.php b/lang/el/onboarding.php index 4da5de0a..d0eccc36 100644 --- a/lang/el/onboarding.php +++ b/lang/el/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Απλώς εξερευνώ προς το παρόν', 'other' => 'Κάτι άλλο', ], + 'referral_source_title' => 'Πώς μας βρήκατε;', + 'referral_source_description' => 'Αυτό μας βοηθά να καταλάβουμε πώς οι άνθρωποι ανακαλύπτουν το TryPost.', + 'referral_source' => [ + 'google' => 'Google ή αναζήτηση', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ή Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Βοηθός AI (ChatGPT, Claude…)', + 'friend' => 'Φίλος ή συνάδελφος', + 'blog' => 'Blog, newsletter ή άρθρο', + 'other' => 'Κάτι άλλο', + ], 'connect' => [ 'title' => 'Συνδέστε το πρώτο σας δίκτυο', 'description' => 'Συνδέστε τουλάχιστον έναν λογαριασμό κοινωνικού δικτύου για να ξεκινήσετε τον προγραμματισμό. Μπορείτε να προσθέσετε κι άλλους ανά πάσα στιγμή.', diff --git a/lang/en/onboarding.php b/lang/en/onboarding.php index 75768dd0..e76cf671 100644 --- a/lang/en/onboarding.php +++ b/lang/en/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Just exploring for now', 'other' => 'Something else', ], + 'referral_source_title' => 'How did you find us?', + 'referral_source_description' => 'This helps us understand how people discover TryPost.', + 'referral_source' => [ + 'google' => 'Google or search', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram or Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI assistant (ChatGPT, Claude…)', + 'friend' => 'Friend or colleague', + 'blog' => 'Blog, newsletter or article', + 'other' => 'Something else', + ], 'connect' => [ 'title' => 'Connect your first network', 'description' => 'Link at least one social account to start scheduling. You can add more anytime.', diff --git a/lang/es/onboarding.php b/lang/es/onboarding.php index f612aa85..4fb16d04 100644 --- a/lang/es/onboarding.php +++ b/lang/es/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Solo estoy explorando por ahora', 'other' => 'Otra cosa', ], + 'referral_source_title' => '¿Cómo nos encontraste?', + 'referral_source_description' => 'Esto nos ayuda a entender cómo la gente descubre TryPost.', + 'referral_source' => [ + 'google' => 'Google o búsqueda', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram o Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Asistente de IA (ChatGPT, Claude…)', + 'friend' => 'Amigo o colega', + 'blog' => 'Blog, newsletter o artículo', + 'other' => 'Otra cosa', + ], 'connect' => [ 'title' => 'Conecta tu primera red', 'description' => 'Vincula al menos una cuenta social para empezar a programar. Puedes añadir más cuando quieras.', diff --git a/lang/fr/onboarding.php b/lang/fr/onboarding.php index 15181f96..7d103c10 100644 --- a/lang/fr/onboarding.php +++ b/lang/fr/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Je découvre pour l\'instant', 'other' => 'Autre chose', ], + 'referral_source_title' => 'Comment nous avez-vous connus ?', + 'referral_source_description' => 'Cela nous aide à comprendre comment les gens découvrent TryPost.', + 'referral_source' => [ + 'google' => 'Google ou recherche', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ou Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistant IA (ChatGPT, Claude…)', + 'friend' => 'Ami ou collègue', + 'blog' => 'Blog, newsletter ou article', + 'other' => 'Autre chose', + ], 'connect' => [ 'title' => 'Connectez votre premier réseau', 'description' => 'Associez au moins un compte social pour commencer à programmer. Vous pourrez en ajouter d\'autres à tout moment.', diff --git a/lang/it/onboarding.php b/lang/it/onboarding.php index 120827b1..0beba033 100644 --- a/lang/it/onboarding.php +++ b/lang/it/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Sto solo dando un\'occhiata', 'other' => 'Qualcos\'altro', ], + 'referral_source_title' => 'Come ci hai trovato?', + 'referral_source_description' => 'Questo ci aiuta a capire come le persone scoprono TryPost.', + 'referral_source' => [ + 'google' => 'Google o ricerca', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram o Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistente IA (ChatGPT, Claude…)', + 'friend' => 'Amico o collega', + 'blog' => 'Blog, newsletter o articolo', + 'other' => 'Qualcos\'altro', + ], 'connect' => [ 'title' => 'Collega la tua prima rete', 'description' => 'Collega almeno un account social per iniziare a programmare. Puoi aggiungerne altri in qualsiasi momento.', diff --git a/lang/ja/onboarding.php b/lang/ja/onboarding.php index a0eda22b..db92adcf 100644 --- a/lang/ja/onboarding.php +++ b/lang/ja/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => '今はまだ様子を見ている', 'other' => 'その他', ], + 'referral_source_title' => 'どこで私たちを知りましたか?', + 'referral_source_description' => 'これは、人々がどのように TryPost を見つけるかを理解するのに役立ちます。', + 'referral_source' => [ + 'google' => 'Google または検索', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram または Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI アシスタント(ChatGPT、Claude など)', + 'friend' => '友人または同僚', + 'blog' => 'ブログ、ニュースレター、記事', + 'other' => 'その他', + ], 'connect' => [ 'title' => '最初のネットワークを接続', 'description' => 'スケジュールを始めるには、少なくとも 1 つのソーシャルアカウントを連携してください。後からいつでも追加できます。', diff --git a/lang/ko/onboarding.php b/lang/ko/onboarding.php index 0fa817ae..fc954799 100644 --- a/lang/ko/onboarding.php +++ b/lang/ko/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => '지금은 둘러보는 중', 'other' => '다른 것', ], + 'referral_source_title' => '저희를 어떻게 알게 되셨나요?', + 'referral_source_description' => '사람들이 TryPost를 어떻게 발견하는지 파악하는 데 도움이 됩니다.', + 'referral_source' => [ + 'google' => 'Google 또는 검색', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram 또는 Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI 어시스턴트 (ChatGPT, Claude 등)', + 'friend' => '친구 또는 동료', + 'blog' => '블로그, 뉴스레터 또는 기사', + 'other' => '기타', + ], 'connect' => [ 'title' => '첫 네트워크 연결', 'description' => '예약을 시작하려면 소셜 계정을 하나 이상 연결하세요. 언제든지 추가할 수 있습니다.', diff --git a/lang/nl/onboarding.php b/lang/nl/onboarding.php index 472dd851..424cfe42 100644 --- a/lang/nl/onboarding.php +++ b/lang/nl/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Voorlopig gewoon aan het verkennen', 'other' => 'Iets anders', ], + 'referral_source_title' => 'Hoe heb je ons gevonden?', + 'referral_source_description' => 'Dit helpt ons te begrijpen hoe mensen TryPost ontdekken.', + 'referral_source' => [ + 'google' => 'Google of zoekmachine', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram of Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI-assistent (ChatGPT, Claude…)', + 'friend' => 'Vriend of collega', + 'blog' => 'Blog, nieuwsbrief of artikel', + 'other' => 'Iets anders', + ], 'connect' => [ 'title' => 'Koppel je eerste netwerk', 'description' => 'Koppel ten minste één social account om te beginnen met plannen. Je kunt er altijd meer toevoegen.', diff --git a/lang/pl/onboarding.php b/lang/pl/onboarding.php index f4ae046c..91092398 100644 --- a/lang/pl/onboarding.php +++ b/lang/pl/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Na razie tylko się rozglądam', 'other' => 'Coś innego', ], + 'referral_source_title' => 'Jak nas znalazłeś?', + 'referral_source_description' => 'To pomaga nam zrozumieć, jak ludzie odkrywają TryPost.', + 'referral_source' => [ + 'google' => 'Google lub wyszukiwarka', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram lub Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Asystent AI (ChatGPT, Claude…)', + 'friend' => 'Znajomy lub współpracownik', + 'blog' => 'Blog, newsletter lub artykuł', + 'other' => 'Coś innego', + ], 'connect' => [ 'title' => 'Połącz swoją pierwszą sieć', 'description' => 'Połącz co najmniej jedno konto społecznościowe, aby zacząć planować. Więcej możesz dodać w dowolnym momencie.', diff --git a/lang/pt-BR/onboarding.php b/lang/pt-BR/onboarding.php index ae9ed497..12ad7962 100644 --- a/lang/pt-BR/onboarding.php +++ b/lang/pt-BR/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Só dando uma olhada por enquanto', 'other' => 'Outra coisa', ], + 'referral_source_title' => 'Como você nos encontrou?', + 'referral_source_description' => 'Isso nos ajuda a entender como as pessoas descobrem o TryPost.', + 'referral_source' => [ + 'google' => 'Google ou busca', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ou Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistente de IA (ChatGPT, Claude…)', + 'friend' => 'Amigo ou colega', + 'blog' => 'Blog, newsletter ou artigo', + 'other' => 'Outra coisa', + ], 'connect' => [ 'title' => 'Conecte sua primeira rede', 'description' => 'Vincule pelo menos uma conta social para começar a agendar. Você pode adicionar mais quando quiser.', diff --git a/lang/ru/onboarding.php b/lang/ru/onboarding.php index 0930a22a..d7b64add 100644 --- a/lang/ru/onboarding.php +++ b/lang/ru/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Пока просто знакомлюсь', 'other' => 'Что-то ещё', ], + 'referral_source_title' => 'Как вы нас нашли?', + 'referral_source_description' => 'Это помогает нам понять, как люди узнают о TryPost.', + 'referral_source' => [ + 'google' => 'Google или поиск', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram или Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'ИИ-ассистент (ChatGPT, Claude…)', + 'friend' => 'Друг или коллега', + 'blog' => 'Блог, рассылка или статья', + 'other' => 'Что-то другое', + ], 'connect' => [ 'title' => 'Подключите первую сеть', 'description' => 'Привяжите хотя бы один социальный аккаунт, чтобы начать планировать. Вы можете добавить другие в любой момент.', diff --git a/lang/tr/onboarding.php b/lang/tr/onboarding.php index 93afaae7..b54630a4 100644 --- a/lang/tr/onboarding.php +++ b/lang/tr/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => 'Şimdilik sadece keşfetmek', 'other' => 'Başka bir şey', ], + 'referral_source_title' => 'Bizi nasıl buldunuz?', + 'referral_source_description' => 'İnsanların TryPost\'u nasıl keşfettiğini anlamamıza yardımcı olur.', + 'referral_source' => [ + 'google' => 'Google veya arama', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram veya Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Yapay zeka asistanı (ChatGPT, Claude…)', + 'friend' => 'Arkadaş veya meslektaş', + 'blog' => 'Blog, bülten veya makale', + 'other' => 'Başka bir şey', + ], 'connect' => [ 'title' => 'İlk ağınızı bağlayın', 'description' => 'Zamanlamaya başlamak için en az bir sosyal hesap bağlayın. İstediğiniz zaman daha fazlasını ekleyebilirsiniz.', diff --git a/lang/zh/onboarding.php b/lang/zh/onboarding.php index e9a11103..9761ce99 100644 --- a/lang/zh/onboarding.php +++ b/lang/zh/onboarding.php @@ -33,6 +33,22 @@ 'just_exploring' => '目前只是随便看看', 'other' => '其他需求', ], + 'referral_source_title' => '您是如何找到我们的?', + 'referral_source_description' => '这有助于我们了解人们是如何发现 TryPost 的。', + 'referral_source' => [ + 'google' => 'Google 或搜索', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram 或 Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI 助手(ChatGPT、Claude 等)', + 'friend' => '朋友或同事', + 'blog' => '博客、新闻通讯或文章', + 'other' => '其他', + ], 'connect' => [ 'title' => '连接你的第一个平台', 'description' => '至少关联一个社交账号即可开始排期。你可以随时添加更多。', diff --git a/resources/js/pages/onboarding/ReferralSource.vue b/resources/js/pages/onboarding/ReferralSource.vue new file mode 100644 index 00000000..76afe58b --- /dev/null +++ b/resources/js/pages/onboarding/ReferralSource.vue @@ -0,0 +1,135 @@ + + + + + + + + + + + + + {{ $t('onboarding.referral_source_title') }} + + + {{ $t('onboarding.referral_source_description') }} + + + + + + + + + + {{ sourceLabel(source) }} + + + + + + + + + + {{ $t('onboarding.continue') }} + + + + + + diff --git a/routes/app.php b/routes/app.php index c368be17..75969ac0 100644 --- a/routes/app.php +++ b/routes/app.php @@ -61,6 +61,8 @@ Route::post('onboarding', [OnboardingController::class, 'store'])->name('app.onboarding.store'); Route::get('onboarding/goals', [OnboardingController::class, 'goals'])->name('app.onboarding.goals'); Route::post('onboarding/goals', [OnboardingController::class, 'storeGoals'])->name('app.onboarding.goals.store'); + Route::get('onboarding/referral-source', [OnboardingController::class, 'referralSource'])->name('app.onboarding.referral-source'); + Route::post('onboarding/referral-source', [OnboardingController::class, 'storeReferralSource'])->name('app.onboarding.referral-source.store'); Route::get('onboarding/connect', [OnboardingController::class, 'connect'])->name('app.onboarding.connect'); Route::post('onboarding/connect', [OnboardingController::class, 'checkout'])->name('app.onboarding.checkout'); Route::get('billing/processing', [BillingController::class, 'processing'])->name('app.billing.processing'); diff --git a/tests/Feature/Onboarding/OnboardingControllerTest.php b/tests/Feature/Onboarding/OnboardingControllerTest.php index 21c00f74..92f1a0dc 100644 --- a/tests/Feature/Onboarding/OnboardingControllerTest.php +++ b/tests/Feature/Onboarding/OnboardingControllerTest.php @@ -7,6 +7,7 @@ use App\Enums\SocialAccount\Platform; use App\Enums\User\Goal; use App\Enums\User\Persona; +use App\Enums\User\ReferralSource; use App\Jobs\PostHog\SendEvent; use App\Models\Account; use App\Models\Plan; @@ -182,11 +183,11 @@ function subscribeOnboardingAccount(Account $account): void 'goals' => [Goal::JustExploring->value, Goal::SaveTime->value], ]); - $response->assertRedirect(route('app.onboarding.connect')); + $response->assertRedirect(route('app.onboarding.referral-source')); expect($this->user->fresh()->goals)->toBe([Goal::JustExploring->value, Goal::SaveTime->value]); }); -test('goals store saves the goals, mirrors to PostHog and advances to the connect step', function () { +test('goals store saves the goals, mirrors to PostHog and advances to the referral-source step', function () { config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test']); Bus::fake(); @@ -196,7 +197,7 @@ function subscribeOnboardingAccount(Account $account): void 'goals' => [Goal::SaveTime->value, Goal::AiContent->value], ]); - $response->assertRedirect(route('app.onboarding.connect')); + $response->assertRedirect(route('app.onboarding.referral-source')); expect($this->user->fresh()->goals)->toBe([Goal::SaveTime->value, Goal::AiContent->value]); Bus::assertDispatched(SendEvent::class); @@ -212,7 +213,7 @@ function subscribeOnboardingAccount(Account $account): void 'goals' => [Goal::JustExploring->value], ]); - $response->assertRedirect(route('app.onboarding.connect')); + $response->assertRedirect(route('app.onboarding.referral-source')); expect($this->user->fresh()->goals)->toBe([Goal::JustExploring->value]); Bus::assertDispatched(SendEvent::class); @@ -239,6 +240,136 @@ function subscribeOnboardingAccount(Account $account): void expect($this->user->fresh()->goals)->toBeNull(); }); +test('referral source renders the source selection for an account that picked persona and goals', function () { + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + + $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.referral-source')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->component('onboarding/ReferralSource') + ->has('sources', count(ReferralSource::cases())) + ); +}); + +test('referral source redirects to the persona step when no persona was chosen', function () { + $response = $this->actingAs($this->user)->get(route('app.onboarding.referral-source')); + + $response->assertRedirect(route('app.onboarding')); +}); + +test('referral source redirects to the goals step when a persona was chosen but no goals', function () { + $this->user->update(['persona' => Persona::Agency->value]); + + $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.referral-source')); + + $response->assertRedirect(route('app.onboarding.goals')); +}); + +test('referral source redirects to calendar in self-hosted mode', function () { + config(['trypost.self_hosted' => true]); + + $response = $this->actingAs($this->user)->get(route('app.onboarding.referral-source')); + + $response->assertRedirect(route('app.calendar')); +}); + +test('referral source redirects to calendar when already subscribed', function () { + subscribeOnboardingAccount($this->user->account); + + $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.referral-source')); + + $response->assertRedirect(route('app.calendar')); +}); + +test('referral source store requires a source', function () { + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), []); + + $response->assertSessionHasErrors('referral_source'); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('referral source store rejects an invalid source', function () { + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => 'not-a-source', + ]); + + $response->assertSessionHasErrors('referral_source'); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('referral source store saves the source, mirrors to PostHog and advances to the connect step', function () { + config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test']); + Bus::fake(); + + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::ProductHunt->value, + ]); + + $response->assertRedirect(route('app.onboarding.connect')); + expect($this->user->fresh()->referral_source)->toBe(ReferralSource::ProductHunt); + + Bus::assertDispatched(SendEvent::class); +}); + +test('referral source store redirects to the persona step when no persona was chosen', function () { + $response = $this->actingAs($this->user)->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::Google->value, + ]); + + $response->assertRedirect(route('app.onboarding')); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('referral source store redirects to the goals step when no goals were chosen', function () { + $this->user->update(['persona' => Persona::Agency->value]); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::Google->value, + ]); + + $response->assertRedirect(route('app.onboarding.goals')); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('referral source store does nothing in self-hosted mode', function () { + config(['trypost.self_hosted' => true]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::Google->value, + ]); + + $response->assertRedirect(route('app.calendar')); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('referral source store redirects an already-subscribed account to the calendar', function () { + subscribeOnboardingAccount($this->user->account); + + $response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::Google->value, + ]); + + $response->assertRedirect(route('app.calendar')); + expect($this->user->fresh()->referral_source)->toBeNull(); +}); + +test('connect redirects to the referral-source step when persona and goals chosen but no source', function () { + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + onboardingWorkspace($this->user); + + $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect')); + + $response->assertRedirect(route('app.onboarding.referral-source')); +}); + test('connect redirects to the goals step when a persona was chosen but no goals', function () { $this->user->update(['persona' => Persona::Agency->value]); onboardingWorkspace($this->user); @@ -249,7 +380,7 @@ function subscribeOnboardingAccount(Account $account): void }); test('connect renders the network grid for an unsubscribed account that picked a persona', function () { - $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value], 'referral_source' => ReferralSource::Google->value]); onboardingWorkspace($this->user); $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect')); @@ -264,7 +395,7 @@ 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]]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value], 'referral_source' => ReferralSource::Google->value]); onboardingWorkspace($this->user); $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); @@ -280,7 +411,7 @@ function subscribeOnboardingAccount(Account $account): void }); 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]]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value], 'referral_source' => ReferralSource::Google->value]); onboardingWorkspace($this->user); $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect')); @@ -295,7 +426,7 @@ function subscribeOnboardingAccount(Account $account): void }); test('connect lists the workspace social accounts already connected', function () { - $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value], 'referral_source' => ReferralSource::Google->value]); $workspace = onboardingWorkspace($this->user); SocialAccount::factory()->create(['workspace_id' => $workspace->id]); @@ -391,7 +522,7 @@ function subscribeOnboardingAccount(Account $account): void }); test('connect redirects to workspace creation when no workspace exists', function () { - $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]); + $this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value], 'referral_source' => ReferralSource::Google->value]); $response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect')); @@ -410,12 +541,17 @@ function subscribeOnboardingAccount(Account $account): void $this->actingAs($this->user->fresh())->post(route('app.onboarding.store'), ['persona' => Persona::Creator->value]) ->assertRedirect(route('app.onboarding.goals')); - // Goals advances to connect. + // Goals advances to the referral-source step. $this->actingAs($this->user->fresh())->post(route('app.onboarding.goals.store'), [ 'goals' => [Goal::AiContent->value, Goal::SaveTime->value], + ])->assertRedirect(route('app.onboarding.referral-source')); + + // Referral source advances to connect. + $this->actingAs($this->user->fresh())->post(route('app.onboarding.referral-source.store'), [ + 'referral_source' => ReferralSource::Friend->value, ])->assertRedirect(route('app.onboarding.connect')); - // Connect renders now that persona, goals and a workspace are present. + // Connect renders now that persona, goals, a source and a workspace are present. $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect'))->assertOk(); // Checkout blocks until a network is connected. @@ -435,6 +571,7 @@ function subscribeOnboardingAccount(Account $account): void expect($this->user->fresh()->persona)->toBe(Persona::Creator); expect($this->user->fresh()->goals)->toBe([Goal::AiContent->value, Goal::SaveTime->value]); + expect($this->user->fresh()->referral_source)->toBe(ReferralSource::Friend); }); test('a self-hosted user never enters the onboarding flow', function () { @@ -445,7 +582,7 @@ function subscribeOnboardingAccount(Account $account): void $this->actingAs($this->user->fresh())->get(route('app.calendar'))->assertOk(); // Every onboarding step just bounces to the calendar. - foreach (['app.onboarding', 'app.onboarding.goals', 'app.onboarding.connect'] as $routeName) { + foreach (['app.onboarding', 'app.onboarding.goals', 'app.onboarding.referral-source', 'app.onboarding.connect'] as $routeName) { $this->actingAs($this->user->fresh())->get(route($routeName))->assertRedirect(route('app.calendar')); } });
+ {{ $t('onboarding.referral_source_description') }} +