diff --git a/src/features/payments/components/PaymentSheet.tsx b/src/features/payments/components/PaymentSheet.tsx
index 14cecf6..e7eec97 100644
--- a/src/features/payments/components/PaymentSheet.tsx
+++ b/src/features/payments/components/PaymentSheet.tsx
@@ -25,11 +25,12 @@ export interface PaymentSheetProps {
orderId: string
amount: number
expireAt: number // epoch ms
+ customerName?: string
children?: React.ReactNode
showStatusCTA?: boolean
}
-export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireAt, children, showStatusCTA = true }: PaymentSheetProps) {
+export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireAt, customerName, children, showStatusCTA = true }: PaymentSheetProps) {
const countdown = useCountdown(expireAt)
const [expanded, setExpanded] = React.useState(true)
return (
@@ -71,6 +72,7 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
Total
{formatCurrencyIDR(amount)}
Order ID #{orderId}
+ {customerName && Nama: {customerName}
}
)}
diff --git a/src/features/payments/snap/SnapPaymentTrigger.tsx b/src/features/payments/snap/SnapPaymentTrigger.tsx
index bd4695f..0befb0e 100644
--- a/src/features/payments/snap/SnapPaymentTrigger.tsx
+++ b/src/features/payments/snap/SnapPaymentTrigger.tsx
@@ -201,18 +201,4 @@ function SnapHostedPayment({ orderId, amount, customer, onSuccess, onError }: Om
{loading && }
)
-}
-
-// Type declaration for window.snap
-declare global {
- interface Window {
- snap?: {
- pay: (token: string, options: {
- onSuccess: (result: any) => void
- onPending: (result: any) => void
- onError: (result: any) => void
- onClose: () => void
- }) => void
- }
- }
}
\ No newline at end of file
diff --git a/src/lib/snapLoader.ts b/src/lib/snapLoader.ts
new file mode 100644
index 0000000..ccb5c20
--- /dev/null
+++ b/src/lib/snapLoader.ts
@@ -0,0 +1,132 @@
+import { Env } from './env'
+import { Logger } from './logger'
+
+let snapLoaded = false
+let snapPromise: Promise | null = null
+
+/**
+ * Dynamically loads Midtrans Snap.js script
+ * Returns a promise that resolves when Snap.js is ready
+ */
+export function loadSnapScript(): Promise {
+ // Return existing promise if already loading
+ if (snapPromise) {
+ return snapPromise
+ }
+
+ // Already loaded
+ if (snapLoaded && window.snap) {
+ return Promise.resolve()
+ }
+
+ // Start loading
+ snapPromise = new Promise((resolve, reject) => {
+ try {
+ const clientKey = Env.MIDTRANS_CLIENT_KEY
+ const midtransEnv = Env.MIDTRANS_ENV || 'sandbox'
+
+ if (!clientKey) {
+ const error = 'MIDTRANS_CLIENT_KEY not configured'
+ Logger.error('snap.load.error', { error })
+ reject(new Error(error))
+ return
+ }
+
+ // Determine Snap.js URL based on environment
+ const snapUrl = midtransEnv === 'production'
+ ? 'https://app.midtrans.com/snap/snap.js'
+ : 'https://app.sandbox.midtrans.com/snap/snap.js'
+
+ Logger.info('snap.load.start', { snapUrl, clientKey: clientKey.substring(0, 10) + '...' })
+
+ // Check if script already exists
+ const existingScript = document.querySelector(`script[src="${snapUrl}"]`)
+ if (existingScript) {
+ Logger.info('snap.load.exists', { snapUrl })
+ // Wait a bit and check if window.snap is available
+ setTimeout(() => {
+ if (window.snap) {
+ snapLoaded = true
+ Logger.info('snap.load.ready', { hasSnap: true })
+ resolve()
+ } else {
+ Logger.error('snap.load.error', { error: 'Script loaded but window.snap not available' })
+ reject(new Error('Snap.js loaded but window.snap not available'))
+ }
+ }, 500)
+ return
+ }
+
+ // Create script element
+ const script = document.createElement('script')
+ script.src = snapUrl
+ script.setAttribute('data-client-key', clientKey)
+
+ script.onload = () => {
+ Logger.info('snap.script.loaded', { snapUrl })
+ console.log('Snap.js script loaded, waiting for initialization...')
+ // Wait a bit for Snap to initialize
+ setTimeout(() => {
+ console.log('After 500ms delay - window.snap:', window.snap)
+ console.log('After 500ms delay - window.snap?.pay:', window.snap?.pay)
+ if (window.snap && typeof window.snap.pay === 'function') {
+ snapLoaded = true
+ Logger.info('snap.load.success', { hasSnap: true, hasPay: true })
+ console.log('ā Snap.js ready!')
+ resolve()
+ } else {
+ const error = 'Snap.js loaded but window.snap.pay not available'
+ Logger.error('snap.load.error', { error, hasSnap: !!window.snap })
+ console.error('ā Snap.js error:', error, { hasSnap: !!window.snap, snapObj: window.snap })
+ reject(new Error(error))
+ }
+ }, 500)
+ }
+
+ script.onerror = (error) => {
+ Logger.error('snap.script.error', { error, snapUrl })
+ reject(new Error('Failed to load Snap.js script'))
+ }
+
+ // Append script to document
+ document.head.appendChild(script)
+ Logger.info('snap.script.appended', { snapUrl })
+
+ } catch (error: any) {
+ Logger.error('snap.load.exception', { error: error.message })
+ reject(error)
+ }
+ })
+
+ return snapPromise
+}
+
+/**
+ * Check if Snap.js is already loaded and ready
+ */
+export function isSnapReady(): boolean {
+ return snapLoaded && !!window.snap && typeof window.snap.pay === 'function'
+}
+
+/**
+ * Wait for Snap.js to be ready, with timeout
+ */
+export function waitForSnap(timeoutMs: number = 5000): Promise {
+ return new Promise((resolve, reject) => {
+ if (isSnapReady()) {
+ resolve()
+ return
+ }
+
+ const startTime = Date.now()
+ const checkInterval = setInterval(() => {
+ if (isSnapReady()) {
+ clearInterval(checkInterval)
+ resolve()
+ } else if (Date.now() - startTime > timeoutMs) {
+ clearInterval(checkInterval)
+ reject(new Error(`Snap.js not ready after ${timeoutMs}ms`))
+ }
+ }, 100)
+ })
+}
diff --git a/src/pages/CheckoutPage.tsx b/src/pages/CheckoutPage.tsx
index 0f72ed5..ee9554e 100644
--- a/src/pages/CheckoutPage.tsx
+++ b/src/pages/CheckoutPage.tsx
@@ -1,11 +1,13 @@
+import React from 'react'
import { Alert } from '../components/alert/Alert'
import { Button } from '../components/ui/button'
import { Env } from '../lib/env'
+import { Logger } from '../lib/logger'
+import { loadSnapScript } from '../lib/snapLoader'
import { PaymentSheet } from '../features/payments/components/PaymentSheet'
-import { PaymentMethodList } from '../features/payments/components/PaymentMethodList'
-import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
-import { SnapPaymentTrigger } from '../features/payments/snap/SnapPaymentTrigger'
import { usePaymentConfig } from '../features/payments/lib/usePaymentConfig'
+import type { PaymentMethod } from '../features/payments/components/PaymentMethodList'
+import { SnapTokenService } from '../features/payments/snap/SnapTokenService'
interface AutoSnapPaymentProps {
orderId: string
amount: number
@@ -15,27 +17,39 @@ interface AutoSnapPaymentProps {
onError?: (error: any) => void
}
-function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSuccess, onError }: AutoSnapPaymentProps) {
+function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: Omit) {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState('')
- const hasTriggered = React.useRef(false)
+ const [paymentTriggered, setPaymentTriggered] = React.useState(false)
+
+ // Debug log immediately on component mount
+ console.log('AutoSnapPayment mounted with:', { orderId, amount, customer })
+ Logger.info('autosnapPayment.mount', { orderId, amount, hasCustomer: !!customer })
React.useEffect(() => {
- // Only trigger when we have valid orderId and amount
- if (!orderId || !amount || hasTriggered.current) return
- hasTriggered.current = true
+ console.log('AutoSnapPayment useEffect triggered', { orderId, amount, paymentTriggered })
+ // Only trigger when we have valid orderId and amount and not already triggered
+ if (!orderId || !amount || paymentTriggered) {
+ console.log('AutoSnapPayment useEffect early return', { hasOrderId: !!orderId, hasAmount: !!amount, alreadyTriggered: paymentTriggered })
+ return
+ }
const triggerPayment = async () => {
+ console.log('triggerPayment function called!')
+ setPaymentTriggered(true) // Mark as triggered immediately
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')
+ // Load Snap.js first
+ Logger.paymentInfo('checkout.auto.snap.loading_script', { orderId })
+ await loadSnapScript()
+ Logger.paymentInfo('checkout.auto.snap.script_loaded', { orderId, hasSnap: !!window.snap })
// Create Snap transaction token
+ Logger.paymentInfo('checkout.auto.snap.calling_api', { orderId, amount })
const token = await SnapTokenService.createToken({
transaction_details: {
order_id: orderId,
@@ -55,45 +69,75 @@ function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSucce
})
Logger.paymentInfo('checkout.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
+ console.log('Token berhasil dibuat:', token)
- // 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')
+ // Verify Snap.js is loaded
+ console.log('window.snap:', window.snap)
+ console.log('window.snap.pay:', window.snap?.pay)
+ console.log('typeof window.snap?.pay:', typeof window.snap?.pay)
+
+ if (!window.snap || typeof window.snap.pay !== 'function') {
+ const errorMsg = `Snap.js not properly loaded: hasSnap=${!!window.snap}, hasPay=${typeof window.snap?.pay}`
+ console.error(errorMsg)
+ throw new Error(errorMsg)
}
+ // Auto-trigger Snap payment popup
+ console.log('Memanggil window.snap.pay dengan token:', token.substring(0, 20) + '...')
+ console.log('Full token:', token)
+ setLoading(false) // Stop loading indicator before showing modal
+
+ 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)
+ setLoading(false)
+ onError?.(result)
+ },
+ onClose: () => {
+ Logger.paymentInfo('checkout.auto.snap.popup.closed', { orderId })
+ setLoading(false)
+ }
+ })
+
} catch (e: any) {
- Logger.paymentError('checkout.auto.snap.payment.error', { orderId, error: e.message })
- const message = 'Gagal memuat pembayaran. Silakan refresh halaman.'
- setError(message)
+ Logger.paymentError('checkout.auto.snap.payment.error', { orderId, error: e.message, stack: e.stack })
+ console.error('Error membuat token Snap:', e)
+
+ // Handle specific error: order_id already taken
+ const errorMessage = e.response?.data?.message || e.message || ''
+ const isOrderTaken = errorMessage.includes('already been taken') ||
+ errorMessage.includes('order_id has already been taken')
+
+ if (isOrderTaken) {
+ const message = 'Order ID sudah digunakan. Pembayaran untuk order ini sudah dibuat. Silakan cek halaman status pembayaran.'
+ setError(message)
+ } else {
+ const message = e.response?.data?.message || e.message || 'Gagal memuat pembayaran. Silakan refresh halaman.'
+ setError(message)
+ }
+
onError?.(e)
- } finally {
setLoading(false)
}
}
// Small delay to ensure UI is rendered
+ console.log('Setting timeout to call triggerPayment in 500ms...')
const timer = setTimeout(triggerPayment, 500)
- return () => clearTimeout(timer)
- }, [orderId, amount, customer, onChargeInitiated, onSuccess, onError])
+ return () => {
+ console.log('Cleanup: clearing timeout')
+ clearTimeout(timer)
+ }
+ }, [orderId, amount, customer, paymentTriggered, onSuccess, onError])
// Don't render anything until we have valid data
if (!orderId || !amount) {
@@ -111,7 +155,13 @@ function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSucce
{error && (
- {error}
+
+
{error}
+
+ Detail Error
+ {JSON.stringify({ orderId, amount, customer }, null, 2)}
+
+
)}
@@ -121,11 +171,17 @@ function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSucce
Menyiapkan pembayaran...
- ) : (
-
- Membuka halaman pembayaran Midtrans...
-
- )}
+ ) : error ? (
+
+
Gagal memuat pembayaran
+
+
+ ) : null}
)
@@ -146,7 +202,6 @@ export function CheckoutPage() {
const amount = 3500000
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>(1)
const [isBusy, setIsBusy] = React.useState(false)
const [form, setForm] = React.useState<{ name: string; contact: string; address: string; notes: string }>({
@@ -188,7 +243,7 @@ export function CheckoutPage() {
)}
-
+
{/* Wizard 2 langkah: Step 1 (Form Dummy) ā Step 2 (Payment - Snap/Core auto-detect) */}
{currentStep === 1 && (
@@ -254,6 +309,11 @@ export function CheckoutPage() {
{currentStep === 2 && (
+ {(() => {
+ console.log('Rendering step 2 - AutoSnapPayment', { orderId, amount, currentStep })
+ Logger.info('checkout.step2.render', { orderId, amount })
+ return null
+ })()}
setLocked(true)}
onSuccess={(result) => {
Logger.info('checkout.payment.success', { orderId, result })
// Handle successful payment
@@ -281,13 +340,4 @@ export function CheckoutPage() {
)
-}
-function defaultEnabled(): Record {
- return {
- bank_transfer: Env.ENABLE_BANK_TRANSFER,
- credit_card: Env.ENABLE_CREDIT_CARD,
- gopay: Env.ENABLE_GOPAY,
- cstore: Env.ENABLE_CSTORE,
- cpay: Env.ENABLE_CPAY,
- }
}
\ No newline at end of file
diff --git a/src/pages/PayPage.tsx b/src/pages/PayPage.tsx
index 1f0ffba..ff2ded5 100644
--- a/src/pages/PayPage.tsx
+++ b/src/pages/PayPage.tsx
@@ -8,6 +8,9 @@ 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 { Logger } from '../lib/logger'
+import { loadSnapScript } from '../lib/snapLoader'
+import { SnapTokenService } from '../features/payments/snap/SnapTokenService'
import React from 'react'
type Method = PaymentMethod | null
@@ -16,30 +19,37 @@ 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) {
+function AutoSnapPayment({ orderId, amount, customer, onSuccess, onError }: AutoSnapPaymentProps) {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState('')
- const hasTriggered = React.useRef(false)
+ const [paymentTriggered, setPaymentTriggered] = React.useState(false)
+
+ console.log('[PayPage] AutoSnapPayment mounted:', { orderId, amount, customer })
+ Logger.info('paypage.autosnapPayment.mount', { orderId, amount, hasCustomer: !!customer })
React.useEffect(() => {
- // Only trigger when we have valid orderId and amount
- if (!orderId || !amount || hasTriggered.current) return
- hasTriggered.current = true
+ console.log('[PayPage] useEffect triggered', { orderId, amount, paymentTriggered })
+ if (!orderId || !amount || paymentTriggered) {
+ console.log('[PayPage] Early return', { hasOrderId: !!orderId, hasAmount: !!amount, alreadyTriggered: paymentTriggered })
+ return
+ }
const triggerPayment = async () => {
+ console.log('[PayPage] triggerPayment called')
+ setPaymentTriggered(true)
try {
setLoading(true)
setError('')
- console.log('[PayPage] Auto-triggering Snap payment:', { orderId, amount, customer })
+ Logger.paymentInfo('paypage.auto.snap.init', { orderId, amount, customer })
- // Import SnapTokenService dynamically to avoid circular deps
- const { SnapTokenService } = await import('../features/payments/snap/SnapTokenService')
+ // Load Snap.js first
+ await loadSnapScript()
+ Logger.paymentInfo('paypage.auto.snap.script_loaded', { orderId, hasSnap: !!window.snap })
// Create Snap transaction token
const token = await SnapTokenService.createToken({
@@ -60,46 +70,68 @@ function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSucce
}]
})
- console.log('[PayPage] Snap token received:', token.substring(0, 10) + '...')
+ Logger.paymentInfo('paypage.auto.snap.token.received', { orderId, token: token.substring(0, 10) + '...' })
+ console.log('[PayPage] Token received:', token)
- // 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')
+ if (!window.snap || typeof window.snap.pay !== 'function') {
+ throw new Error(`Snap.js not loaded: hasSnap=${!!window.snap}`)
}
+ console.log('[PayPage] Calling window.snap.pay')
+ setLoading(false)
+
+ window.snap.pay(token, {
+ onSuccess: (result: any) => {
+ Logger.paymentInfo('paypage.auto.snap.payment.success', { orderId, transactionId: result.transaction_id })
+ onSuccess?.(result)
+ },
+ onPending: (result: any) => {
+ Logger.paymentInfo('paypage.auto.snap.payment.pending', { orderId, transactionId: result.transaction_id })
+ },
+ onError: (result: any) => {
+ Logger.paymentError('paypage.auto.snap.payment.error', { orderId, error: result })
+ setError('Pembayaran gagal. Silakan coba lagi.')
+ setLoading(false)
+ onError?.(result)
+ },
+ onClose: () => {
+ Logger.paymentInfo('paypage.auto.snap.popup.closed', { orderId })
+ setLoading(false)
+ }
+ })
+
} catch (e: any) {
- console.error('[PayPage] Auto-payment error:', e.message)
- const message = 'Gagal memuat pembayaran. Silakan refresh halaman.'
- setError(message)
+ Logger.paymentError('paypage.auto.snap.payment.error', { orderId, error: e.message })
+ console.error('[PayPage] Error:', e)
+
+ // Handle specific error: order_id already taken (payment already exists)
+ const errorMessage = e.response?.data?.message || e.message || ''
+ const isOrderTaken = errorMessage.includes('already been taken') ||
+ errorMessage.includes('order_id has already been taken')
+
+ if (isOrderTaken) {
+ // Order already has payment, redirect to status page
+ Logger.paymentInfo('paypage.order.already_exists', { orderId })
+ console.log('[PayPage] Order already has payment, redirecting to status...')
+
+ // Show message briefly then redirect
+ setError('Pembayaran untuk order ini sudah dibuat. Mengalihkan ke halaman status...')
+ setTimeout(() => {
+ window.location.href = `/payments/${orderId}/status`
+ }, 2000)
+ } else {
+ setError(e.response?.data?.message || e.message || 'Gagal memuat pembayaran')
+ }
+
onError?.(e)
- } finally {
setLoading(false)
}
}
- // Small delay to ensure UI is rendered
+ console.log('[PayPage] Setting timeout')
const timer = setTimeout(triggerPayment, 500)
return () => clearTimeout(timer)
- }, [orderId, amount, customer, onChargeInitiated, onSuccess, onError])
+ }, [orderId, amount, customer, paymentTriggered, onSuccess, onError])
// Don't render anything until we have valid data
if (!orderId || !amount) {
@@ -127,11 +159,17 @@ function AutoSnapPayment({ orderId, amount, customer, onChargeInitiated, onSucce
Menyiapkan pembayaran...
- ) : (
-
- Membuka halaman pembayaran Midtrans...
-
- )}
+ ) : error ? (
+
+
Gagal memuat pembayaran
+
+
+ ) : null}
)
@@ -146,9 +184,8 @@ export function PayPage() {
const [selectedMethod] = useState(null)
const [locked, setLocked] = useState(false)
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()
+ usePaymentConfig()
const currentStep = 2
useEffect(() => {
@@ -162,7 +199,6 @@ export function PayPage() {
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)
} catch {
@@ -237,12 +273,10 @@ export function PayPage() {
orderId={orderId}
amount={amount}
customer={customer}
- onChargeInitiated={() => {
- lockOrder(orderId)
- setLocked(true)
- }}
onSuccess={(result) => {
console.log('[PayPage] Payment success:', result)
+ lockOrder(orderId)
+ setLocked(true)
nav.toStatus(orderId, selectedMethod || undefined)
}}
onError={(error) => {
diff --git a/src/services/api.ts b/src/services/api.ts
index 1420c05..ef8dc65 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -53,7 +53,15 @@ api.interceptors.response.use(
const url = error.config?.url || ''
const status = error.response?.status
const fullUrl = `${baseURL}${url}`
- Logger.error('api.error', { baseURL, url, fullUrl, status, message: error.message })
+ const responseData = error.response?.data
+ Logger.error('api.error', { baseURL, url, fullUrl, status, message: error.message, responseData })
+ console.error('API Error:', {
+ fullUrl,
+ status,
+ message: error.message,
+ responseData,
+ config: error.config
+ })
throw error
}
)
diff --git a/src/types/snap.d.ts b/src/types/snap.d.ts
new file mode 100644
index 0000000..830568b
--- /dev/null
+++ b/src/types/snap.d.ts
@@ -0,0 +1,17 @@
+// Midtrans Snap.js type definitions
+interface SnapPaymentOptions {
+ onSuccess?: (result: any) => void
+ onPending?: (result: any) => void
+ onError?: (result: any) => void
+ onClose?: () => void
+}
+
+interface Snap {
+ pay: (token: string, options?: SnapPaymentOptions) => void
+ hide: () => void
+ show: () => void
+}
+
+interface Window {
+ snap?: Snap
+}
diff --git a/test-create-payment-link.cjs b/test-create-payment-link.cjs
new file mode 100644
index 0000000..378d9ab
--- /dev/null
+++ b/test-create-payment-link.cjs
@@ -0,0 +1,34 @@
+const axios = require('axios');
+const fs = require('fs');
+
+async function createPaymentLink() {
+ // Read file and remove BOM if present
+ let jsonContent = fs.readFileSync('c:/laragon/www/core-midtrans-cifo/tmp-createtransaksi.json', 'utf8');
+ // Remove BOM
+ if (jsonContent.charCodeAt(0) === 0xFEFF) {
+ jsonContent = jsonContent.slice(1);
+ }
+
+ const payload = JSON.parse(jsonContent);
+
+ try {
+ console.log('Creating payment link...');
+ console.log('Payload:', JSON.stringify(payload, null, 2));
+
+ const response = await axios.post('http://localhost:8000/createtransaksi', payload, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': 'dev-key'
+ }
+ });
+
+ console.log('\nā Success!');
+ console.log('Response:', JSON.stringify(response.data, null, 2));
+ console.log('\nš Payment URL:', response.data.data.url);
+ } catch (error) {
+ console.log('ā Error:', error.response?.status, error.response?.data);
+ console.log('Full error:', error.message);
+ }
+}
+
+createPaymentLink();
diff --git a/test-frontend-payload.cjs b/test-frontend-payload.cjs
new file mode 100644
index 0000000..e369ab7
--- /dev/null
+++ b/test-frontend-payload.cjs
@@ -0,0 +1,35 @@
+const axios = require('axios');
+
+async function testFrontendPayload() {
+ // Simulate the exact payload sent from CheckoutPage.tsx AutoSnapPayment
+ const payload = {
+ transaction_details: {
+ order_id: 'order-1733280000000-12345', // example orderId
+ gross_amount: 3500000
+ },
+ customer_details: {
+ first_name: 'Demo User',
+ email: 'demo@example.com',
+ phone: undefined // as sent from frontend when contact is email
+ },
+ item_details: [{
+ id: 'order-1733280000000-12345',
+ name: 'Payment',
+ price: 3500000,
+ quantity: 1
+ }]
+ };
+
+ try {
+ console.log('Testing frontend-like payload...');
+ console.log('Payload:', JSON.stringify(payload, null, 2));
+
+ const response = await axios.post('http://localhost:8000/api/payments/snap/token', payload);
+ console.log('Success:', response.data);
+ } catch (error) {
+ console.log('Error:', error.response?.status, error.response?.data);
+ console.log('Full error:', error.message);
+ }
+}
+
+testFrontendPayload();
\ No newline at end of file
diff --git a/test-snap-token.cjs b/test-snap-token.cjs
new file mode 100644
index 0000000..87daf13
--- /dev/null
+++ b/test-snap-token.cjs
@@ -0,0 +1,34 @@
+const axios = require('axios');
+
+async function testSnapToken() {
+ const payload = {
+ transaction_details: {
+ order_id: 'test-order-123',
+ gross_amount: 100000
+ },
+ customer_details: {
+ first_name: 'Test User',
+ email: 'test@example.com',
+ phone: '08123456789'
+ },
+ item_details: [{
+ id: 'test-order-123',
+ name: 'Test Payment',
+ price: 100000,
+ quantity: 1
+ }]
+ };
+
+ try {
+ console.log('Testing Snap token creation...');
+ console.log('Payload:', JSON.stringify(payload, null, 2));
+
+ const response = await axios.post('http://localhost:8000/api/payments/snap/token', payload);
+ console.log('Success:', response.data);
+ } catch (error) {
+ console.log('Error:', error.response?.status, error.response?.data);
+ console.log('Full error:', error.message);
+ }
+}
+
+testSnapToken();
\ No newline at end of file
diff --git a/tmp-createtransaksi.json b/tmp-createtransaksi.json
index 49d2134..c937e8d 100644
--- a/tmp-createtransaksi.json
+++ b/tmp-createtransaksi.json
@@ -1,12 +1,12 @@
{
- "mercant_id": "REFNO-001",
- "timestamp": 1731300000000,
+ "mercant_id": "REFNO-002",
+ "timestamp": 1733283600000,
"deskripsi": "Bayar Internet",
"nominal": 200000,
- "nama": "Demo User",
+ "nama": "Demo User 2",
"no_telepon": "081234567890",
- "email": "demo@example.com",
+ "email": "demo2@example.com",
"item": [
- { "item_id": "TKG-2511131", "nama": "Internet", "harga": 200000, "qty": 1 }
+ { "item_id": "TKG-2512041", "nama": "Internet Desember", "harga": 200000, "qty": 1 }
]
}