Kurulum
npx trkit add odeme odeme-onaylari.env.local
TRKIT_PAY_PROVIDER=iyzico # iyzico | paytr | mock
IYZICO_API_KEY=sandbox-...
IYZICO_SECRET_KEY=sandbox-...
IYZICO_SANDBOX=1Geliştirmede TRKIT_PAY_PROVIDER=mock ile gerçek sağlayıcı olmadan tüm akışı deneyebilirsiniz.
Akış
Sepet → Ödeme sayfası (onaylar) → createCheckout() → Sağlayıcı formu
→ Sağlayıcı /api/odeme/callback'e bildirir → verifyCallback() → sipariş güncellenir
→ Kullanıcı sonuç sayfasına yönlendirilir1. Ödeme sayfası ve onaylar
Mesafeli Sözleşmeler Yönetmeliği'ne göre ön bilgilendirme formu, sipariş onayından önce ve sipariş bilgileriyle gösterilmeli:
app/odeme/page.tsx
import { generate, toHtml } from "@trkit-app-pack/legal";
import config from "@/trkit.config";
import { OdemeOnaylari } from "@/components/trkit/odeme-onaylari";
export default async function OdemeSayfasi() {
const sepet = await sepetiGetir();
const order = {
no: sepet.id, tarih: new Date().toISOString(), odemeYontemi: "Kredi kartı",
alici: { adSoyad: sepet.adSoyad, adres: sepet.adres, telefon: sepet.telefon, eposta: sepet.eposta },
urunler: sepet.kalemler.map((k) => ({ ad: k.ad, adet: k.adet, birimFiyat: k.fiyatKurus })),
kargoUcreti: sepet.kargoKurus,
};
return (
<form action={odemeyiBaslat}>
<OdemeOnaylari
onBilgilendirmeHtml={toHtml(generate("on-bilgilendirme", config, { order }).document)}
mesafeliSatisHtml={toHtml(generate("mesafeli-satis", config, { order }).document)}
/>
<button>Ödemeye geç</button>
</form>
);
}2. Ödemeyi başlatma (sunucu)
app/odeme/actions.ts
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { pay } from "@/lib/pay";
export async function odemeyiBaslat(formData: FormData) {
if (formData.get("onay[sozlesme]") !== "on") throw new Error("Sözleşme onayı gerekli");
const sepet = await sepetiGetir();
const ip = (await headers()).get("x-forwarded-for")?.split(",")[0] ?? "127.0.0.1";
const session = await pay.createCheckout({
orderId: sepet.id, // PayTR: yalnızca harf ve rakam
amount: sepet.toplamKurus, // kalemlerin toplamına eşit olmalı
buyer: {
id: sepet.uyeId, name: sepet.ad, surname: sepet.soyad,
email: sepet.eposta, phone: sepet.telefon, ip,
address: { city: sepet.il, district: sepet.ilce, line: sepet.adres },
},
items: [
...sepet.kalemler.map((k) => ({ id: k.id, name: k.ad, category: k.kategori, unitPrice: k.fiyatKurus, quantity: k.adet })),
{ id: "kargo", name: "Kargo", category: "Kargo", unitPrice: sepet.kargoKurus, type: "virtual" as const },
],
callbackUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/api/odeme/callback`,
successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/odeme/basarili`,
failUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/odeme/basarisiz`,
installments: [1, 2, 3, 6],
});
if (session.paymentPageUrl) redirect(session.paymentPageUrl); // iyzico
redirect(`/odeme/form?token=${session.token}`); // PayTR: iframeUrl'i <iframe> ile göster
}3. Callback
npx trkit add odeme ile gelen app/api/odeme/callback/route.ts bildirimi doğrular. Yapmanız gereken siparisiGuncelle() fonksiyonunu doldurmak:
async function siparisiGuncelle(result: PaymentResult) {
const siparis = await db.siparis.find(result.orderId);
if (!siparis || siparis.durum === "odendi") return; // idempotent
if (result.status !== "success") return db.siparis.update(result.orderId, { durum: "basarisiz" });
if (result.paidAmount! < siparis.toplamKurus) throw new Error("Tutar uyuşmuyor");
await db.siparis.update(result.orderId, { durum: "odendi", odemeId: result.paymentId });
}Güvenlik kuralları
- Başarı sayfasına güvenmeyin.
/odeme/basariliadresine herkes gidebilir. Siparişin ödendiğine yalnızca callback karar verir. - Sırlar sunucuda.
lib/pay.tsserver-onlyiçerir; client component'ten içe aktarılırsa derleme hata verir. - Tutarı sunucuda hesaplayın. Formdan gelen fiyatı kullanmayın.
- Kuruş kullanın.
0.1 + 0.2 !== 0.3.toKurus("129,50")→12950. - PayTR "OK" bekler.
callbackResponse()bunu döndürür; aynı bildirim tekrar gelebilir. - iyzico fraud incelemesi.
fraudStatusincelemedeyse sonuçpendingdöner; siparişi kargoya vermeyin.
Sağlayıcı değiştirmek
.env.local içinde TRKIT_PAY_PROVIDER=paytr ve PayTR anahtarları. PayTR panelinde Bildirim URL'sini https://alanadiniz/api/odeme/callback olarak ayarlayın. Kodunuz değişmez.
İade
await pay.refund({ paymentId: siparis.odemeId, amount: 5900, ip });