Compare commits

..

No commits in common. "ef4dcb87952e858c308a5d83dba622e4820419a9" and "11ca3f5117531f84f3a42ba9131d0e59081dd5aa" have entirely different histories.

8 changed files with 104 additions and 392 deletions

View File

@ -158,153 +158,4 @@ Kemudian `POST` ke `http://localhost:8000/api/payments/webhook` dengan body beri
- Di settlement sukses, backend menghitung signature ERP (`sha512`) dan mengirim payload ke `ERP_NOTIFICATION_URL` jika `ERP_ENABLE_NOTIF=true` dan konfigurasi lengkap. - Di settlement sukses, backend menghitung signature ERP (`sha512`) dan mengirim payload ke `ERP_NOTIFICATION_URL` jika `ERP_ENABLE_NOTIF=true` dan konfigurasi lengkap.
## Postman Collection ## Postman Collection
- Anda dapat mengimpor koleksi: `docs/qa/payment-link.postman_collection.json` untuk mencoba endpoint di atas. - Anda dapat mengimpor koleksi: `docs/qa/payment-link.postman_collection.json` untuk mencoba endpoint di atas.
## Pengujian via Postman (Langkah Lengkap)
### 1) Import & Setup Environment
- Import koleksi: `docs/qa/payment-link.postman_collection.json`.
- Buat Environment (mis. `Midtrans-CIFO-Local`) dengan variabel:
- `baseUrl`: `http://localhost:8000`
- `paymentLinkBase`: `http://localhost:5175/pay` (sesuaikan port frontend)
- `externalApiKey`: kosong atau set sesuai `EXTERNAL_API_KEY` (jika diaktifkan)
- `token`: kosong (akan diisi dari respon create)
- `order_id`: kosong (akan diisi dari respon create)
Catatan: Koleksi memakai variabel ini pada URL/body. Pastikan environment terpilih saat menjalankan request.
### 2) Jalankan Koleksi (Urutan Dasar)
1. `1) Create Transaction`
- Body default: `item_id`, `nominal`, `customer`, `allowed_methods`.
- Jika `EXTERNAL_API_KEY` aktif, pastikan header `X-API-KEY: {{externalApiKey}}` terisi.
2. `2) Resolve Token`
- Memakai `{{token}}` dari langkah 1.
3. `3) Charge - Bank Transfer (BCA)` atau `3) Charge - CStore (Indomaret)`
- Memakai `{{order_id}}` dari langkah 1.
4. `4) Payment Status`
- Memakai `{{order_id}}` untuk cek status.
### 3) Skrip Test untuk Otomatis Mengisi Variabel
Tambahkan skrip berikut pada tab `Tests` di request `1) Create Transaction` agar `token`, `order_id`, dan `paymentLinkUrl` otomatis tersimpan ke variabel koleksi:
```javascript
// Tests: 1) Create Transaction
pm.test('Create returns token and url', function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json).to.have.property('token');
pm.expect(json).to.have.property('url');
pm.expect(json).to.have.property('order_id');
});
const res = pm.response.json();
pm.collectionVariables.set('token', res.token);
pm.collectionVariables.set('order_id', res.order_id);
pm.collectionVariables.set('paymentLinkUrl', res.url);
```
Tambahkan skrip sederhana di `2) Resolve Token` untuk validasi payload:
```javascript
pm.test('Resolve returns payload fields', function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json).to.have.property('order_id');
pm.expect(json).to.have.property('nominal');
});
```
Di `3) Charge - Bank Transfer (BCA)`, tambahkan assert dasar:
```javascript
pm.test('Charge is created (pending)', function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json).to.have.property('status_code');
pm.expect(json.status_code).to.eql('201');
pm.expect(json.transaction_status).to.eql('pending');
});
```
Di `4) Payment Status`, periksa status:
```javascript
pm.test('Status returns transaction info', function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json).to.have.property('order_id');
pm.expect(json).to.have.property('transaction_status');
});
```
### 4) Menambah Request GoPay/QR (Opsional)
Jika Anda ingin menguji GoPay/QR, tambahkan request baru di koleksi:
- Method: `POST`
- URL: `{{baseUrl}}/api/payments/charge`
- Body (raw JSON):
```json
{
"payment_type": "gopay",
"transaction_details": { "order_id": "{{order_id}}", "gross_amount": 150000 },
"gopay": { "enable_qr": true }
}
```
Catatan: Bila menghasilkan `400 Bad Request`, cek kembali konfigurasi sandbox merchant dan parameter tambahan yang dibutuhkan (misal `qr_black_white`, `callback_url`).
### 5) Membuka Halaman Pay dari Postman
Setelah `1) Create Transaction`, variabel `paymentLinkUrl` tersimpan. Anda bisa klik tombol `Open in Browser` (ikon tautan) pada Postman untuk membuka `{{paymentLinkUrl}}` langsung di browser.
### 6) Uji Webhook dengan Pre-request Script
Tambahkan request baru `Webhook (Manual)` dengan URL `{{baseUrl}}/api/payments/webhook`. Isi body dengan fields Midtrans. Untuk menghitung `signature_key` otomatis, gunakan Pre-request Script berikut (CryptoJS tersedia di sandbox Postman):
```javascript
// Pre-request: Webhook signature
const orderId = pm.collectionVariables.get('order_id');
const statusCode = pm.collectionVariables.get('status_code') || '200';
const grossAmount = pm.collectionVariables.get('gross_amount') || '150000';
const serverKey = pm.collectionVariables.get('server_key'); // set di environment
if (!serverKey) {
console.warn('server_key is not set in environment');
}
const raw = String(orderId) + String(statusCode) + String(grossAmount) + String(serverKey);
const sig = CryptoJS.SHA512(raw).toString(CryptoJS.enc.Hex);
pm.collectionVariables.set('signature_key', sig);
```
Body contoh (raw JSON):
```json
{
"order_id": "{{order_id}}",
"status_code": "{{status_code}}",
"gross_amount": "{{gross_amount}}",
"signature_key": "{{signature_key}}",
"transaction_status": "settlement"
}
```
Tambahkan test sederhana untuk memverifikasi respons webhook:
```javascript
pm.test('Webhook acknowledged', function () {
pm.response.to.have.status(200);
const json = pm.response.json();
pm.expect(json).to.have.property('ok');
pm.expect(json.ok).to.eql(true);
});
```
### 7) Postman Runner (Otomasi)
- Gunakan Runner untuk menjalankan berurutan: `1) Create Transaction``2) Resolve Token``3) Charge``4) Status`.
- Pastikan environment dipilih. Anda dapat menambah delay antar request jika diperlukan.
### 8) Tips & Variabel Tambahan
- Sesuaikan `paymentLinkBase` ke port frontend aktif (mis. `5175`).
- Jika `EXTERNAL_API_KEY` aktif, isi `externalApiKey` di environment.
- Simpan `server_key` (Midtrans Server Key) di environment untuk uji webhook.
- Tambahkan `gross_amount`, `status_code` variabel supaya mudah dikustom saat uji webhook/status.

View File

@ -4,8 +4,8 @@
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
}, },
"variable": [ "variable": [
{ "key": "baseUrl", "value": "https://be-midtrans-cifo.winteraccess.id" }, { "key": "baseUrl", "value": "http://localhost:8000" },
{ "key": "paymentLinkBase", "value": "https://midtrans-cifo.winteraccess.id/pay" }, { "key": "paymentLinkBase", "value": "http://localhost:5174/pay" },
{ "key": "externalApiKey", "value": "" }, { "key": "externalApiKey", "value": "" },
{ "key": "token", "value": "" }, { "key": "token", "value": "" },
{ "key": "order_id", "value": "" } { "key": "order_id", "value": "" }
@ -25,31 +25,6 @@
"raw": "{\n \"item_id\": \"order-demo-1\",\n \"nominal\": 150000,\n \"customer\": { \"name\": \"Demo User\", \"email\": \"demo@example.com\" },\n \"allowed_methods\": [\"bank_transfer\", \"credit_card\", \"gopay\", \"cstore\"]\n}" "raw": "{\n \"item_id\": \"order-demo-1\",\n \"nominal\": 150000,\n \"customer\": { \"name\": \"Demo User\", \"email\": \"demo@example.com\" },\n \"allowed_methods\": [\"bank_transfer\", \"credit_card\", \"gopay\", \"cstore\"]\n}"
} }
}, },
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"let res = {};",
"try { res = pm.response.json(); } catch(e) { res = {}; }",
"if (res && res.token) {",
" pm.collectionVariables.set('token', res.token);",
"}",
"if (res && res.order_id) {",
" pm.collectionVariables.set('order_id', res.order_id);",
"}",
"if (res && res.url) {",
" pm.collectionVariables.set('paymentLinkUrl', res.url);",
"}",
"pm.test('Create Transaction returns token and order_id', function () {",
" pm.expect(res.token, 'token exists').to.be.a('string');",
" pm.expect(res.order_id, 'order_id exists').to.be.a('string');",
"});"
]
}
}
],
"response": [] "response": []
}, },
{ {
@ -59,32 +34,6 @@
"header": [], "header": [],
"url": { "raw": "{{baseUrl}}/api/payment-links/{{token}}", "host": ["{{baseUrl}}"], "path": ["api","payment-links","{{token}}"] } "url": { "raw": "{{baseUrl}}/api/payment-links/{{token}}", "host": ["{{baseUrl}}"], "path": ["api","payment-links","{{token}}"] }
}, },
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const t = pm.variables.get('token');",
"if (!t) { throw new Error(\"Missing token. Run '1) Create Transaction' first.\"); }"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"let res = {};",
"try { res = pm.response.json(); } catch(e) { res = {}; }",
"pm.test('Resolve Token payload has order_id and nominal', function () {",
" pm.expect(res.order_id, 'order_id').to.be.a('string');",
" pm.expect(res.nominal, 'nominal').to.exist;",
"});"
]
}
}
],
"response": [] "response": []
}, },
{ {
@ -120,18 +69,6 @@
"header": [], "header": [],
"url": { "raw": "{{baseUrl}}/api/payments/{{order_id}}/status", "host": ["{{baseUrl}}"], "path": ["api","payments","{{order_id}}","status"] } "url": { "raw": "{{baseUrl}}/api/payments/{{order_id}}/status", "host": ["{{baseUrl}}"], "path": ["api","payments","{{order_id}}","status"] }
}, },
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const oid = pm.variables.get('order_id');",
"if (!oid) { throw new Error(\"Missing order_id. Run '1) Create Transaction' first.\"); }"
]
}
}
],
"response": [] "response": []
} }
] ]

View File

@ -257,42 +257,26 @@ app.post('/createtransaksi', (req, res) => {
logWarn('createtransaksi.unauthorized', { id: req.id }) logWarn('createtransaksi.unauthorized', { id: req.id })
return res.status(401).json({ error: 'UNAUTHORIZED', message: 'X-API-KEY invalid' }) return res.status(401).json({ error: 'UNAUTHORIZED', message: 'X-API-KEY invalid' })
} }
const itemId = req?.body?.item_id || req?.body?.order_id
// Skema baru:
// {
// mercant_id, timestamp, deskripsi, nominal,
// nama, no_telepon, email,
// item: [{ item_id, nama, harga, qty }, ...]
// }
const mercantId = req?.body?.mercant_id
const nominalRaw = req?.body?.nominal const nominalRaw = req?.body?.nominal
const items = Array.isArray(req?.body?.item) ? req.body.item : [] const customer = req?.body?.customer
const primaryItemId = items?.[0]?.item_id
const order_id = String(primaryItemId || mercantId || req?.body?.order_id || req?.body?.item_id || '')
// Bentuk customer dari field nama/no_telepon/email
const customer = {
name: req?.body?.nama,
phone: req?.body?.no_telepon,
email: req?.body?.email,
}
const allowed_methods = req?.body?.allowed_methods const allowed_methods = req?.body?.allowed_methods
if (!itemId || typeof nominalRaw === 'undefined') {
if (!order_id || typeof nominalRaw === 'undefined') { logWarn('createtransaksi.bad_request', { id: req.id })
logWarn('createtransaksi.bad_request', { id: req.id, hasOrderId: !!order_id, hasNominal: typeof nominalRaw !== 'undefined' }) return res.status(400).json({ error: 'BAD_REQUEST', message: 'item_id and nominal are required' })
return res.status(400).json({ error: 'BAD_REQUEST', message: 'order_id (mercant_id atau item[0].item_id) dan nominal wajib ada' })
} }
const order_id = String(itemId)
const nominal = Number(nominalRaw) const nominal = Number(nominalRaw)
const now = Date.now() const now = Date.now()
const ttlMin = PAYMENT_LINK_TTL_MINUTES > 0 ? PAYMENT_LINK_TTL_MINUTES : 30 const ttlMin = PAYMENT_LINK_TTL_MINUTES > 0 ? PAYMENT_LINK_TTL_MINUTES : 30
const expire_at = now + ttlMin * 60 * 1000 const expire_at = now + ttlMin * 60 * 1000
// Block jika sudah selesai // Block if already completed
if (notifiedOrders.has(order_id)) { if (notifiedOrders.has(order_id)) {
logWarn('createtransaksi.completed', { order_id }) logWarn('createtransaksi.completed', { order_id })
return res.status(409).json({ error: 'ORDER_COMPLETED', message: 'Order already completed' }) return res.status(409).json({ error: 'ORDER_COMPLETED', message: 'Order already completed' })
} }
// Block jika ada link aktif belum expired // Block if active link exists and not expired yet
const existing = activeOrders.get(order_id) const existing = activeOrders.get(order_id)
if (existing && existing > now) { if (existing && existing > now) {
logWarn('createtransaksi.active_exists', { order_id }) logWarn('createtransaksi.active_exists', { order_id })
@ -303,9 +287,7 @@ app.post('/createtransaksi', (req, res) => {
const url = `${PAYMENT_LINK_BASE}/${token}` const url = `${PAYMENT_LINK_BASE}/${token}`
activeOrders.set(order_id, expire_at) activeOrders.set(order_id, expire_at)
logInfo('createtransaksi.issued', { order_id, expire_at }) logInfo('createtransaksi.issued', { order_id, expire_at })
res.json({ url, token, order_id, nominal, expire_at })
// Respons mengikuti format yang diminta
res.json({ status: '200', messages: 'SUCCESS', data: { url } })
} catch (e) { } catch (e) {
logError('createtransaksi.error', { id: req.id, message: e?.message }) logError('createtransaksi.error', { id: req.id, message: e?.message })
res.status(500).json({ error: 'CREATE_ERROR', message: e?.message || 'Internal error' }) res.status(500).json({ error: 'CREATE_ERROR', message: e?.message || 'Internal error' })

View File

@ -1,21 +1,6 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './styles/globals.css' import './styles/globals.css'
// Force light theme in case hosting injects a global 'dark' class
(() => {
const html = document.documentElement
try {
if (html.classList.contains('dark')) {
html.classList.remove('dark')
}
document.body.classList.remove('dark')
// Hint browsers to prefer light color scheme
html.style.colorScheme = 'light'
html.setAttribute('data-theme', 'light')
} catch {
// noop
}
})()
import { AppRouter } from './app/router' import { AppRouter } from './app/router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

View File

@ -178,6 +178,13 @@ export function CheckoutPage() {
if (m === 'bank_transfer') { if (m === 'bank_transfer') {
return ( return (
<div className="space-y-2" aria-live="polite"> <div className="space-y-2" aria-live="polite">
<div className="flex justify-center">
<img
src="/logos/logo-semua-bank.PNG"
alt="Logo semua bank yang didukung"
className="max-h-20 sm:max-h-24 object-contain"
/>
</div>
<div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div> <div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div>
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}> <div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => ( {(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => (

View File

@ -7,7 +7,6 @@ import { BankTransferPanel } from '../features/payments/components/BankTransferP
import { CardPanel } from '../features/payments/components/CardPanel' import { CardPanel } from '../features/payments/components/CardPanel'
import { GoPayPanel } from '../features/payments/components/GoPayPanel' import { GoPayPanel } from '../features/payments/components/GoPayPanel'
import { CStorePanel } from '../features/payments/components/CStorePanel' import { CStorePanel } from '../features/payments/components/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'
@ -22,13 +21,11 @@ export function PayPage() {
const [expireAt, setExpireAt] = useState<number>(Date.now() + 30 * 60 * 1000) const [expireAt, setExpireAt] = useState<number>(Date.now() + 30 * 60 * 1000)
const [selectedMethod, setSelectedMethod] = useState<Method>(null) const [selectedMethod, setSelectedMethod] = useState<Method>(null)
const [locked, setLocked] = useState<boolean>(false) const [locked, setLocked] = useState<boolean>(false)
const [selectedBank, setSelectedBank] = useState<BankKey | null>(null) const [selectedBank, setSelectedBank] = useState<string>('bca')
const [selectedStore, setSelectedStore] = useState<'alfamart' | 'indomaret' | null>(null) const [selectedStore, setSelectedStore] = useState<string>('indomaret')
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 [isBusy, setIsBusy] = useState(false)
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -113,135 +110,91 @@ export function PayPage() {
orderId={orderId} orderId={orderId}
amount={amount} amount={amount}
expireAt={expireAt} expireAt={expireAt}
showStatusCTA={currentStep === 3} showStatusCTA={false}
> >
<div className="space-y-4"> <div className="space-y-4">
{currentStep === 2 && ( <h2 className="text-lg font-semibold">Pilih metode pembayaran</h2>
<div className="space-y-3"> <PaymentMethodList
<PaymentMethodList selected={selectedMethod ?? undefined}
selected={selectedMethod ?? undefined} onSelect={(m) => setSelectedMethod(m as Method)}
onSelect={(m) => { disabled={locked}
setSelectedMethod(m as Method) enabled={enabledMap}
if (m === 'bank_transfer' || m === 'cstore') { renderPanel={(m) => {
// Panel pemilihan bank/toko akan muncul di bawah item, dan lanjut ke step 3 setelah memilih if (m === 'bank_transfer') return (
} else if (m === 'cpay') { <div className="mt-2">
try { <p className="text-sm text-slate-600">Pilih bank</p>
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank') <div className="mt-2 flex gap-2">
} catch {} {['bca', 'bni', 'bri', 'permata'].map((bank) => (
} else { <button
setIsBusy(true) key={bank}
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300) className={`px-3 py-2 rounded border ${selectedBank === bank ? 'border-blue-600' : 'border-slate-300'}`}
} onClick={() => setSelectedBank(bank)}
}} >
disabled={locked} {bank.toUpperCase()}
enabled={enabledMap} </button>
renderPanel={(m) => { ))}
const enabled = enabledMap[m] </div>
if (!enabled) { </div>
return ( )
<div className="p-2"> if (m === 'cstore') return (
<Alert title="Metode nonaktif">Metode pembayaran ini dinonaktifkan di konfigurasi lingkungan.</Alert> <div className="mt-2">
</div> <p className="text-sm text-slate-600">Pilih gerai</p>
) <div className="mt-2 flex gap-2">
} {['indomaret', 'alfamart'].map((store) => (
if (m === 'bank_transfer') { <button
return ( key={store}
<div className="space-y-2" aria-live="polite"> className={`px-3 py-2 rounded border ${selectedStore === store ? 'border-blue-600' : 'border-slate-300'}`}
<div className="text-xs text-black/60 dark:text-white/60">Pilih bank untuk membuat Virtual Account</div> onClick={() => setSelectedStore(store)}
<div className={`grid grid-cols-3 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}> >
{(['bca','bni','bri','cimb','mandiri','permata'] as BankKey[]).map((bk) => ( {store}
<button </button>
key={bk} ))}
type="button" </div>
onClick={() => { </div>
setSelectedBank(bk) )
setIsBusy(true) return null
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300) }}
}} />
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
aria-label={`Pilih bank ${bk.toUpperCase()}`}
>
<BankLogo bank={bk} />
</button>
))}
</div>
{isBusy && (
<div className="text-xs text-black/60 dark:text-white/60 inline-flex items-center gap-2">
<span className="h-3 w-3 animate-spin rounded-full border-2 border-black/40 dark:border-white/40 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-black/60 dark:text-white/60">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) => (
<button
key={st}
type="button"
onClick={() => {
setSelectedStore(st)
setIsBusy(true)
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
}}
className="rounded border border-black/10 dark:border-white/10 bg-white dark:bg-black/20 p-2 flex items-center justify-center overflow-hidden hover:bg-black/5 dark:hover:bg-white/10"
aria-label={`Pilih toko ${st.toUpperCase()}`}
>
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
</button>
))}
</div>
</div>
)
}
return null
}}
/>
</div>
)}
{currentStep === 3 && ( <div className="mt-6">
<div className="space-y-3" aria-live="polite"> {selectedMethod === 'bank_transfer' && (
{selectedMethod === 'bank_transfer' && ( <BankTransferPanel
<BankTransferPanel locked={locked}
locked={locked} onChargeInitiated={() => setLocked(true)}
onChargeInitiated={() => setLocked(true)} orderId={orderId}
orderId={orderId} amount={amount}
amount={amount} defaultBank={selectedBank as any}
defaultBank={(selectedBank ?? 'bca')} />
/> )}
)}
{selectedMethod === 'credit_card' && ( {selectedMethod === 'credit_card' && (
<CardPanel <CardPanel
locked={locked} locked={locked}
onChargeInitiated={() => setLocked(true)} onChargeInitiated={() => setLocked(true)}
orderId={orderId} orderId={orderId}
amount={amount} amount={amount}
/> />
)} )}
{selectedMethod === 'gopay' && (
<GoPayPanel {selectedMethod === 'gopay' && (
locked={locked} <GoPayPanel
onChargeInitiated={() => setLocked(true)} locked={locked}
orderId={orderId} onChargeInitiated={() => setLocked(true)}
amount={amount} orderId={orderId}
/> amount={amount}
)} />
{selectedMethod === 'cstore' && ( )}
<CStorePanel
locked={locked} {selectedMethod === 'cstore' && (
onChargeInitiated={() => setLocked(true)} <CStorePanel
orderId={orderId} locked={locked}
amount={amount} onChargeInitiated={() => setLocked(true)}
defaultStore={selectedStore ?? undefined} orderId={orderId}
/> amount={amount}
)} defaultStore={selectedStore as any}
</div> />
)} )}
</div>
</div> </div>
</PaymentSheet> </PaymentSheet>
) )

View File

@ -49,11 +49,9 @@ api.interceptors.response.use(
return response return response
}, },
(error) => { (error) => {
const baseURL = error.config?.baseURL || ''
const url = error.config?.url || '' const url = error.config?.url || ''
const status = error.response?.status const status = error.response?.status
const fullUrl = `${baseURL}${url}` Logger.error('api.error', { url, status, message: error.message })
Logger.error('api.error', { baseURL, url, fullUrl, status, message: error.message })
throw error throw error
} }
) )

View File

@ -13,7 +13,6 @@ body {
background-color: #ffffff; background-color: #ffffff;
color: #000000; color: #000000;
} }
.light, html[data-theme="light"] body { background-color: #ffffff; color: #000000; }
.dark body { background-color: #000000; color: #ffffff; } .dark body { background-color: #000000; color: #ffffff; }
a { color: #dc2626; } a { color: #dc2626; }
a:hover { color: #b91c1c; } a:hover { color: #b91c1c; }