feat(web): plan preselection from landing pricing (CRO #7)

Carry the plan a visitor picks on the landing pricing through to checkout:
- Pricing CTAs now pass ?plan=<key>; logged-in users go straight to
  /dashboard/subscription, others to /register.
- Subscription page accepts ?plan= (and a localStorage carrier that
  survives the value-first register→search onboarding detour) and
  preselects the plan — advancing to brand selection (or payment for
  Full) — unless the sub is active/pending or onboarding is in progress.
- Register validates ?plan and persists it on signup so the choice isn't
  lost through the trial onboarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 03:01:26 +03:00
parent 0dc298631d
commit c76ab7a280
3 changed files with 60 additions and 8 deletions

View File

@@ -14,14 +14,21 @@ import { useRef, useState } from "react";
export const Route = createFileRoute("/_auth/register")({
component: RegisterPage,
validateSearch: (search: Record<string, unknown>): { vin?: string; ref?: string } => ({
validateSearch: (
search: Record<string, unknown>,
): { vin?: string; ref?: string; plan?: string } => ({
vin: (search.vin as string) || undefined,
ref: (search.ref as string) || undefined,
plan: (search.plan as string) || undefined,
}),
});
// Carry a plan chosen on the marketing pricing through the value-first
// onboarding detour so the subscription page can preselect it later.
const PENDING_PLAN_KEY = "sase-pending-plan";
function RegisterPage() {
const { vin, ref } = Route.useSearch();
const { vin, ref, plan } = Route.useSearch();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -92,6 +99,7 @@ function RegisterPage() {
// (covers both email and Google OAuth signups).
capture("user_signed_up", { method: "email" });
trackMeta("CompleteRegistration", { method: "email" });
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
toast.success("Hesap oluşturuldu!");
window.location.href = redirectUrl;
} catch {
@@ -123,6 +131,7 @@ function RegisterPage() {
startAction("register", { method: "google" });
capture("user_signed_up", { method: "google" });
trackMeta("CompleteRegistration", { method: "google" });
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
signIn.social({ provider: "google", callbackURL: redirectUrl });
}}
>

View File

@@ -75,6 +75,7 @@ interface SubscriptionSearch {
ref?: string | null;
stripe?: string;
session_id?: string;
plan?: string;
}
export const Route = createFileRoute("/dashboard/subscription/")({
@@ -83,10 +84,15 @@ export const Route = createFileRoute("/dashboard/subscription/")({
ref: search.ref ? String(search.ref) : undefined,
stripe: search.stripe ? String(search.stripe) : undefined,
session_id: search.session_id ? String(search.session_id) : undefined,
plan: search.plan ? String(search.plan) : undefined,
}),
component: SubscriptionPage,
});
/** localStorage carrier so a plan chosen pre-signup survives the value-first
* onboarding detour (register → search) and preselects on the next visit. */
export const PENDING_PLAN_KEY = "sase-pending-plan";
interface SubscriptionBrand {
brandId: string;
brandName: string;
@@ -414,6 +420,24 @@ export function SubscriptionPage() {
setStep("payment");
}, [subscription, selectedPlanKey]);
// Preselect a plan chosen from the marketing pricing — via ?plan= or the
// pre-signup localStorage carrier. Skipped for active/pending subscriptions,
// during the welcome onboarding, or once the user has started selecting.
const hasPreselectedRef = useRef(false);
useEffect(() => {
if (hasPreselectedRef.current || isLoading || welcome || selectedPlanKey) return;
if (subscription?.status === "active" || subscription?.status === "pending") return;
const stored = typeof window !== "undefined" ? localStorage.getItem(PENDING_PLAN_KEY) : null;
const planKey = search.plan ?? stored ?? null;
if (!planKey || !plans.some((p) => p.key === planKey)) return;
hasPreselectedRef.current = true;
localStorage.removeItem(PENDING_PLAN_KEY);
capture("plan_selected", { plan: planKey, source: "marketing_preselect" });
setSelectedPlanKey(planKey);
setSelectedBrandIds([]);
setStep(planKey === "full" ? "payment" : "brands");
}, [isLoading, welcome, selectedPlanKey, subscription, search.plan]);
const cancelPendingMutation = useMutation({
mutationFn: () => api.patch("/subscriptions/cancel-pending"),
onSuccess: () => {

View File

@@ -1604,6 +1604,7 @@ export function HomePage() {
{[
{
name: "1 Marka",
planKey: "brand1",
description: "Tek marka için erişim",
price: "200",
yearly: "2.000",
@@ -1616,6 +1617,7 @@ export function HomePage() {
},
{
name: "2 Marka",
planKey: "brand2",
description: "İki farklı marka",
price: "350",
yearly: "3.500",
@@ -1628,6 +1630,7 @@ export function HomePage() {
},
{
name: "3 Marka",
planKey: "brand3",
description: "Üç marka kapsamlı",
price: "500",
yearly: "5.000",
@@ -1667,11 +1670,23 @@ export function HomePage() {
</li>
))}
</ul>
<Link to="/register" className="mt-7">
<Button variant="outline" className="w-full rounded-full">
30 gün ücretsiz dene
</Button>
</Link>
{isAuthenticated ? (
<Link
to="/dashboard/subscription"
search={{ plan: plan.planKey }}
className="mt-7"
>
<Button variant="outline" className="w-full rounded-full">
30 gün ücretsiz dene
</Button>
</Link>
) : (
<Link to="/register" search={{ plan: plan.planKey }} className="mt-7">
<Button variant="outline" className="w-full rounded-full">
30 gün ücretsiz dene
</Button>
</Link>
)}
</div>
))}
</div>
@@ -1707,7 +1722,11 @@ export function HomePage() {
yıllık 9.990 TL — ayda 832 TL'ye denk gelir
</p>
<Link to="/register" className="mt-8 inline-block">
<Link
to={isAuthenticated ? "/dashboard/subscription" : "/register"}
search={{ plan: "full" }}
className="mt-8 inline-block"
>
<Button variant="brand" size="lg" className="rounded-full">
30 gün ücretsiz dene
<ArrowRight className="ml-2 size-4" />