recurr.
Next.js

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şim

Kurulum

app/billing/[[...billing]]/page.tsx
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

AlanTipAçıklama
plansPlan[]Sunucudan gelen plan listesi
isPendingbooleanYükleniyor durumu
errorError | nullYükleme hatası
selectedPlanIdstringSeçili plan ID'si
setSelectedPlanId(id: string) => voidPlan seçimini değiştir
emailstringEmail input değeri
setEmail(email: string) => voidEmail değerini değiştir
goToCheckout(planId: string, email: string) => voidCheckout 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

app/billing/components/MyPlanSelector.tsx
'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

AlanTipAçıklama
planIdstringURL'den okunan plan ID'si
emailstringURL'den okunan email
testModebooleanTest modu aktif mi
phaseCheckoutPhaseMevcut aşama
errorMessagestring | nullHata mesajı
submit(fields: CardFields) => Promise<void>Ödemeyi başlat

CheckoutPhase Değerleri

DeğerAçı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

app/billing/components/MyCheckout.tsx
'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

AlanTipAçıklama
emailstringEmail input değeri
setEmail(email: string) => voidEmail değerini değiştir
lookupSubscription() => voidAbonelik aramasını başlat
subscriberSubscriber | undefinedBulunan abone bilgisi
isLookingbooleanArama devam ediyor
lookupErrorError | nullArama hatası
cancelSubscription() => voidAboneliği iptal et
isCancellingbooleanİptal işlemi devam ediyor
cancelErrorError | nullİptal hatası
cancelSuccessbooleanİ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

app/billing/components/MyPortal.tsx
'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

AlanTipAçıklama
apiBasestringBilling API base path
billingBasePathstringBilling sayfaları URL prefix'i
afterSuccessPathstringBaşarılı ödeme sonrası yönlendirme
testModebooleanTest modu aktif mi
spRecord<string, string>Normalize edilmiş URL parametreleri

Örnek — success sayfasında kullanım

app/billing/components/MySuccess.tsx
'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>
  )
}

On this page