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:
@@ -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 });
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user