merge(olcum/liste-uretim-basladi): feat(olcum): /listem?uret=1 yollarına kaynak parametresi

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-22 01:38:36 +03:00
6 changed files with 127 additions and 15 deletions

View File

@@ -20,12 +20,18 @@ export const metadata: Metadata = {
export default function ListemPage({ export default function ListemPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ acildi?: string; uret?: string }>; // kaynak: üretim denemesini hangi yüzeyin başlattığı (bkz.
// uretim-basladi-olayi.tsx). Doğrulaması okuyan tarafta, allowlist ile.
searchParams: Promise<{ acildi?: string; uret?: string; kaynak?: string }>;
}) { }) {
return ( return (
<Suspense fallback={<ListemIcerikSkeleton />}> <Suspense fallback={<ListemIcerikSkeleton />}>
{searchParams.then(({ acildi, uret }) => ( {searchParams.then(({ acildi, uret, kaynak }) => (
<ListemIcerik uret={uret === "1"} acildi={acildi === "1"} /> <ListemIcerik
uret={uret === "1"}
acildi={acildi === "1"}
kaynak={kaynak}
/>
))} ))}
</Suspense> </Suspense>
); );

View File

@@ -154,7 +154,7 @@ async function PaketCta() {
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok {/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
de yoksa boş durum sihirbaza yönlendirir. */} de yoksa boş durum sihirbaza yönlendirir. */}
<Link href="/listem?uret=1" className="underline"> <Link href="/listem?uret=1&kaynak=odeme_sonrasi" className="underline">
listeni oluştur listeni oluştur
</Link> </Link>
</div> </div>

View File

@@ -20,6 +20,7 @@ import type { Mesaj } from "./sohbet-client";
import { ListemGovde } from "./listem-govde"; import { ListemGovde } from "./listem-govde";
import { ListemBosCta } from "./listem-bos-cta"; import { ListemBosCta } from "./listem-bos-cta";
import { ListeUretici } from "./liste-uretici"; import { ListeUretici } from "./liste-uretici";
import { UretimBasladiOlayi } from "./uretim-basladi-olayi";
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", { const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0, maximumFractionDigits: 0,
@@ -28,9 +29,13 @@ const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
export async function ListemIcerik({ export async function ListemIcerik({
uret, uret,
acildi, acildi,
kaynak,
}: { }: {
uret: boolean; uret: boolean;
acildi: boolean; acildi: boolean;
/** Üretim denemesini başlatan yüzey (`?kaynak=`); doğrulaması
* UretimBasladiOlayi'nın allowlist'inde. */
kaynak?: string;
}) { }) {
// Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce // Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce
// "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır, // "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır,
@@ -39,7 +44,14 @@ export async function ListemIcerik({
const cerezProfili = cerezdenTercihProfili( const cerezProfili = cerezdenTercihProfili(
(await cookies()).get(PROFIL_CEREZ_ADI)?.value, (await cookies()).get(PROFIL_CEREZ_ADI)?.value,
); );
const geriYol = uret || cerezProfili ? "/listem?uret=1" : "/listem"; // `kaynak` giriş round-trip'inde korunur; yoksa girişten dönen herkes
// "bilinmiyor" okunurdu. `uret=1` HER ZAMAN ilk parametre kalmalı:
// giris-karti.tsx:31 funnel metnini `startsWith("/listem?uret=1")` ile
// seçiyor, sıra bozulursa "Seçimlerin hazır" metni kaybolur.
const geriYol =
uret || cerezProfili
? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}`
: "/listem";
const session = await verifySession(geriYol); const session = await verifySession(geriYol);
const [user, satir, sohbetGecmisi] = await Promise.all([ const [user, satir, sohbetGecmisi] = await Promise.all([
getCurrentUser(), getCurrentUser(),
@@ -71,11 +83,20 @@ export async function ListemIcerik({
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim {/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim
burada koşar, animasyon ve hata/kredi durumları burada yaşar. */} burada koşar, animasyon ve hata/kredi durumları burada yaşar. */}
{uret ? ( {uret ? (
<ListeUretici <>
hasPaket={Boolean(user.hasPaket)} {/* Denemenin paydası: üretim gövdesiyle kardeş, aynı koşulda
sunucuProfil={cerezProfili} mount olur (bkz. uretim-basladi-olayi.tsx). */}
bosDurum={bosDurum} <UretimBasladiOlayi
/> kaynak={kaynak ?? "bilinmiyor"}
paketli={Boolean(user.hasPaket)}
sunucuProfil={cerezProfili}
/>
<ListeUretici
hasPaket={Boolean(user.hasPaket)}
sunucuProfil={cerezProfili}
bosDurum={bosDurum}
/>
</>
) : ( ) : (
bosDurum bosDurum
)} )}

View File

@@ -0,0 +1,70 @@
"use client";
// Üretim denemesinin paydasını ölçen görünmez bileşen: /listem?uret=1 ile
// üretim gövdesi kurulduğunda `liste_uretim_basladi` atar. Bugüne kadar
// yalnız başarı (`liste_olusturuldu`) sayıldığı için "kaç kişi denedi, kaçı
// listeye ulaştı" sorusunun paydası yoktu.
//
// Olay bilerek üretimi başlatan `liste-uretici.tsx`'te değil, onun kardeşi
// olan bu dosyada duruyor (o dosya bu turda başka bir işin altında). İkisi de
// `listem-icerik.tsx`'te aynı `uret` dalında mount edildiğinden koşulları
// birebir aynı: biri mount oluyorsa diğeri de oluyor.
import { useEffect, useRef } from "react";
import { olay } from "@/lib/analitik";
import type { TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
/**
* `?kaynak=` sözleşmesinin izinli değerleri. Değer URL'den, yani kullanıcının
* denetiminden geliyor: allowlist olmadan Rybbit'e keyfi dizgi yazılabilir
* (kardinalite patlaması, panele enjekte edilen metin). Listede olmayan her
* şey `"bilinmiyor"`a düşer. Yeni bir üretim yüzeyi eklerken değerini önce
* buraya ekle, sonra href'e koy.
*/
const IZINLI_KAYNAKLAR = new Set([
"sihirbaz",
"hazir_cta",
"giris_donusu",
"bos_durum",
"odeme_sonrasi",
"bilinmiyor",
]);
export function UretimBasladiOlayi({
kaynak,
paketli,
sunucuProfil,
}: {
/** `?kaynak=` ham değeri; allowlist dışındaysa "bilinmiyor" raporlanır. */
kaynak: string;
paketli: boolean;
/** Profil çerezinden SSR'da çözülen yedek profil (bkz. listem-icerik). */
sunucuProfil: TercihProfili | null;
}) {
// Mount başına tek atış. StrictMode geliştirmede effect'i iki kez koşuyor;
// `liste-uretici.tsx`'teki `basladi` ref'iyle aynı desen. `olayBirKez`
// BİLEREK kullanılmıyor: o oturum başına tekilleştirir, oysa aynı oturumda
// ikinci deneme de paydaya girmeli.
const atildi = useRef(false);
useEffect(() => {
if (atildi.current) return;
atildi.current = true;
const kaynakGuvenli = IZINLI_KAYNAKLAR.has(kaynak) ? kaynak : "bilinmiyor";
olay("liste_uretim_basladi", {
kaynak: kaynakGuvenli,
paketli: paketli ? "evet" : "hayir",
// Üretimin gerçekten başlayabilmesinin koşulu: bu ifade
// `liste-uretici.tsx`'teki `tercihProfiliOku() ?? sunucuProfil`
// ifadesinin ikizidir — ikisi ayrı dosyada olduğu için elle eşit
// tutulmalı. O dosya açıldığında (bkz. backlog: olayın gerçek çağrı
// yerine taşınması) tek yere alınacak. Ham sıralama/seçim GÖNDERİLMEZ
// (bkz. lib/analitik.ts kuralı) — yalnız profilin varlığı.
profil: (tercihProfiliOku() ?? sunucuProfil) ? "var" : "yok",
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}

View File

@@ -17,7 +17,14 @@ import { SihirbazModal } from "./sihirbaz-modal";
// Üretim /listem'de koşar (bkz. listem/liste-uretici.tsx); sihirbaz // Üretim /listem'de koşar (bkz. listem/liste-uretici.tsx); sihirbaz
// tamamlanınca seçimler storage'a yazılıp kullanıcı buraya yönlendirilir. // tamamlanınca seçimler storage'a yazılıp kullanıcı buraya yönlendirilir.
const LISTEM_URET_HREF = "/listem?uret=1"; // `kaynak` denemenin hangi yüzeyden başladığını ölçer (bkz.
// rapor/components/uretim-basladi-olayi.tsx — izinli değerler orada).
// `uret=1` HER ZAMAN ilk parametre kalmalı: giris-karti.tsx:31 funnel
// metnini `callbackURL.startsWith("/listem?uret=1")` ile seçiyor, sıra
// bozulursa girişteki "Seçimlerin hazır" metni kaybolur.
function listemUretHref(kaynak: string) {
return `/listem?uret=1&kaynak=${kaynak}`;
}
// SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz // SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz
const bosAbone = () => () => {}; const bosAbone = () => () => {};
@@ -182,10 +189,12 @@ export function SihirbazBolumu({
await modalCikisiniOynat(); await modalCikisiniOynat();
setElleAcik(false); setElleAcik(false);
if (!girisliMi) { if (!girisliMi) {
router.push(`/giris?callback=${encodeURIComponent(LISTEM_URET_HREF)}`); router.push(
`/giris?callback=${encodeURIComponent(listemUretHref("sihirbaz"))}`,
);
return; return;
} }
router.push(LISTEM_URET_HREF); router.push(listemUretHref("sihirbaz"));
} }
/** /**
@@ -213,7 +222,7 @@ export function SihirbazBolumu({
if (otomatikGonder && !otomatikBasladi.current) { if (otomatikGonder && !otomatikBasladi.current) {
otomatikBasladi.current = true; otomatikBasladi.current = true;
// replace: geri tuşu bu sayfaya dönünce tekrar /listem'e sekmesin // replace: geri tuşu bu sayfaya dönünce tekrar /listem'e sekmesin
router.replace(LISTEM_URET_HREF); router.replace(listemUretHref("giris_donusu"));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [otomatikGonder]); }, [otomatikGonder]);
@@ -242,7 +251,7 @@ export function SihirbazBolumu({
// kaydedilmesi olarak sunulur. Girişten dönüşte üretim /listem'de başlar. // kaydedilmesi olarak sunulur. Girişten dönüşte üretim /listem'de başlar.
// Sinyal URL'den (?hazir=1) gelir ki SSR'da doğru panel render edilsin. // Sinyal URL'den (?hazir=1) gelir ki SSR'da doğru panel render edilsin.
const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !raporVar); const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !raporVar);
const girisUrl = `/giris?callback=${encodeURIComponent(LISTEM_URET_HREF)}`; const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("hazir_cta"))}`;
// Kapı 1 ölçümü: panel yalnızca girisBekliyor dalında render edilir; ref // Kapı 1 ölçümü: panel yalnızca girisBekliyor dalında render edilir; ref
// başka dalda bağlanmadığından observer da yalnızca o durumda kurulur. // başka dalda bağlanmadığından observer da yalnızca o durumda kurulur.
const hazirPanelRef = useGorunumOlayi<HTMLElement>("hazir_panel_goruntulendi"); const hazirPanelRef = useGorunumOlayi<HTMLElement>("hazir_panel_goruntulendi");

View File

@@ -17,6 +17,12 @@ export type OlayAdi =
| "sihirbaz_adim" | "sihirbaz_adim"
| "sihirbaz_tamamlandi" | "sihirbaz_tamamlandi"
| "giris_denendi" | "giris_denendi"
// Üretim denemesinin paydası: /listem?uret=1 ile üretim gövdesi mount
// olduğunda atılır (sihirbaz, hazır panel CTA'sı, giriş dönüşü, boş durum
// CTA'sı ve ödeme sonrası kartı — hangisi olduğu `kaynak` özelliğinde).
// Bilerek `olayBirKez` KULLANILMAZ: aynı oturumdaki ikinci deneme de
// sayılmalı, yoksa payda başarıdan (liste_olusturuldu) küçük kalır.
| "liste_uretim_basladi"
| "liste_olusturuldu" | "liste_olusturuldu"
| "liste_guncellendi" | "liste_guncellendi"
| "liste_revize" | "liste_revize"