epic-6-snap-hybrid-complete #15
|
|
@ -9,6 +9,118 @@ import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
|
|||
import { Logger } from '../lib/logger'
|
||||
import React from 'react'
|
||||
|
||||
interface AutoSnapPaymentProps {
|
||||
orderId: string
|
||||
amount: number
|
||||
customer?: { name?: string; phone?: string; email?: string }
|
||||
onChargeInitiated?: () => void
|
||||
onSuccess?: (result: any) => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSuccess, onError }: AutoSnapPaymentProps) {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [error, setError] = React.useState('')
|
||||
const hasTriggered = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasTriggered.current) return
|
||||
hasTriggered.current = true
|
||||
|
||||
const triggerPayment = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
Logger.paymentInfo('checkout.auto.snap.init', { orderId, amount, customer })
|
||||
|
||||
// Import SnapTokenService dynamically to avoid circular deps
|
||||
const { SnapTokenService } = await import('../features/payments/snap/SnapTokenService')
|
||||
|
||||
// Create Snap transaction token
|
||||
const token = await SnapTokenService.createToken({
|
||||
transaction_details: {
|
||||
order_id: orderId,
|
||||
gross_amount: amount
|
||||
},
|
||||
customer_details: customer ? {
|
||||
first_name: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone
|
||||
} : undefined,
|
||||
item_details: [{
|
||||
id: orderId,
|
||||
name: 'Payment',
|
||||
price: amount,
|
||||
quantity: 1
|
||||
}]
|
||||
})
|
||||
|
||||
Logger.paymentInfo('checkout.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
|
||||
|
||||
// Auto-trigger Snap payment popup
|
||||
if (window.snap && typeof window.snap.pay === 'function') {
|
||||
window.snap.pay(token, {
|
||||
onSuccess: (result: any) => {
|
||||
Logger.paymentInfo('checkout.auto.snap.payment.success', { orderId, transactionId: result.transaction_id })
|
||||
onSuccess?.(result)
|
||||
},
|
||||
onPending: (result: any) => {
|
||||
Logger.paymentInfo('checkout.auto.snap.payment.pending', { orderId, transactionId: result.transaction_id })
|
||||
},
|
||||
onError: (result: any) => {
|
||||
Logger.paymentError('checkout.auto.snap.payment.error', { orderId, error: result })
|
||||
const message = 'Pembayaran gagal. Silakan coba lagi.'
|
||||
setError(message)
|
||||
onError?.(result)
|
||||
},
|
||||
onClose: () => {
|
||||
Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Snap.js not loaded')
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
Logger.paymentError('checkout.auto.snap.payment.error', { orderId, error: e.message })
|
||||
const message = 'Gagal memuat pembayaran. Silakan refresh halaman.'
|
||||
setError(message)
|
||||
onError?.(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to ensure UI is rendered
|
||||
const timer = setTimeout(triggerPayment, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [orderId, amount, customer, onChargeInitiated, onSuccess, onError])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert title="Pembayaran Gagal">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent mx-auto"></div>
|
||||
<p className="text-sm text-gray-600">Menyiapkan pembayaran...</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600">
|
||||
Membuka halaman pembayaran Midtrans...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CheckoutPage() {
|
||||
const apiBase = Env.API_BASE_URL
|
||||
const clientKey = Env.MIDTRANS_CLIENT_KEY
|
||||
|
|
@ -132,7 +244,7 @@ export function CheckoutPage() {
|
|||
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-3" aria-live="polite">
|
||||
<SnapPaymentTrigger
|
||||
<AutoSnapPayment
|
||||
orderId={orderId}
|
||||
amount={amount}
|
||||
customer={{
|
||||
|
|
@ -140,7 +252,6 @@ export function CheckoutPage() {
|
|||
email: form.contact.includes('@') ? form.contact : undefined,
|
||||
phone: !form.contact.includes('@') ? form.contact : undefined
|
||||
}}
|
||||
paymentMethod={'bank_transfer'}
|
||||
onChargeInitiated={() => setLocked(true)}
|
||||
onSuccess={(result) => {
|
||||
Logger.info('checkout.payment.success', { orderId, result })
|
||||
|
|
|
|||
|
|
@ -2,16 +2,128 @@ import { useEffect, useMemo, useState } from 'react'
|
|||
import { useParams } from 'react-router-dom'
|
||||
import { PaymentSheet } from '../features/payments/components/PaymentSheet'
|
||||
import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
|
||||
import { SnapPaymentTrigger } from '../features/payments/snap/SnapPaymentTrigger'
|
||||
import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
|
||||
import { Alert } from '../components/alert/Alert'
|
||||
import { Button } from '../components/ui/button'
|
||||
import { getPaymentLinkPayload } from '../services/api'
|
||||
import { isOrderLocked, lockOrder } from '../features/payments/lib/chargeLock'
|
||||
import { usePaymentNavigation } from '../features/payments/lib/navigation'
|
||||
import React from 'react'
|
||||
|
||||
type Method = PaymentMethod | null
|
||||
|
||||
interface AutoSnapPaymentProps {
|
||||
orderId: string
|
||||
amount: number
|
||||
customer?: { name?: string; phone?: string; email?: string }
|
||||
onChargeInitiated?: () => void
|
||||
onSuccess?: (result: any) => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSuccess, onError }: AutoSnapPaymentProps) {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [error, setError] = React.useState('')
|
||||
const hasTriggered = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasTriggered.current) return
|
||||
hasTriggered.current = true
|
||||
|
||||
const triggerPayment = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
console.log('[PayPage] Auto-triggering Snap payment:', { orderId, amount, customer })
|
||||
|
||||
// Import SnapTokenService dynamically to avoid circular deps
|
||||
const { SnapTokenService } = await import('../features/payments/snap/SnapTokenService')
|
||||
|
||||
// Create Snap transaction token
|
||||
const token = await SnapTokenService.createToken({
|
||||
transaction_details: {
|
||||
order_id: orderId,
|
||||
gross_amount: amount
|
||||
},
|
||||
customer_details: customer ? {
|
||||
first_name: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone
|
||||
} : undefined,
|
||||
item_details: [{
|
||||
id: orderId,
|
||||
name: 'Payment',
|
||||
price: amount,
|
||||
quantity: 1
|
||||
}]
|
||||
})
|
||||
|
||||
console.log('[PayPage] Snap token received:', token.substring(0, 10) + '...')
|
||||
|
||||
// Auto-trigger Snap payment popup
|
||||
if (window.snap && typeof window.snap.pay === 'function') {
|
||||
window.snap.pay(token, {
|
||||
onSuccess: (result: any) => {
|
||||
console.log('[PayPage] Payment success:', result)
|
||||
onSuccess?.(result)
|
||||
},
|
||||
onPending: (result: any) => {
|
||||
console.log('[PayPage] Payment pending:', result)
|
||||
},
|
||||
onError: (result: any) => {
|
||||
console.error('[PayPage] Payment error:', result)
|
||||
const message = 'Pembayaran gagal. Silakan coba lagi.'
|
||||
setError(message)
|
||||
onError?.(result)
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('[PayPage] Snap popup closed')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error('Snap.js not loaded')
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('[PayPage] Auto-payment error:', e.message)
|
||||
const message = 'Gagal memuat pembayaran. Silakan refresh halaman.'
|
||||
setError(message)
|
||||
onError?.(e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to ensure UI is rendered
|
||||
const timer = setTimeout(triggerPayment, 500)
|
||||
return () => clearTimeout(timer)
|
||||
}, [orderId, amount, customer, onChargeInitiated, onSuccess, onError])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<Alert title="Pembayaran Gagal">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent mx-auto"></div>
|
||||
<p className="text-sm text-gray-600">Menyiapkan pembayaran...</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-600">
|
||||
Membuka halaman pembayaran Midtrans...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function PayPage() {
|
||||
const { token } = useParams()
|
||||
const nav = usePaymentNavigation()
|
||||
|
|
@ -108,11 +220,10 @@ export function PayPage() {
|
|||
)}
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-3" aria-live="polite">
|
||||
<SnapPaymentTrigger
|
||||
<AutoSnapPayment
|
||||
orderId={orderId}
|
||||
amount={amount}
|
||||
customer={customer}
|
||||
paymentMethod={selectedMethod || 'bank_transfer'}
|
||||
onChargeInitiated={() => {
|
||||
lockOrder(orderId)
|
||||
setLocked(true)
|
||||
|
|
|
|||
Loading…
Reference in New Issue