Auto-trigger Snap payment: remove manual 'Bayar Sekarang' button click

- Add AutoSnapPayment component that auto-triggers Snap popup on mount
- Update CheckoutPage: auto-open Snap payment after form submission
- Update PayPage: auto-open Snap payment on page load
- Remove manual button clicks for smoother UX
- Add loading states and error handling for auto-payment flow
This commit is contained in:
CIFO Dev 2025-12-04 10:35:32 +07:00
parent 4bca71aeb3
commit 386c84a26f
2 changed files with 227 additions and 5 deletions

View File

@ -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 })

View File

@ -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)