نمونه کد فرانتاند
دو صفحهی فرانت لازم دارید:
- Checkout — کاربر پلن یا کالا را انتخاب میکند و به درگاه میرود.
- Callback — کاربر بعد از پرداخت به اینجا برمیگردد و وضعیت تراکنش به او نشان داده میشود.
نمونهها در React (Next.js)، Vue 3 و Angular ارائه شدهاند. منطق در همهشان یکسان است — تفاوت در syntax UI framework است.
این نمونهها از Tailwind برای استایل استفاده میکنند، اما هیچ بخش از منطق پرداخت به Tailwind وابسته نیست. میتوانید کلاسهای CSS را با هر styling system دیگری جایگزین کنید.
صفحهی Checkout
React (Next.js)
'use client'
import { useEffect, useState } from 'react'
interface Plan {
id: string
name: string
price_toman: number
final_price_toman: number
discount: number
duration_days: number
}
export default function CheckoutPage() {
const [plans, setPlans] = useState<Plan[]>([])
const [selected, setSelected] = useState('premium_monthly')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('/api/payment/plans', { headers: authHeaders() })
.then(r => r.json())
.then(d => setPlans(d.plans))
.catch(() => setError('خطا در دریافت پلنها'))
}, [])
const handlePayment = async () => {
setLoading(true); setError(null)
try {
const res = await fetch('/api/payment/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ plan: selected }),
})
if (!res.ok) throw new Error('خطا در اتصال به درگاه')
const data = await res.json()
window.location.href = data.url
} catch (e: any) {
setError(e.message)
setLoading(false)
}
}
const selectedPlan = plans.find(p => p.id === selected)
return (
<div className="container mx-auto p-4" dir="rtl">
<h1 className="text-2xl font-bold mb-6">انتخاب پلن</h1>
<div className="grid md:grid-cols-2 gap-4 mb-6">
{plans.map(plan => (
<PlanCard
key={plan.id}
plan={plan}
selected={selected === plan.id}
onSelect={() => setSelected(plan.id)}
/>
))}
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 p-4 rounded-lg mb-4">
{error}
</div>
)}
<button
onClick={handlePayment}
disabled={loading || !selectedPlan}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold py-3 rounded-lg"
>
{loading ? 'در حال اتصال به درگاه...' : 'پرداخت'}
</button>
</div>
)
}
function PlanCard({ plan, selected, onSelect }: {
plan: Plan; selected: boolean; onSelect: () => void
}) {
return (
<div
onClick={onSelect}
className={`border-2 rounded-lg p-6 cursor-pointer transition-all ${
selected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-blue-300'
}`}
>
<h3 className="text-xl font-semibold mb-2">{plan.name}</h3>
<p className="text-gray-600 mb-4">
{plan.duration_days === 30 ? '۳۰ روز' : '۳۶۵ روز'}
</p>
{plan.discount > 0 && (
<div className="mb-2">
<span className="line-through text-gray-400">
{plan.price_toman.toLocaleString('fa-IR')} تومان
</span>
<span className="mr-2 text-green-600 font-bold">{plan.discount}٪ تخفیف</span>
</div>
)}
<p className="text-2xl font-bold text-blue-600">
{plan.final_price_toman.toLocaleString('fa-IR')} تومان
</p>
</div>
)
}
function authHeaders() {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : ''
return token ? { Authorization: `Bearer ${token}` } : {}
}صفحهی Callback
این صفحه پس از بازگشت از درگاه باز میشود. سه حالت دارد: در حال بررسی، موفق، ناموفق.
React (Next.js)
'use client'
import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
type Status = 'verifying' | 'success' | 'failed'
function CallbackContent() {
const params = useSearchParams()
const router = useRouter()
const [status, setStatus] = useState<Status>('verifying')
useEffect(() => {
const hashId = params.get('hash_id')
const referenceId = params.get('reference_id')
const callbackStatus = params.get('status')
if (callbackStatus === 'failed' || !hashId || !referenceId) {
setStatus('failed')
return
}
const token = localStorage.getItem('token') || ''
fetch('/api/payment/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ hash_id: hashId, reference_id: referenceId }),
})
.then(r => r.json())
.then(d => setStatus(d.success ? 'success' : 'failed'))
.catch(() => setStatus('failed'))
}, [])
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50" dir="rtl">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
{status === 'verifying' && <p>در حال تایید پرداخت...</p>}
{status === 'success' && (
<>
<h1 className="text-xl font-bold mb-2">پرداخت موفق</h1>
<p className="text-gray-600 mb-6">اشتراک شما فعال شد.</p>
<button
onClick={() => router.push('/')}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-lg"
>
ادامه
</button>
</>
)}
{status === 'failed' && (
<>
<h1 className="text-xl font-bold mb-2">پرداخت ناموفق</h1>
<button
onClick={() => router.push('/payment/checkout')}
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-lg"
>
پرداخت مجدد
</button>
</>
)}
</div>
</div>
)
}
export default function CallbackPage() {
return (
<Suspense fallback={<p>...</p>}>
<CallbackContent />
</Suspense>
)
}صفحهی Callback را همیشه با /payment/verify در backend خود ست کنید — حتی اگر status=success در URL باشد. وضعیت در URL قابل اعتماد نیست؛ تنها پاسخ verify از درگاه معتبر است.
Last updated on