Fix Snap payment flow: direct to payment UI in step 2 and add customer/item details to API request
- Remove intermediate payment method selection step in PayPage and CheckoutPage - Update PayPage: direct step 2 payment interface (no wizard) - Update CheckoutPage: 2-step wizard (form → payment) - Add customer details (name, email, phone) to SnapPaymentTrigger - Add item_details to Snap token request (required by Midtrans API) - Fix 400 error from /api/payments/snap/token endpoint - Clean up unused imports and variables - Create payment link generation script (scripts/create-test-payment.mjs)
This commit is contained in:
parent
d051c46ac4
commit
4bca71aeb3
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:8000/createtransaksi'
|
||||||
|
const API_KEY = 'dev-key'
|
||||||
|
|
||||||
|
const orderId = `SNAPTEST-${Date.now()}`
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
mercant_id: 'TESTMERCHANT',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
deskripsi: 'Testing Snap Payment Mode',
|
||||||
|
nominal: 150000,
|
||||||
|
nama: 'Test Snap User',
|
||||||
|
no_telepon: '081234567890',
|
||||||
|
email: 'test@snap.com',
|
||||||
|
item: [
|
||||||
|
{
|
||||||
|
item_id: orderId,
|
||||||
|
nama: 'Test Product Snap',
|
||||||
|
harga: 150000,
|
||||||
|
qty: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': API_KEY
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text()
|
||||||
|
console.error('❌ Error:', response.status, error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const paymentUrl = data.data?.url || data.payment_url
|
||||||
|
const token = paymentUrl ? paymentUrl.split('/pay/')[1] : null
|
||||||
|
|
||||||
|
console.log('✅ Payment link created successfully!')
|
||||||
|
console.log('\n🔗 Snap Mode Payment Link:')
|
||||||
|
console.log(paymentUrl.replace('https://midtrans-cifo.winteraccess.id', 'http://localhost:5173'))
|
||||||
|
console.log('\n📋 Order ID:', orderId)
|
||||||
|
console.log('💰 Amount: Rp 150,000')
|
||||||
|
console.log('🔑 Mode: SNAP (Hosted UI)')
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
console.log('\n📄 Token:', token.substring(0, 50) + '...')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to create payment link:', error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
import { AppLayout } from './AppLayout'
|
import { AppLayout } from './AppLayout'
|
||||||
// import { CheckoutPage } from '../pages/CheckoutPage'
|
import { CheckoutPage } from '../pages/CheckoutPage'
|
||||||
import { PaymentStatusPage } from '../pages/PaymentStatusPage'
|
import { PaymentStatusPage } from '../pages/PaymentStatusPage'
|
||||||
import { PaymentHistoryPage } from '../pages/PaymentHistoryPage'
|
import { PaymentHistoryPage } from '../pages/PaymentHistoryPage'
|
||||||
import { NotFoundPage } from '../pages/NotFoundPage'
|
import { NotFoundPage } from '../pages/NotFoundPage'
|
||||||
// import { DemoStorePage } from '../pages/DemoStorePage'
|
import { DemoStorePage } from '../pages/DemoStorePage'
|
||||||
import { InitPage } from '../pages/InitialPage'
|
import { InitPage } from '../pages/InitialPage'
|
||||||
import { PayPage } from '../pages/PayPage'
|
import { PayPage } from '../pages/PayPage'
|
||||||
|
|
||||||
|
|
@ -15,7 +15,8 @@ const router = createBrowserRouter([
|
||||||
errorElement: <div role="alert">Terjadi kesalahan. Coba muat ulang.</div>,
|
errorElement: <div role="alert">Terjadi kesalahan. Coba muat ulang.</div>,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <InitPage /> },
|
{ index: true, element: <InitPage /> },
|
||||||
// { path: 'checkout', element: <CheckoutPage /> },
|
{ path: 'checkout', element: <CheckoutPage /> },
|
||||||
|
{ path: 'demo', element: <DemoStorePage /> },
|
||||||
{ path: 'pay/:token', element: <PayPage /> },
|
{ path: 'pay/:token', element: <PayPage /> },
|
||||||
{ path: 'payments/:orderId/status', element: <PaymentStatusPage /> },
|
{ path: 'payments/:orderId/status', element: <PaymentStatusPage /> },
|
||||||
{ path: 'history', element: <PaymentHistoryPage /> },
|
{ path: 'history', element: <PaymentHistoryPage /> },
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { SnapTokenService } from './SnapTokenService'
|
||||||
interface SnapPaymentTriggerProps {
|
interface SnapPaymentTriggerProps {
|
||||||
orderId: string
|
orderId: string
|
||||||
amount: number
|
amount: number
|
||||||
|
customer?: { name?: string; phone?: string; email?: string }
|
||||||
paymentMethod?: string
|
paymentMethod?: string
|
||||||
onSuccess?: (result: any) => void
|
onSuccess?: (result: any) => void
|
||||||
onError?: (error: any) => void
|
onError?: (error: any) => void
|
||||||
|
|
@ -19,6 +20,7 @@ interface SnapPaymentTriggerProps {
|
||||||
export function SnapPaymentTrigger({
|
export function SnapPaymentTrigger({
|
||||||
orderId,
|
orderId,
|
||||||
amount,
|
amount,
|
||||||
|
customer,
|
||||||
paymentMethod,
|
paymentMethod,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
|
|
@ -43,6 +45,7 @@ export function SnapPaymentTrigger({
|
||||||
<SnapHostedPayment
|
<SnapHostedPayment
|
||||||
orderId={orderId}
|
orderId={orderId}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
|
customer={customer}
|
||||||
onSuccess={onSuccess}
|
onSuccess={onSuccess}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
|
|
@ -105,7 +108,7 @@ function CorePaymentComponent({ paymentMethod, orderId, amount, onChargeInitiate
|
||||||
return <Component orderId={orderId} amount={amount} onChargeInitiated={onChargeInitiated} />
|
return <Component orderId={orderId} amount={amount} onChargeInitiated={onChargeInitiated} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function SnapHostedPayment({ orderId, amount, onSuccess, onError }: Omit<SnapPaymentTriggerProps, 'paymentMethod' | 'onChargeInitiated'>) {
|
function SnapHostedPayment({ orderId, amount, customer, onSuccess, onError }: Omit<SnapPaymentTriggerProps, 'paymentMethod' | 'onChargeInitiated'>) {
|
||||||
const [loading, setLoading] = React.useState(false)
|
const [loading, setLoading] = React.useState(false)
|
||||||
const [error, setError] = React.useState('')
|
const [error, setError] = React.useState('')
|
||||||
|
|
||||||
|
|
@ -114,7 +117,7 @@ function SnapHostedPayment({ orderId, amount, onSuccess, onError }: Omit<SnapPay
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
Logger.paymentInfo('snap.payment.init', { orderId, amount })
|
Logger.paymentInfo('snap.payment.init', { orderId, amount, customer })
|
||||||
|
|
||||||
// Create Snap transaction token using service
|
// Create Snap transaction token using service
|
||||||
const token = await SnapTokenService.createToken({
|
const token = await SnapTokenService.createToken({
|
||||||
|
|
@ -122,10 +125,17 @@ function SnapHostedPayment({ orderId, amount, onSuccess, onError }: Omit<SnapPay
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
gross_amount: amount
|
gross_amount: amount
|
||||||
},
|
},
|
||||||
// Add customer details if available
|
customer_details: customer ? {
|
||||||
customer_details: {
|
first_name: customer.name,
|
||||||
// These would come from props or context
|
email: customer.email,
|
||||||
}
|
phone: customer.phone
|
||||||
|
} : undefined,
|
||||||
|
item_details: [{
|
||||||
|
id: orderId,
|
||||||
|
name: 'Payment',
|
||||||
|
price: amount,
|
||||||
|
quantity: 1
|
||||||
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
Logger.paymentInfo('snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
|
Logger.paymentInfo('snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,10 @@ export interface SnapTokenRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SnapTokenResponse {
|
export interface SnapTokenResponse {
|
||||||
|
token: string | {
|
||||||
token: string
|
token: string
|
||||||
|
redirect_url: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SnapTokenService {
|
export class SnapTokenService {
|
||||||
|
|
@ -38,16 +41,28 @@ export class SnapTokenService {
|
||||||
|
|
||||||
const response = await api.post<SnapTokenResponse>('/payments/snap/token', request)
|
const response = await api.post<SnapTokenResponse>('/payments/snap/token', request)
|
||||||
|
|
||||||
if (!response.data?.token) {
|
// Handle both response formats:
|
||||||
|
// 1. Direct string: { token: "abc123" }
|
||||||
|
// 2. Nested object: { token: { token: "abc123", redirect_url: "..." } }
|
||||||
|
let tokenString: string
|
||||||
|
if (typeof response.data?.token === 'string') {
|
||||||
|
tokenString = response.data.token
|
||||||
|
} else if (response.data?.token && typeof response.data.token === 'object') {
|
||||||
|
tokenString = response.data.token.token
|
||||||
|
} else {
|
||||||
throw new Error('Invalid token response from server')
|
throw new Error('Invalid token response from server')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!tokenString) {
|
||||||
|
throw new Error('Empty token received from server')
|
||||||
|
}
|
||||||
|
|
||||||
TransactionLogger.log('SNAP', 'token.created', {
|
TransactionLogger.log('SNAP', 'token.created', {
|
||||||
orderId: request.transaction_details.order_id,
|
orderId: request.transaction_details.order_id,
|
||||||
tokenLength: response.data.token.length
|
tokenLength: tokenString.length
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data.token
|
return tokenString
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
TransactionLogger.logPaymentError('SNAP', request.transaction_details.order_id, error)
|
TransactionLogger.logPaymentError('SNAP', request.transaction_details.order_id, error)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export function CheckoutPage() {
|
||||||
const expireAt = Date.now() + 59 * 60 * 1000 + 32 * 1000 // 00:59:32
|
const expireAt = Date.now() + 59 * 60 * 1000 + 32 * 1000 // 00:59:32
|
||||||
const [selected, setSelected] = React.useState<PaymentMethod | null>(null)
|
const [selected, setSelected] = React.useState<PaymentMethod | null>(null)
|
||||||
const [locked, setLocked] = React.useState(false)
|
const [locked, setLocked] = React.useState(false)
|
||||||
const [currentStep, setCurrentStep] = React.useState<1 | 2 | 3>(1)
|
const [currentStep, setCurrentStep] = React.useState<1 | 2>(1)
|
||||||
const [isBusy, setIsBusy] = React.useState(false)
|
const [isBusy, setIsBusy] = React.useState(false)
|
||||||
const [form, setForm] = React.useState<{ name: string; contact: string; address: string; notes: string }>({
|
const [form, setForm] = React.useState<{ name: string; contact: string; address: string; notes: string }>({
|
||||||
name: 'Demo User',
|
name: 'Demo User',
|
||||||
|
|
@ -66,8 +66,8 @@ export function CheckoutPage() {
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PaymentSheet merchantName="Simaya" orderId={orderId} amount={amount} expireAt={expireAt} showStatusCTA={currentStep === 3}>
|
<PaymentSheet merchantName="Simaya" orderId={orderId} amount={amount} expireAt={expireAt} showStatusCTA={currentStep === 2}>
|
||||||
{/* Wizard 3 langkah: Step 1 (Form Dummy) → Step 2 (Pilih Metode) → Step 3 (Panel Metode) */}
|
{/* Wizard 2 langkah: Step 1 (Form Dummy) → Step 2 (Payment - Snap/Core auto-detect) */}
|
||||||
{currentStep === 1 && (
|
{currentStep === 1 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium">Konfirmasi data checkout</div>
|
<div className="text-sm font-medium">Konfirmasi data checkout</div>
|
||||||
|
|
@ -114,6 +114,8 @@ export function CheckoutPage() {
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
|
// Set default payment method (bank_transfer for demo)
|
||||||
|
setSelected('bank_transfer')
|
||||||
setTimeout(() => { setCurrentStep(2); setIsBusy(false) }, 400)
|
setTimeout(() => { setCurrentStep(2); setIsBusy(false) }, 400)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -122,57 +124,23 @@ export function CheckoutPage() {
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 border-t-transparent" aria-hidden />
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/70 border-t-transparent" aria-hidden />
|
||||||
Memuat…
|
Memuat…
|
||||||
</span>
|
</span>
|
||||||
) : 'Next'}
|
) : 'Lanjut ke Pembayaran'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div className="space-y-3">
|
|
||||||
<PaymentMethodList
|
|
||||||
selected={selected ?? undefined}
|
|
||||||
onSelect={(m) => {
|
|
||||||
setSelected(m)
|
|
||||||
if (m === 'bank_transfer' || m === 'cstore') {
|
|
||||||
// SnapPaymentTrigger will handle the bank/store selection internally
|
|
||||||
setIsBusy(true)
|
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
|
||||||
} else if (m === 'cpay') {
|
|
||||||
// Redirect ke aplikasi cPay (CIFO Token) di Play Store
|
|
||||||
try {
|
|
||||||
Logger.info('cpay.redirect.start')
|
|
||||||
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
|
||||||
Logger.info('cpay.redirect.done')
|
|
||||||
} catch (e) {
|
|
||||||
Logger.error('cpay.redirect.error', { message: (e as Error)?.message })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsBusy(true)
|
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={locked}
|
|
||||||
enabled={runtimeCfg?.paymentToggles
|
|
||||||
? {
|
|
||||||
bank_transfer: runtimeCfg.paymentToggles.bank_transfer,
|
|
||||||
credit_card: runtimeCfg.paymentToggles.credit_card,
|
|
||||||
gopay: runtimeCfg.paymentToggles.gopay,
|
|
||||||
cstore: runtimeCfg.paymentToggles.cstore,
|
|
||||||
cpay: !!runtimeCfg.paymentToggles.cpay,
|
|
||||||
}
|
|
||||||
: undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === 3 && (
|
|
||||||
<div className="space-y-3" aria-live="polite">
|
<div className="space-y-3" aria-live="polite">
|
||||||
{selected && (
|
|
||||||
<SnapPaymentTrigger
|
<SnapPaymentTrigger
|
||||||
orderId={orderId}
|
orderId={orderId}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
paymentMethod={selected}
|
customer={{
|
||||||
|
name: form.name,
|
||||||
|
email: form.contact.includes('@') ? form.contact : undefined,
|
||||||
|
phone: !form.contact.includes('@') ? form.contact : undefined
|
||||||
|
}}
|
||||||
|
paymentMethod={'bank_transfer'}
|
||||||
onChargeInitiated={() => setLocked(true)}
|
onChargeInitiated={() => setLocked(true)}
|
||||||
onSuccess={(result) => {
|
onSuccess={(result) => {
|
||||||
Logger.info('checkout.payment.success', { orderId, result })
|
Logger.info('checkout.payment.success', { orderId, result })
|
||||||
|
|
@ -183,13 +151,6 @@ export function CheckoutPage() {
|
||||||
// Handle payment error
|
// Handle payment error
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{selected && !(runtimeCfg?.paymentToggles ?? defaultEnabled())[selected] && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<Alert title="Metode nonaktif">Metode pembayaran ini dinonaktifkan di konfigurasi lingkungan.</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* No back/next controls on Step 3 as requested */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PaymentSheet>
|
</PaymentSheet>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { PaymentSheet } from '../features/payments/components/PaymentSheet'
|
import { PaymentSheet } from '../features/payments/components/PaymentSheet'
|
||||||
import { PaymentMethodList } from '../features/payments/components/PaymentMethodList'
|
|
||||||
import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
|
import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
|
||||||
import { BankTransferPanel } from '../features/payments/core/BankTransferPanel'
|
import { SnapPaymentTrigger } from '../features/payments/snap/SnapPaymentTrigger'
|
||||||
import { CardPanel } from '../features/payments/core/CardPanel'
|
|
||||||
import { GoPayPanel } from '../features/payments/core/GoPayPanel'
|
|
||||||
import { CStorePanel } from '../features/payments/core/CStorePanel'
|
|
||||||
import { BankLogo, type BankKey, LogoAlfamart, LogoIndomaret } from '../features/payments/components/PaymentLogos'
|
|
||||||
import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
|
import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
|
||||||
import { Alert } from '../components/alert/Alert'
|
import { Alert } from '../components/alert/Alert'
|
||||||
import { Button } from '../components/ui/button'
|
import { Button } from '../components/ui/button'
|
||||||
|
|
@ -23,15 +18,13 @@ export function PayPage() {
|
||||||
const [orderId, setOrderId] = useState<string>('')
|
const [orderId, setOrderId] = useState<string>('')
|
||||||
const [amount, setAmount] = useState<number>(0)
|
const [amount, setAmount] = useState<number>(0)
|
||||||
const [expireAt, setExpireAt] = useState<number>(Date.now() + 24 * 60 * 60 * 1000)
|
const [expireAt, setExpireAt] = useState<number>(Date.now() + 24 * 60 * 60 * 1000)
|
||||||
const [selectedMethod, setSelectedMethod] = useState<Method>(null)
|
const [selectedMethod] = useState<Method>(null)
|
||||||
const [locked, setLocked] = useState<boolean>(false)
|
const [locked, setLocked] = useState<boolean>(false)
|
||||||
const [selectedBank, setSelectedBank] = useState<BankKey | null>(null)
|
const [customer, setCustomer] = useState<{ name?: string; phone?: string; email?: string } | undefined>(undefined)
|
||||||
const [selectedStore, setSelectedStore] = useState<'alfamart' | 'indomaret' | null>(null)
|
|
||||||
const [allowedMethods, setAllowedMethods] = useState<string[] | undefined>(undefined)
|
const [allowedMethods, setAllowedMethods] = useState<string[] | undefined>(undefined)
|
||||||
const [error, setError] = useState<{ code?: string; message?: string } | null>(null)
|
const [error, setError] = useState<{ code?: string; message?: string } | null>(null)
|
||||||
const { data: runtimeCfg } = usePaymentConfig()
|
const { data: runtimeCfg } = usePaymentConfig()
|
||||||
const [currentStep, setCurrentStep] = useState<2 | 3>(2)
|
const currentStep = 2
|
||||||
const [isBusy, setIsBusy] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
@ -43,6 +36,7 @@ export function PayPage() {
|
||||||
setOrderId(payload.order_id)
|
setOrderId(payload.order_id)
|
||||||
setAmount(payload.nominal)
|
setAmount(payload.nominal)
|
||||||
setExpireAt(payload.expire_at ?? Date.now() + 24 * 60 * 60 * 1000)
|
setExpireAt(payload.expire_at ?? Date.now() + 24 * 60 * 60 * 1000)
|
||||||
|
setCustomer(payload.customer)
|
||||||
setAllowedMethods(payload.allowed_methods)
|
setAllowedMethods(payload.allowed_methods)
|
||||||
setError(null)
|
setError(null)
|
||||||
if (isOrderLocked(payload.order_id)) setLocked(true)
|
if (isOrderLocked(payload.order_id)) setLocked(true)
|
||||||
|
|
@ -58,26 +52,7 @@ export function PayPage() {
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
const merchantName = useMemo(() => '', [])
|
const merchantName = useMemo(() => '', [])
|
||||||
|
|
||||||
const isExpired = expireAt ? Date.now() > expireAt : false
|
const isExpired = expireAt ? Date.now() > expireAt : false
|
||||||
const enabledMap: Record<PaymentMethod, boolean> = useMemo(() => {
|
|
||||||
const base = runtimeCfg?.paymentToggles
|
|
||||||
const allow = allowedMethods
|
|
||||||
const all: Record<PaymentMethod, boolean> = {
|
|
||||||
bank_transfer: base?.bank_transfer ?? true,
|
|
||||||
credit_card: base?.credit_card ?? true,
|
|
||||||
gopay: base?.gopay ?? true,
|
|
||||||
cstore: base?.cstore ?? true,
|
|
||||||
cpay: base?.cpay ?? false,
|
|
||||||
}
|
|
||||||
if (allow && Array.isArray(allow)) {
|
|
||||||
for (const k of (Object.keys(all) as PaymentMethod[])) {
|
|
||||||
if (k === 'cpay') continue
|
|
||||||
all[k] = allow.includes(k) && all[k]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return all
|
|
||||||
}, [runtimeCfg, allowedMethods])
|
|
||||||
|
|
||||||
if (error || isExpired) {
|
if (error || isExpired) {
|
||||||
const title = isExpired ? 'Link pembayaran telah kedaluwarsa' : 'Link pembayaran tidak valid'
|
const title = isExpired ? 'Link pembayaran telah kedaluwarsa' : 'Link pembayaran tidak valid'
|
||||||
|
|
@ -118,7 +93,7 @@ export function PayPage() {
|
||||||
orderId={orderId}
|
orderId={orderId}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
expireAt={expireAt}
|
expireAt={expireAt}
|
||||||
showStatusCTA={currentStep === 3}
|
showStatusCTA={currentStep === 2}
|
||||||
>
|
>
|
||||||
<div className="space-y-4 px-4 py-6">
|
<div className="space-y-4 px-4 py-6">
|
||||||
{locked && currentStep === 2 && (
|
{locked && currentStep === 2 && (
|
||||||
|
|
@ -132,130 +107,24 @@ export function PayPage() {
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div className="space-y-3">
|
|
||||||
<PaymentMethodList
|
|
||||||
selected={selectedMethod ?? undefined}
|
|
||||||
onSelect={(m) => {
|
|
||||||
setSelectedMethod(m as Method)
|
|
||||||
if (m === 'bank_transfer' || m === 'cstore') {
|
|
||||||
void 0
|
|
||||||
} else if (m === 'cpay') {
|
|
||||||
try {
|
|
||||||
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
|
||||||
} catch { void 0 }
|
|
||||||
} else {
|
|
||||||
setIsBusy(true)
|
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={locked}
|
|
||||||
enabled={enabledMap}
|
|
||||||
renderPanel={(m) => {
|
|
||||||
const enabled = enabledMap[m]
|
|
||||||
if (!enabled) {
|
|
||||||
return (
|
|
||||||
<div className="p-2">
|
|
||||||
<Alert title="Metode nonaktif">Metode pembayaran ini dinonaktifkan di konfigurasi lingkungan.</Alert>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (m === 'bank_transfer') {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" aria-live="polite">
|
|
||||||
<div className="text-xs text-gray-600">Pilih bank untuk membuat Virtual Account</div>
|
|
||||||
<div className={`grid grid-cols-2 md:grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
|
||||||
{(['bca', 'bni', 'bri', 'cimb', 'mandiri', 'permata'] as BankKey[]).map((bk) => (
|
|
||||||
<button
|
|
||||||
key={bk}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedBank(bk)
|
|
||||||
setIsBusy(true)
|
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
|
||||||
}}
|
|
||||||
className="rounded border border-gray-300 bg-white p-3 md:p-2 w-full flex items-center justify-center overflow-hidden hover:bg-gray-100"
|
|
||||||
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
|
||||||
>
|
|
||||||
<BankLogo bank={bk} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{isBusy && (
|
|
||||||
<div className="text-xs text-gray-600 inline-flex items-center gap-2">
|
|
||||||
<span className="h-3 w-3 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" aria-hidden />
|
|
||||||
Menyiapkan VA…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (m === 'cstore') {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" aria-live="polite">
|
|
||||||
<div className="text-xs text-gray-600">Pilih toko untuk membuat kode pembayaran</div>
|
|
||||||
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
|
||||||
{/* {(['alfamart', 'indomaret'] as const).map((st) => ( */}
|
|
||||||
{(['alfamart'] as const).map((st) => (
|
|
||||||
<button
|
|
||||||
key={st}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedStore(st)
|
|
||||||
setIsBusy(true)
|
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
|
||||||
}}
|
|
||||||
className="rounded border border-gray-300 bg-white p-3 md:p-2 w-full flex items-center justify-center hover:bg-gray-100"
|
|
||||||
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
|
||||||
>
|
|
||||||
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === 3 && (
|
|
||||||
<div className="space-y-3" aria-live="polite">
|
<div className="space-y-3" aria-live="polite">
|
||||||
{selectedMethod === 'bank_transfer' && (
|
<SnapPaymentTrigger
|
||||||
<BankTransferPanel
|
|
||||||
locked={locked}
|
|
||||||
onChargeInitiated={() => { lockOrder(orderId); setLocked(true) }}
|
|
||||||
orderId={orderId}
|
orderId={orderId}
|
||||||
amount={amount}
|
amount={amount}
|
||||||
defaultBank={(selectedBank ?? 'bca')}
|
customer={customer}
|
||||||
|
paymentMethod={selectedMethod || 'bank_transfer'}
|
||||||
|
onChargeInitiated={() => {
|
||||||
|
lockOrder(orderId)
|
||||||
|
setLocked(true)
|
||||||
|
}}
|
||||||
|
onSuccess={(result) => {
|
||||||
|
console.log('[PayPage] Payment success:', result)
|
||||||
|
nav.toStatus(orderId, selectedMethod || undefined)
|
||||||
|
}}
|
||||||
|
onError={(error) => {
|
||||||
|
console.error('[PayPage] Payment error:', error)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{selectedMethod === 'credit_card' && (
|
|
||||||
<CardPanel
|
|
||||||
locked={locked}
|
|
||||||
onChargeInitiated={() => { lockOrder(orderId); setLocked(true) }}
|
|
||||||
orderId={orderId}
|
|
||||||
amount={amount}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedMethod === 'gopay' && (
|
|
||||||
<GoPayPanel
|
|
||||||
locked={locked}
|
|
||||||
onChargeInitiated={() => { lockOrder(orderId); setLocked(true) }}
|
|
||||||
orderId={orderId}
|
|
||||||
amount={amount}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedMethod === 'cstore' && (
|
|
||||||
<CStorePanel
|
|
||||||
locked={locked}
|
|
||||||
onChargeInitiated={() => { lockOrder(orderId); setLocked(true) }}
|
|
||||||
orderId={orderId}
|
|
||||||
amount={amount}
|
|
||||||
defaultStore={selectedStore ?? undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue