Sayfa Özelleştirme
Headless hook'larla kendi UI'ınızı billing sistemiyle entegre edin
Genel Bakış
Billing sayfalarının 4 tanesi kendi bileşeninizle tamamen değiştirilebilir. Override ettiğiniz bileşen hiç prop almaz — tüm veri, durum ve aksiyonlara headless hook'lar üzerinden ulaşırsınız.
usePlans() → choose-plan sayfası için
useCheckout() → checkout sayfası için
usePortal() → portal sayfası için
usePageContext() → tüm sayfalarda config değerlerine erişimKurulum
import { BillingPortal } from '@vinenastudio/recurr-nextjs/ui'
import { MyPlanSelector } from './components/MyPlanSelector'
import { MyCheckout } from './components/MyCheckout'
import { MySuccess } from './components/MySuccess'
import { MyPortal } from './components/MyPortal'
export default BillingPortal({
apiBase: '/api/billing',
afterSuccessPath: '/dashboard',
pages: {
'choose-plan': MyPlanSelector,
checkout: MyCheckout,
success: MySuccess,
portal: MyPortal,
},
})Her custom bileşen "use client" direktifine sahip olmalı ve QueryProvider kapsamı içinde çalışır (framework tarafından otomatik sarmalanır).
usePlans()
choose-plan sayfası için plan listesi, seçim durumu ve checkout yönlendirmesini sağlar.
import { usePlans } from '@vinenastudio/recurr-nextjs/ui'Dönüş Değerleri
| Alan | Tip | Açıklama |
|---|---|---|
plans | Plan[] | Sunucudan gelen plan listesi |
isPending | boolean | Yükleniyor durumu |
error | Error | null | Yükleme hatası |
selectedPlanId | string | Seçili plan ID'si |
setSelectedPlanId | (id: string) => void | Plan seçimini değiştir |
email | string | Email input değeri |
setEmail | (email: string) => void | Email değerini değiştir |
goToCheckout | (planId: string, email: string) => void | Checkout sayfasına yönlendir |
Plan Tipi
interface Plan {
id: string
name: string
amount: number // kuruş cinsinden (100 = 1 TL/USD)
currency: string
interval: string // "monthly" | "yearly" | "weekly"
trialDays?: number
}Örnek
'use client'
import { usePlans } from '@vinenastudio/recurr-nextjs/ui'
export function MyPlanSelector() {
const {
plans, isPending, error,
selectedPlanId, setSelectedPlanId,
email, setEmail, goToCheckout,
} = usePlans()
if (isPending) return <div>Yükleniyor…</div>
if (error) return <div>Hata: {error.message}</div>
return (
<form onSubmit={(e) => { e.preventDefault(); goToCheckout(selectedPlanId, email) }}>
{plans.map((plan) => (
<button key={plan.id} type="button" onClick={() => setSelectedPlanId(plan.id)}>
{plan.name}
</button>
))}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Devam Et</button>
</form>
)
}useCheckout()
checkout sayfası için kart formu durumu ve PayTR ödeme akışını yönetir.
planId ve email değerlerini URL parametrelerinden otomatik okur.
import { useCheckout, TEST_CARDS } from '@vinenastudio/recurr-nextjs/ui'Dönüş Değerleri
| Alan | Tip | Açıklama |
|---|---|---|
planId | string | URL'den okunan plan ID'si |
email | string | URL'den okunan email |
testMode | boolean | Test modu aktif mi |
phase | CheckoutPhase | Mevcut aşama |
errorMessage | string | null | Hata mesajı |
submit | (fields: CardFields) => Promise<void> | Ödemeyi başlat |
CheckoutPhase Değerleri
| Değer | Açıklama |
|---|---|
"idle" | Bekleme — form gösterilir |
"loading" | Sunucudan token hazırlanıyor |
"submitting" | PayTR'ye form gönderiliyor |
"error" | Hata oluştu |
CardFields Tipi
interface CardFields {
ccOwner: string
cardNumber: string // "XXXX XXXX XXXX XXXX" formatında (boşluklu)
expiryMonth: string // "01"–"12"
expiryYear: string // "25"–"36" (2 hane)
cvv: string
}TEST_CARDS Sabiti
testMode: true iken form geliştirme kolaylığı için önceden doldurulmuş test kartları:
import { TEST_CARDS } from '@vinenastudio/recurr-nextjs/ui'
// TEST_CARDS: TestCard[] — label, cardNumber, expiryMonth, expiryYear, cvv, ccOwnerÖrnek
'use client'
import { useState } from 'react'
import { useCheckout, TEST_CARDS, type CardFields } from '@vinenastudio/recurr-nextjs/ui'
export function MyCheckout() {
const { testMode, phase, errorMessage, submit } = useCheckout()
const [form, setForm] = useState<CardFields>({
ccOwner: '', cardNumber: '', expiryMonth: '', expiryYear: '', cvv: '',
})
return (
<form onSubmit={(e) => { e.preventDefault(); void submit(form) }}>
{testMode && TEST_CARDS.map((card) => (
<button key={card.cardNumber} type="button"
onClick={() => setForm({ ccOwner: card.ccOwner, cardNumber: card.cardNumber,
expiryMonth: card.expiryMonth, expiryYear: card.expiryYear, cvv: card.cvv })}>
{card.label}
</button>
))}
{phase === 'error' && <p>{errorMessage}</p>}
<input value={form.ccOwner} onChange={(e) => setForm(p => ({ ...p, ccOwner: e.target.value }))} />
{/* ... diğer alanlar */}
<button type="submit" disabled={phase === 'loading' || phase === 'submitting'}>
{phase === 'loading' ? 'Hazırlanıyor…' : 'Ödemeyi Tamamla'}
</button>
</form>
)
}submit() çağrıldığında framework sunucudan PayTR token'ı alır ve kartı doğrudan PayTR'ye gönderir — kart bilgileri hiçbir zaman sizin sunucunuza iletilmez.
usePortal()
portal sayfası için abonelik sorgulama ve iptal işlemlerini yönetir.
import { usePortal } from '@vinenastudio/recurr-nextjs/ui'Dönüş Değerleri
| Alan | Tip | Açıklama |
|---|---|---|
email | string | Email input değeri |
setEmail | (email: string) => void | Email değerini değiştir |
lookupSubscription | () => void | Abonelik aramasını başlat |
subscriber | Subscriber | undefined | Bulunan abone bilgisi |
isLooking | boolean | Arama devam ediyor |
lookupError | Error | null | Arama hatası |
cancelSubscription | () => void | Aboneliği iptal et |
isCancelling | boolean | İptal işlemi devam ediyor |
cancelError | Error | null | İptal hatası |
cancelSuccess | boolean | İptal başarılı |
Subscriber Tipi
interface Subscriber {
id: string
email: string
planId: string
status: string // "active" | "trial" | "paused" | "past_due" | "cancelled"
nextBillingDate: string // ISO 8601
cancelledAt?: string // ISO 8601
}Örnek
'use client'
import { usePortal } from '@vinenastudio/recurr-nextjs/ui'
export function MyPortal() {
const {
email, setEmail, lookupSubscription,
subscriber, isLooking, lookupError,
cancelSubscription, isCancelling, cancelSuccess,
} = usePortal()
return (
<div>
<form onSubmit={(e) => { e.preventDefault(); lookupSubscription() }}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">{isLooking ? 'Aranıyor…' : 'Sorgula'}</button>
</form>
{lookupError && <p>{lookupError.message}</p>}
{cancelSuccess && <p>Aboneliğiniz iptal edildi.</p>}
{subscriber && (
<div>
<p>{subscriber.email} — {subscriber.status}</p>
{subscriber.status !== 'cancelled' && (
<button onClick={cancelSubscription} disabled={isCancelling}>
{isCancelling ? 'İptal ediliyor…' : 'Aboneliği İptal Et'}
</button>
)}
</div>
)}
</div>
)
}usePageContext()
BillingPortal konfigürasyonuna herhangi bir custom bileşenden erişir.
import { usePageContext } from '@vinenastudio/recurr-nextjs/ui'Dönüş Değerleri
| Alan | Tip | Açıklama |
|---|---|---|
apiBase | string | Billing API base path |
billingBasePath | string | Billing sayfaları URL prefix'i |
afterSuccessPath | string | Başarılı ödeme sonrası yönlendirme |
testMode | boolean | Test modu aktif mi |
sp | Record<string, string> | Normalize edilmiş URL parametreleri |
Örnek — success sayfasında kullanım
'use client'
import Link from 'next/link'
import { usePageContext } from '@vinenastudio/recurr-nextjs/ui'
export function MySuccess() {
const { afterSuccessPath } = usePageContext()
return (
<div>
<h1>Ödeme başarılı!</h1>
<Link href={afterSuccessPath}>Dashboard'a dön</Link>
</div>
)
}