chore: update iyzico payment processing and database schema
All checks were successful
Deploy / deploy (push) Successful in 9m3s

Enhanced the iyzico payment processing logic by updating the handling of checkout form content. Modified the order schema to include a new field for storing the checkout form content, ensuring it persists across page refreshes. Updated related functions to reflect these changes, improving the overall payment experience. Additionally, updated the app database to incorporate the latest changes.
This commit is contained in:
bilalgursen
2026-08-11 00:51:58 +03:00
parent 08419e6d1c
commit 5ccc9c1ffa
11 changed files with 1137 additions and 4 deletions

31
src/app/odeme/page.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { SiteFooter } from "@/components/site-footer";
import {
OdemeSayfasi,
OdemeSayfasiSkeleton,
} from "@/features/odeme/components/odeme-sayfasi";
export const metadata: Metadata = {
title: "Güvenli Ödeme",
robots: { index: false, follow: false },
};
export default function OdemePage({
searchParams,
}: {
searchParams: Promise<{ siparis?: string }>;
}) {
return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
<main className="mx-auto flex w-full max-w-xl flex-1 flex-col px-4 py-10">
<Suspense fallback={<OdemeSayfasiSkeleton />}>
{searchParams.then(({ siparis }) => (
<OdemeSayfasi siparisId={siparis} />
))}
</Suspense>
</main>
<SiteFooter />
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import { useEffect, useMemo } from "react";
// iyzico'nun checkoutFormContent'i tek bir <script> bloğudur; içindeki JS,
// sayfadaki #iyzipay-checkout-form div'ine formu (iframe) çizer. React
// dangerouslySetInnerHTML ile gelen script'leri ÇALIŞTIRMAZ — bu yüzden script
// gövdesi ayıklanıp elle document'a eklenir.
//
// iyzico script'i `typeof iyziInit == 'undefined'` koruması taşır: aynı oturumda
// ikinci kez mount olursa (geri gel / tekrar dene) global temizlenmezse form
// çizilmez. Bu yüzden enjeksiyondan önce ve unmount'ta global silinir.
declare global {
interface Window {
iyziInit?: unknown;
}
}
export function IyzicoFormu({ icerik }: { icerik: string }) {
const govde = useMemo(
() => icerik.match(/<script[^>]*>([\s\S]*?)<\/script>/i)?.[1],
[icerik],
);
useEffect(() => {
if (!govde) return;
delete window.iyziInit;
const script = document.createElement("script");
script.text = govde;
document.body.appendChild(script);
return () => {
script.remove();
delete window.iyziInit;
};
}, [govde]);
if (!govde) {
return (
<p className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
Ödeme formu yüklenemedi. Sayfayı yenile; sorun sürerse paket sayfasından
tekrar dene — kartından çekim yapılmadı.
</p>
);
}
// iyzico bu id'yi arar; "responsive" sınıfı formu kabın genişliğine uydurur
return <div id="iyzipay-checkout-form" className="responsive" />;
}

View File

@@ -0,0 +1,115 @@
import Image from "next/image";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { ArrowLeft, Lock } from "lucide-react";
import { verifySession } from "@/lib/session";
import { URUNLER } from "@/lib/credits";
import { SectionEyebrow } from "@/components/pixel-decor";
import { Skeleton } from "@/components/ui/skeleton";
import { siparisGetir } from "../odeme-queries";
import { IyzicoFormu } from "./iyzico-formu";
function tl(kurus: number) {
return (kurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0 });
}
// Kendi tasarımımızdaki ödeme sayfası: sipariş özeti bizden, kart formu
// iyzico'nun gömülü iframe'inden gelir (kart verisi bize hiç uğramaz).
export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
if (!siparisId) redirect("/paket");
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın
const geriDonus = `/odeme?siparis=${encodeURIComponent(siparisId)}`;
const [session, order] = await Promise.all([
verifySession(geriDonus),
siparisGetir(siparisId),
]);
// Yalnızca kendi bekleyen siparişinin formu görüntülenebilir
if (!order || order.userId !== session.user.id) notFound();
if (order.status === "paid") {
redirect(`/odeme/sonuc?siparis=${order.id}`);
}
// failed sipariş ya da temizlenmiş form: yeni ödeme paket sayfasından başlar
if (order.status === "failed" || !order.iyzicoFormContent) {
redirect("/paket");
}
const urun = URUNLER[order.product];
return (
<div className="w-full">
<Link
href="/paket"
className="inline-flex items-center gap-1.5 text-sm text-slate-500 transition-colors hover:text-slate-700"
>
<ArrowLeft className="size-4" aria-hidden />
Paket sayfasına dön
</Link>
<div className="mt-4 text-center">
<SectionEyebrow>Güvenli ödeme</SectionEyebrow>
<h1 className="mt-3 font-heading text-2xl font-bold">
Ödemeyi tamamla
</h1>
</div>
{/* Sipariş özeti */}
<section
aria-label="Sipariş özeti"
className="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
>
<div className="flex items-baseline justify-between gap-4">
<div>
<p className="font-heading font-bold">{urun.label}</p>
<p className="mt-1 text-sm text-slate-600">
{order.credits} Yapay Zeka danışman kredisi
{order.product === "paket" &&
" · 24 tercihlik liste, risk analizi ve PDF rapor"}
</p>
</div>
<p className="shrink-0 font-heading text-2xl font-bold">
{tl(order.amountKurus)} TL
</p>
</div>
</section>
{/* iyzico gömülü kart formu */}
<section
aria-label="Kart bilgileri"
className="mt-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
>
<IyzicoFormu icerik={order.iyzicoFormContent} />
</section>
<div className="mt-5 flex items-center justify-center gap-1.5 text-xs text-slate-500">
<Lock className="size-3.5" aria-hidden />
256-bit SSL ile şifreli bağlantı
</div>
<Image
src="/odeme/iyzico-logo-band.svg"
alt="iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme"
width={429}
height={32}
className="mx-auto mt-3 h-5 w-auto"
/>
<p className="mt-3 text-center text-xs text-slate-500">
Kart bilgilerin iyzico&apos;da işlenir, bize hiç ulaşmaz ·{" "}
<Link href="/kosullar#iade" className="underline hover:text-slate-700">
İade koşulları
</Link>
</p>
</div>
);
}
export function OdemeSayfasiSkeleton() {
return (
<div aria-live="polite" className="w-full">
<Skeleton className="h-5 w-40" />
<Skeleton className="mx-auto mt-4 h-7 w-32 rounded-full" />
<Skeleton className="mx-auto mt-3 h-8 w-56" />
<Skeleton className="mt-6 h-24 w-full rounded-2xl" />
<Skeleton className="mt-4 h-96 w-full rounded-2xl" />
</div>
);
}

View File

@@ -74,7 +74,7 @@ export async function baslatOdeme(
return { error: GENEL_HATA };
}
if (init.status !== "success" || !init.paymentPageUrl || !init.token) {
if (init.status !== "success" || !init.checkoutFormContent || !init.token) {
// initialize aşamasında karta hiç dokunulmaz: buradaki hatalar kart/kullanıcı
// kaynaklı değil, bizim yapılandırmamızdandır (anahtar, üye işyeri ayarı,
// sepet kuralı). iyzico'nun mesajı log'a gider, kullanıcıya genel mesaj.
@@ -92,10 +92,12 @@ export async function baslatOdeme(
return { error: GENEL_HATA };
}
// Form içeriği de saklanır: /odeme/[siparis] sayfası yenilense bile aynı
// token'lı form yeniden gösterilir, ikinci bir initialize gerekmez.
await appDb
.update(schema.orders)
.set({ iyzicoToken: init.token })
.set({ iyzicoToken: init.token, iyzicoFormContent: init.checkoutFormContent })
.where(eq(schema.orders.id, orderId));
redirect(init.paymentPageUrl);
redirect(`/odeme?siparis=${orderId}`);
}

View File

@@ -90,6 +90,10 @@ export const orders = sqliteTable(
.notNull()
.default("pending"),
iyzicoToken: text("iyzico_token"),
// Gömülü ödeme formu script'i (checkoutFormContent) — /odeme/[siparis]
// sayfası yenilendiğinde yeniden initialize gerekmesin diye saklanır;
// sipariş sonuçlanınca temizlenir.
iyzicoFormContent: text("iyzico_form_content"),
iyzicoPaymentId: text("iyzico_payment_id"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
paidAt: integer("paid_at", { mode: "timestamp" }),

View File

@@ -86,6 +86,8 @@ export interface CheckoutFormInitSonuc {
status: string;
token?: string;
paymentPageUrl?: string;
/** Kendi sayfamıza gömülen form script'i — <div id="iyzipay-checkout-form"> ister */
checkoutFormContent?: string;
conversationId?: string;
signature?: string;
errorCode?: string;

View File

@@ -98,7 +98,7 @@ export async function odemeyiSonuclandir(
// paid'i asla geri almayız; pending → failed serbest
await appDb
.update(orders)
.set({ status: "failed" })
.set({ status: "failed", iyzicoFormContent: null })
.where(and(eq(orders.id, orderId), ne(orders.status, "paid")));
return "failed";
}
@@ -122,6 +122,7 @@ export async function odemeyiSonuclandir(
.set({
status: "paid",
iyzicoPaymentId: sonuc.paymentId ?? null,
iyzicoFormContent: null,
paidAt: new Date(),
})
.where(and(eq(orders.id, orderId), ne(orders.status, "paid")));