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