chore: update iyzico payment processing and database schema
All checks were successful
Deploy / deploy (push) Successful in 9m3s
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:
31
src/app/odeme/page.tsx
Normal file
31
src/app/odeme/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/features/odeme/components/iyzico-formu.tsx
Normal file
49
src/features/odeme/components/iyzico-formu.tsx
Normal 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" />;
|
||||
}
|
||||
115
src/features/odeme/components/odeme-sayfasi.tsx
Normal file
115
src/features/odeme/components/odeme-sayfasi.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")));
|
||||
|
||||
Reference in New Issue
Block a user