# Oturum açmış müşteri

> Sitenizde oturum açmış müşteri için yorum formunun sipariş numarası sorulmadan açılması, ad ve e-posta yazmadan soru sorulması ve sipariş detayı sayfasında ürünlerin değerlendirilmesi.

Kaynak: /tr/integrate/signed-in-customers/

Müşteri sitenizde oturum açmışsa, sipariş numarası ve e-posta adresi sorulmadan yorum yazması sağlanabilir. Bunun için sunucunuz, oturumdaki e-posta adresiyle müşterinin bu ürüne yorum yazıp yazamayacağını Sayproof'a sorar ve yanıtı script'e iletir.

Soru-cevap açıksa oturum açmış müşteri soru sorarken de adını ve e-posta adresini yazmaz: [Soru-cevap](#soru-cevap). Sipariş detayı sayfasında ise siparişin ürünleri sayfanın içinde değerlendirilebilir: [Sipariş detayı sayfası](#siparis-detayi-sayfasi).

Temel kural değişmez: yorum yazma hakkını tarayıcı değil, Sayproof'taki sipariş kaydı belirler. Oturum yalnızca müşterinin kim olduğunu belirtir.

## Çalışma şekli

1. Container'a `data-review-viewer="pending"` attribute'u eklenir. Yanıt gelene kadar script yorum yazma alanını göstermez.
2. Tarayıcı, **sizin** sitenizdeki bir endpoint'e (örneğin `/api/sayproof/viewer`) ürün için müşterinin durumunu sorar.
3. Sunucunuz oturumdaki e-posta adresini okur ve sunucu anahtarıyla Sayproof'a sorar: `GET /v1/viewer`.
4. Yanıt tarayıcıya döner; sayfa bu yanıtı `Sayproof.update(container, { viewer })` ile script'e iletir.

Müşterinin durumu sayfanın HTML'ine eklenmez; kişiye özeldir ve her seferinde önbellek kullanılmadan sorgulanır. Böylece ürün sayfasının kendisi önbellekte tutulabilir.

## Yanıtlar

| `state` | Anlamı | Müşterinin gördüğü |
|---|---|---|
| `can_write` | Teslim edilmiş ve henüz yorum yazılmamış bir satın alması var | "Yorum yaz" düğmesi; form sipariş sorulmadan açılır |
| `written` | Bu ürün için yorum yazmış | Kendi yorumunun durumu: yayında, incelemede, reddedildi ya da kaldırıldı |
| `none` | Yorum yazabileceği bir satın alması yok | Oturum açmamış ziyaretçiyle aynı doğrulama alanı |
| `anonymous` | Oturum yok ya da yanıt alınamadı | Oturum açmamış ziyaretçiyle aynı doğrulama alanı |

Yalnızca kanalın pazarındaki siparişler dikkate alınır. E-posta adresi küçük harfe dönüştürülerek birebir eşleştirilir.

## Sunucu tarafı

Sayproof'a gönderilen istek:

```http
GET https://api.sayproof.example/v1/viewer?product=URUN-KODU&scope=variant&lang=tr&channel=web
Authorization: Bearer SUNUCU-ANAHTARINIZ
X-Sayproof-Customer: ayse@ornek.com
```

- E-posta adresi yalnızca `X-Sayproof-Customer` header'ında gönderilir; URL'e eklenmez.
- Bu endpoint yalnızca sunucu anahtarıyla çalışır. Tarayıcıdan site anahtarıyla gönderilen istek `403 refused / server_only` yanıtı alır.
- Bir sunucu anahtarıyla 5 dakikada 6.000 istek gönderilebilir. Tüm müşterilerin istekleri sizin sunucunuz üzerinden geldiği için müşteriler birbirinin sınırını etkilemez.
- Yanıt `Cache-Control: no-store` header'ını taşır; kendi tarafınızda da önbelleğe almayın.

Next.js Route Handler örneği. Oturum bilgisini kendi kimlik doğrulama kütüphanenizden alın; buradaki `auth()` yalnızca örnektir:

```ts
import "server-only";
import { auth } from "@/auth";

export const dynamic = "force-dynamic";

const base = process.env.SAYPROOF_BASE || process.env.NEXT_PUBLIC_SAYPROOF_BASE;
const noStore = { "Cache-Control": "no-store" };
const anonymous = () => Response.json({ state: "anonymous" }, { headers: noStore });

export async function GET(request: Request) {
  const asked = new URL(request.url).searchParams;
  const product = asked.get("product");
  const email = (await auth())?.user?.email;
  if (!product || !email) return anonymous();
  const query = new URLSearchParams({
    product,
    scope: asked.get("scope") === "group" ? "group" : "variant",
    lang: asked.get("locale") ?? "tr",
    channel: process.env.SAYPROOF_CHANNEL ?? "web",
  });
  try {
    const answer = await fetch(`${base}/v1/viewer?${query}`, {
      headers: {
        Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}`,
        "X-Sayproof-Customer": email,
      },
      cache: "no-store",
      signal: AbortSignal.timeout(3000),
    });
    return answer.ok ? Response.json(await answer.json(), { headers: noStore }) : anonymous();
  } catch {
    return anonymous();
  }
}
```

Başka bir sunucu dilinde de aynı üç kural geçerlidir: e-posta adresini tarayıcıdan değil oturumdan okuyun; hata durumunda `anonymous` döndürün; yanıtı önbelleğe almayın.

## Sayfa tarafı

```html
<div id="reviews"
     data-review-mount
     data-review-product="URUN-KODU"
     data-review-viewer="pending"></div>
```

```js
function whenSayproofReady(callback) {
  if (window.Sayproof) callback(window.Sayproof);
  else window.addEventListener("sayproof:ready", () => callback(window.Sayproof), { once: true });
}

async function askViewer(container) {
  const query = new URLSearchParams({
    product: container.dataset.reviewProduct,
    scope: container.dataset.reviewScope ?? "variant",
    locale: document.documentElement.lang || "tr",
  });
  try {
    const answer = await fetch(`/api/sayproof/viewer?${query}`, {
      credentials: "same-origin",
      cache: "no-store",
      signal: AbortSignal.timeout(8000),
    });
    return answer.ok ? await answer.json() : { state: "anonymous" };
  } catch {
    return { state: "anonymous" };
  }
}

const container = document.querySelector("#reviews");
askViewer(container).then((viewer) => whenSayproofReady((sayproof) => sayproof.update(container, { viewer })));
```

Yanıt hiç gelmese bile script'e bir değer iletin: `{ state: "anonymous" }` ya da `viewer: null`. Aksi hâlde yorum yazma alanı bekleme durumunda kalır.

## Yorum gönderildikten sonra

Gönderim başarılı olduğunda script müşterinin durumunu kendisi `written` olarak günceller; aynı sayfada form yeniden açılmaz. Kendi listenizi yenilemek isterseniz container'ın yaydığı `sayproof:submitted` event'ini dinleyin: [JavaScript API](/tr/reference/javascript-api.md).

## Soru-cevap

Marka soru-cevabı açtıysa `GET /v1/viewer` yanıtı `state` değeri ne olursa olsun bir `asker` alanı da taşır. Script bu alanı görünce soru formunda ad ve e-posta alanlarını göstermez, yerine "Ayşe K. olarak soruyorsunuz. Cevap gelince e-postanıza haber veririz." satırını gösterir. Soru oturumdaki e-posta adresiyle kaydedilir ve cevap yayına çıktığında bu adrese haber verilir.

```json
{
  "state": "none",
  "reviews": [],
  "limits": { "…": "…" },
  "asker": { "token": "…", "name": "Ayşe K." }
}
```

- `asker.token`: markanıza bağlı, imzalı ve 1 saat geçerli bir token. Script soruyu bu token'la gönderir; Sayproof e-posta adresini ve adı token'dan okur, tarayıcı e-posta adresini göndermez. Token'ın içeriğini kendi kodunuzda okumayın ve değiştirmeyin.
- `asker.name`: sorunun yanında görünecek ad. İsteğe bağlı `X-Sayproof-Customer-Name` header'ıyla gönderilir; Türkçe karakterler için değeri UTF-8 olarak percent-encode edin (JavaScript'te `encodeURIComponent`). Boşluklar sadeleştirilir, en fazla 120 karakter alınır. Header gönderilmezse `name` boş gelir, formda "Hesabınızla soruyorsunuz. Cevap gelince e-postanıza haber veririz." yazar ve soru "Anonim" olarak görünür. Ad sayfada herkese görünür; tam ad yerine "Ayşe K." gibi kısa bir biçim gönderin.
- Token'ın süresi dolduysa ya da geçersizse soru `403 token_refused / asker_expired` ya da `asker_invalid` yanıtı alır. Script bu durumda formu, yazılan soruyu koruyarak oturum açmamış ziyaretçinin formuna çevirir.

```http
GET https://api.sayproof.example/v1/viewer?product=URUN-KODU&scope=variant&lang=tr&channel=web
Authorization: Bearer SUNUCU-ANAHTARINIZ
X-Sayproof-Customer: ayse@ornek.com
X-Sayproof-Customer-Name: Ay%C5%9Fe%20K.
```

Yukarıdaki Next.js örneğinde header'lara `"X-Sayproof-Customer-Name": encodeURIComponent(name)` satırını eklemeniz yeterlidir; adı e-posta adresi gibi oturumdan okuyun.

## Sipariş detayı sayfası

Hesap bölümünüzdeki sipariş detayı sayfasında, siparişin her ürünü için sayfanın içinde bir puan ve yorum formu gösterilebilir. Dialog açılmaz ve sipariş numarası sorulmaz; her ürünün kendi **Gönder** düğmesi vardır. Sunucunuz oturumdaki e-posta adresi ve sipariş numarasıyla Sayproof'a sorar, yanıtı sayfanın HTML'ine ekler; script bölümü bu yanıttan kurar.

```http
GET https://api.sayproof.example/v1/viewer/order?order=100245&channel=web&lang=tr
Authorization: Bearer SUNUCU-ANAHTARINIZ
X-Sayproof-Customer: ayse@ornek.com
```

Yanıttaki her satır bir sipariş ürünüdür ve `state` değeri taşır:

| `state` | Anlamı | Müşterinin gördüğü |
|---|---|---|
| `can_write` | Ürün teslim edilmiş ve henüz yorum yazılmamış | Yıldızlar, ürün için tanımlıysa değerlendirme başlıkları, yorum metni, fotoğraf ve video alanı ve satırın **Gönder** düğmesi |
| `pending` | Yorum gönderilmiş, incelemede | "Yorumunuz alındı; incelemeden sonra yayınlanacak." |
| `published` | Yorum yayında | "Bu ürün için yorumunuz yayında. Teşekkürler!" |
| `rejected` | Yorum yayınlanmadı | "Yorumunuz yayınlanmadı. Size gönderdiğimiz e-postadaki bağlantıyla düzeltip yeniden gönderebilirsiniz." |
| `removed` | Yorum şu anda yayında değil | "Bu ürün için yorumunuz şu an yayında değil." |
| `waiting` | Teslimat henüz doğrulanmadı | "Ürün size ulaştıktan sonra değerlendirebilirsiniz." |
| `closed` | Bu ürün için artık yorum yazılamıyor (örneğin yazma süresi dolmuş ya da ürün iptal veya iade edilmiş) | "Bu ürün için yorum yazılamıyor." |

Sipariş bulunamazsa ya da başka bir müşteriye aitse yanıt `200` olur ve `lines` boş döner; script bu durumda bölümü hiç çizmez ve container boş kalır. Ürün sayfasındaki isteğin kuralları burada da geçerlidir: e-posta adresini oturumdan okuyun, isteği yalnızca sunucu anahtarıyla gönderin (tarayıcıdan gönderilen istek `403 refused / server_only` alır), yanıtı önbelleğe almayın. Rate limit `GET /v1/viewer` ile ortaktır.

Sunucu tarafında yanıtı bir `<script type="application/json">` etiketine güvenle yazan fonksiyon:

```ts
import "server-only";

const base = process.env.SAYPROOF_BASE || process.env.NEXT_PUBLIC_SAYPROOF_BASE;
const unsafe: Record<string, string> = { "<": "\\u003c", ">": "\\u003e", "&": "\\u0026", " ": "\\u2028", " ": "\\u2029" };

export async function sayproofOrderState(order: string, email: string | null | undefined, locale: string) {
  if (!order || !email) return null;
  const query = new URLSearchParams({ order, channel: process.env.SAYPROOF_CHANNEL ?? "web", lang: locale });
  try {
    const answer = await fetch(`${base}/v1/viewer/order?${query}`, {
      headers: { Authorization: `Bearer ${process.env.SAYPROOF_SSR_KEY}`, "X-Sayproof-Customer": email },
      cache: "no-store",
      signal: AbortSignal.timeout(3000),
    });
    if (!answer.ok) return null;
    const state = await answer.json();
    if (!state.lines.length) return null;
    const json = JSON.stringify(state).replace(/[<>&  ]/g, (character) => unsafe[character]);
    return `<script type="application/json" data-review-order-state>${json}</script>`;
  } catch {
    return null;
  }
}
```

Sayfada etiket, değeri sipariş numarası olan bir `data-review-order` öğesinin içine yazılır; sayfada Sayproof script'i de bulunmalıdır. Script öğeyi kendisi bulur:

```html
<div data-review-order="100245">
  <script type="application/json" data-review-order-state>{"order":"100245","token":"…","lines":[…]}</script>
</div>
```

Sayfa tarayıcıda oluşturuluyorsa aynı yanıt `window.Sayproof.mountOrder(öğe, yanıt)` ile verilir: [JavaScript API](/tr/reference/javascript-api.md). Her ürün ayrı gönderilir; gönderim başarılı olduğunda satır `pending` durumuna geçer, öğe `sayproof:submitted` event'ini `detail: { order, line }` ile yayar. Yıldız verilmeden gönderilmek istenirse "Göndermeden önce yıldız verin." mesajı gösterilir. Öğeler ve durumlar için: [DOM attribute'ları › Sipariş detayında değerlendirme](/tr/reference/dom-attributes.md#siparis-detayinda-degerlendirme).
