diff --git a/scripts/create-test-payment.mjs b/scripts/create-test-payment.mjs
new file mode 100644
index 0000000..ef9838e
--- /dev/null
+++ b/scripts/create-test-payment.mjs
@@ -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)
+}
diff --git a/src/app/router.tsx b/src/app/router.tsx
index 5cc3e8a..a89a08e 100644
--- a/src/app/router.tsx
+++ b/src/app/router.tsx
@@ -1,10 +1,10 @@
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { AppLayout } from './AppLayout'
-// import { CheckoutPage } from '../pages/CheckoutPage'
+import { CheckoutPage } from '../pages/CheckoutPage'
import { PaymentStatusPage } from '../pages/PaymentStatusPage'
import { PaymentHistoryPage } from '../pages/PaymentHistoryPage'
import { NotFoundPage } from '../pages/NotFoundPage'
-// import { DemoStorePage } from '../pages/DemoStorePage'
+import { DemoStorePage } from '../pages/DemoStorePage'
import { InitPage } from '../pages/InitialPage'
import { PayPage } from '../pages/PayPage'
@@ -15,7 +15,8 @@ const router = createBrowserRouter([
errorElement:
Terjadi kesalahan. Coba muat ulang.
,
children: [
{ index: true, element: },
- // { path: 'checkout', element: },
+ { path: 'checkout', element: },
+ { path: 'demo', element: },
{ path: 'pay/:token', element: },
{ path: 'payments/:orderId/status', element: },
{ path: 'history', element: },
diff --git a/src/features/payments/snap/SnapPaymentTrigger.tsx b/src/features/payments/snap/SnapPaymentTrigger.tsx
index e0b909b..bd4695f 100644
--- a/src/features/payments/snap/SnapPaymentTrigger.tsx
+++ b/src/features/payments/snap/SnapPaymentTrigger.tsx
@@ -10,6 +10,7 @@ import { SnapTokenService } from './SnapTokenService'
interface SnapPaymentTriggerProps {
orderId: string
amount: number
+ customer?: { name?: string; phone?: string; email?: string }
paymentMethod?: string
onSuccess?: (result: any) => void
onError?: (error: any) => void
@@ -19,6 +20,7 @@ interface SnapPaymentTriggerProps {
export function SnapPaymentTrigger({
orderId,
amount,
+ customer,
paymentMethod,
onSuccess,
onError,
@@ -43,6 +45,7 @@ export function SnapPaymentTrigger({
@@ -105,7 +108,7 @@ function CorePaymentComponent({ paymentMethod, orderId, amount, onChargeInitiate
return
}
-function SnapHostedPayment({ orderId, amount, onSuccess, onError }: Omit) {
+function SnapHostedPayment({ orderId, amount, customer, onSuccess, onError }: Omit) {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState('')
@@ -114,7 +117,7 @@ function SnapHostedPayment({ orderId, amount, onSuccess, onError }: Omit('/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')
}
+ if (!tokenString) {
+ throw new Error('Empty token received from server')
+ }
+
TransactionLogger.log('SNAP', 'token.created', {
orderId: request.transaction_details.order_id,
- tokenLength: response.data.token.length
+ tokenLength: tokenString.length
})
- return response.data.token
+ return tokenString
} catch (error) {
TransactionLogger.logPaymentError('SNAP', request.transaction_details.order_id, error)
diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx
index dc6feed..1b4178f 100644
--- a/src/pages/CheckoutPage.tsx
+++ b/src/pages/CheckoutPage.tsx
@@ -25,7 +25,7 @@ export function CheckoutPage() {
const expireAt = Date.now() + 59 * 60 * 1000 + 32 * 1000 // 00:59:32
const [selected, setSelected] = React.useState(null)
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 [form, setForm] = React.useState<{ name: string; contact: string; address: string; notes: string }>({
name: 'Demo User',
@@ -66,8 +66,8 @@ export function CheckoutPage() {
)}
-
- {/* 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 && (
Konfirmasi data checkout
@@ -114,6 +114,8 @@ export function CheckoutPage() {
disabled={isBusy}
onClick={() => {
setIsBusy(true)
+ // Set default payment method (bank_transfer for demo)
+ setSelected('bank_transfer')
setTimeout(() => { setCurrentStep(2); setIsBusy(false) }, 400)
}}
>
@@ -122,74 +124,33 @@ export function CheckoutPage() {
Memuatβ¦
- ) : 'Next'}
+ ) : 'Lanjut ke Pembayaran'}
)}
{currentStep === 2 && (
-
-
{
- 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}
- />
-
- )}
-
- {currentStep === 3 && (
- {selected && (
-
setLocked(true)}
- onSuccess={(result) => {
- Logger.info('checkout.payment.success', { orderId, result })
- // Handle successful payment
- }}
- onError={(error) => {
- Logger.error('checkout.payment.error', { orderId, error })
- // Handle payment error
- }}
- />
- )}
- {selected && !(runtimeCfg?.paymentToggles ?? defaultEnabled())[selected] && (
-
-
Metode pembayaran ini dinonaktifkan di konfigurasi lingkungan.
-
- )}
- {/* No back/next controls on Step 3 as requested */}
+ setLocked(true)}
+ onSuccess={(result) => {
+ Logger.info('checkout.payment.success', { orderId, result })
+ // Handle successful payment
+ }}
+ onError={(error) => {
+ Logger.error('checkout.payment.error', { orderId, error })
+ // Handle payment error
+ }}
+ />
)}
diff --git a/src/pages/PayPage.tsx b/src/pages/PayPage.tsx
index 5b0ae2c..ca64f8e 100644
--- a/src/pages/PayPage.tsx
+++ b/src/pages/PayPage.tsx
@@ -1,13 +1,8 @@
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { PaymentSheet } from '../features/payments/components/PaymentSheet'
-import { PaymentMethodList } from '../features/payments/components/PaymentMethodList'
import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
-import { BankTransferPanel } from '../features/payments/core/BankTransferPanel'
-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 { 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'
@@ -23,15 +18,13 @@ export function PayPage() {
const [orderId, setOrderId] = useState('')
const [amount, setAmount] = useState(0)
const [expireAt, setExpireAt] = useState(Date.now() + 24 * 60 * 60 * 1000)
- const [selectedMethod, setSelectedMethod] = useState(null)
+ const [selectedMethod] = useState(null)
const [locked, setLocked] = useState(false)
- const [selectedBank, setSelectedBank] = useState(null)
- const [selectedStore, setSelectedStore] = useState<'alfamart' | 'indomaret' | null>(null)
+ const [customer, setCustomer] = useState<{ name?: string; phone?: string; email?: string } | undefined>(undefined)
const [allowedMethods, setAllowedMethods] = useState(undefined)
const [error, setError] = useState<{ code?: string; message?: string } | null>(null)
const { data: runtimeCfg } = usePaymentConfig()
- const [currentStep, setCurrentStep] = useState<2 | 3>(2)
- const [isBusy, setIsBusy] = useState(false)
+ const currentStep = 2
useEffect(() => {
let cancelled = false
@@ -43,6 +36,7 @@ export function PayPage() {
setOrderId(payload.order_id)
setAmount(payload.nominal)
setExpireAt(payload.expire_at ?? Date.now() + 24 * 60 * 60 * 1000)
+ setCustomer(payload.customer)
setAllowedMethods(payload.allowed_methods)
setError(null)
if (isOrderLocked(payload.order_id)) setLocked(true)
@@ -58,26 +52,7 @@ export function PayPage() {
}, [token])
const merchantName = useMemo(() => '', [])
-
const isExpired = expireAt ? Date.now() > expireAt : false
- const enabledMap: Record = useMemo(() => {
- const base = runtimeCfg?.paymentToggles
- const allow = allowedMethods
- const all: Record = {
- 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) {
const title = isExpired ? 'Link pembayaran telah kedaluwarsa' : 'Link pembayaran tidak valid'
@@ -118,7 +93,7 @@ export function PayPage() {
orderId={orderId}
amount={amount}
expireAt={expireAt}
- showStatusCTA={currentStep === 3}
+ showStatusCTA={currentStep === 2}
>
{locked && currentStep === 2 && (
@@ -132,132 +107,26 @@ export function PayPage() {
)}
{currentStep === 2 && (
-
-
{
- 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)
- }
+
+
{
+ lockOrder(orderId)
+ setLocked(true)
}}
- disabled={locked}
- enabled={enabledMap}
- renderPanel={(m) => {
- const enabled = enabledMap[m]
- if (!enabled) {
- return (
-
-
Metode pembayaran ini dinonaktifkan di konfigurasi lingkungan.
-
- )
- }
- if (m === 'bank_transfer') {
- return (
-
-
Pilih bank untuk membuat Virtual Account
-
- {(['bca', 'bni', 'bri', 'cimb', 'mandiri', 'permata'] as BankKey[]).map((bk) => (
-
- ))}
-
- {isBusy && (
-
-
- Menyiapkan VAβ¦
-
- )}
-
- )
- }
- if (m === 'cstore') {
- return (
-
-
Pilih toko untuk membuat kode pembayaran
-
- {/* {(['alfamart', 'indomaret'] as const).map((st) => ( */}
- {(['alfamart'] as const).map((st) => (
-
- ))}
-
-
- )
- }
- return null
+ onSuccess={(result) => {
+ console.log('[PayPage] Payment success:', result)
+ nav.toStatus(orderId, selectedMethod || undefined)
+ }}
+ onError={(error) => {
+ console.error('[PayPage] Payment error:', error)
}}
/>
)}
-
- {currentStep === 3 && (
-
- {selectedMethod === 'bank_transfer' && (
- { lockOrder(orderId); setLocked(true) }}
- orderId={orderId}
- amount={amount}
- defaultBank={(selectedBank ?? 'bca')}
- />
- )}
- {selectedMethod === 'credit_card' && (
- { lockOrder(orderId); setLocked(true) }}
- orderId={orderId}
- amount={amount}
- />
- )}
- {selectedMethod === 'gopay' && (
- { lockOrder(orderId); setLocked(true) }}
- orderId={orderId}
- amount={amount}
- />
- )}
- {selectedMethod === 'cstore' && (
- { lockOrder(orderId); setLocked(true) }}
- orderId={orderId}
- amount={amount}
- defaultStore={selectedStore ?? undefined}
- />
- )}
-
- )}
)