feat/payment-ux-story-1-1 #14
|
|
@ -2,9 +2,9 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/simaya.png"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>core-midtrans-cifo</title>
|
<title>Simaya Midtrans | Retail Payment</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
|
@ -124,7 +124,7 @@ const ENABLE = {
|
||||||
// --- Payment Link Config
|
// --- Payment Link Config
|
||||||
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || ''
|
const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || ''
|
||||||
const PAYMENT_LINK_SECRET = process.env.PAYMENT_LINK_SECRET || ''
|
const PAYMENT_LINK_SECRET = process.env.PAYMENT_LINK_SECRET || ''
|
||||||
const PAYMENT_LINK_TTL_MINUTES = parseInt(process.env.PAYMENT_LINK_TTL_MINUTES || '30', 10)
|
const PAYMENT_LINK_TTL_MINUTES = parseInt(process.env.PAYMENT_LINK_TTL_MINUTES || '1440', 10)
|
||||||
const PAYMENT_LINK_BASE = process.env.PAYMENT_LINK_BASE || 'http://localhost:5174/pay'
|
const PAYMENT_LINK_BASE = process.env.PAYMENT_LINK_BASE || 'http://localhost:5174/pay'
|
||||||
const activeOrders = new Map() // order_id -> expire_at
|
const activeOrders = new Map() // order_id -> expire_at
|
||||||
// Map untuk menyimpan mercant_id per order_id agar notifikasi ERP bisa dinamis
|
// Map untuk menyimpan mercant_id per order_id agar notifikasi ERP bisa dinamis
|
||||||
|
|
@ -234,7 +234,7 @@ app.get('/api/payment-links/:token', (req, res) => {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
logWarn('paymentlink.invalid', { error: result.error })
|
logWarn('paymentlink.invalid', { error: result.error })
|
||||||
if (isDevEnv()) {
|
if (isDevEnv()) {
|
||||||
const ttlMin = PAYMENT_LINK_TTL_MINUTES > 0 ? PAYMENT_LINK_TTL_MINUTES : 30
|
const ttlMin = PAYMENT_LINK_TTL_MINUTES > 0 ? PAYMENT_LINK_TTL_MINUTES : 1440
|
||||||
const fallback = { order_id: token, nominal: 150000, expire_at: Date.now() + ttlMin * 60 * 1000 }
|
const fallback = { order_id: token, nominal: 150000, expire_at: Date.now() + ttlMin * 60 * 1000 }
|
||||||
logInfo('paymentlink.dev.fallback', { order_id: fallback.order_id })
|
logInfo('paymentlink.dev.fallback', { order_id: fallback.order_id })
|
||||||
return res.json(fallback)
|
return res.json(fallback)
|
||||||
|
|
@ -386,7 +386,7 @@ app.post('/createtransaksi', async (req, res) => {
|
||||||
}
|
}
|
||||||
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 : 1440
|
||||||
const expire_at = now + ttlMin * 60 * 1000
|
const expire_at = now + ttlMin * 60 * 1000
|
||||||
|
|
||||||
// Block jika sudah selesai
|
// Block jika sudah selesai
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
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 { PayPage } from '../pages/PayPage'
|
import { PayPage } from '../pages/PayPage'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
|
|
@ -13,8 +14,8 @@ const router = createBrowserRouter([
|
||||||
element: <AppLayout />,
|
element: <AppLayout />,
|
||||||
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: <DemoStorePage /> },
|
{ index: true, element: <InitPage /> },
|
||||||
{ path: 'checkout', element: <CheckoutPage /> },
|
// { path: 'checkout', element: <CheckoutPage /> },
|
||||||
{ 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 /> },
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export function CStoreLogosRow({ compact = false, size }: { compact?: boolean; s
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<LogoAlfamart compact={compact} size={size} />
|
<LogoAlfamart compact={compact} size={size} />
|
||||||
<LogoIndomaret compact={compact} size={size} />
|
{/* <LogoIndomaret compact={compact} size={size} /> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ function useCountdown(expireAt: number) {
|
||||||
}, [])
|
}, [])
|
||||||
const remainMs = Math.max(0, expireAt - now)
|
const remainMs = Math.max(0, expireAt - now)
|
||||||
const totalSec = Math.floor(remainMs / 1000)
|
const totalSec = Math.floor(remainMs / 1000)
|
||||||
const mm = String(Math.floor(totalSec / 60)).padStart(2, '0')
|
const hh = String(Math.floor(totalSec / 3600)).padStart(2, '0')
|
||||||
|
const mm = String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0')
|
||||||
const ss = String(totalSec % 60).padStart(2, '0')
|
const ss = String(totalSec % 60).padStart(2, '0')
|
||||||
return `${mm}:${ss}`
|
return `${hh}:${mm}:${ss}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentSheetProps {
|
export interface PaymentSheetProps {
|
||||||
|
|
@ -63,22 +64,21 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Amount panel */}
|
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="p-4 border-b border-black/10 flex items-start justify-between">
|
<div className="p-4 border-b border-black/10 flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-black/60">Total</div>
|
<div className="text-xs text-black">Total</div>
|
||||||
<div className="text-xl font-semibold">{formatCurrencyIDR(amount)}</div>
|
<div className="text-xl font-semibold">{formatCurrencyIDR(amount)}</div>
|
||||||
<div className="text-xs text-black/60">Order ID #{orderId}</div>
|
<div className="text-xs text-black/60">Order ID #{orderId}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Body */}
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{children}
|
{children}
|
||||||
<TrustStrip location="sheet" />
|
<TrustStrip location="sheet" />
|
||||||
</div>
|
</div>
|
||||||
{/* Sticky CTA (mobile-friendly) */}
|
|
||||||
{showStatusCTA && (
|
{showStatusCTA && (
|
||||||
<div className="sticky bottom-0 bg-white/95 backdrop-blur border-t border-black/10 p-3 pb-[env(safe-area-inset-bottom)]">
|
<div className="sticky bottom-0 bg-white/95 backdrop-blur border-t border-black/10 p-3 pb-[env(safe-area-inset-bottom)]">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -93,4 +93,4 @@ export function PaymentSheet({ merchantName = 'Simaya', orderId, amount, expireA
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export function usePaymentNavigation() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
return {
|
return {
|
||||||
toCheckout() {
|
toCheckout() {
|
||||||
navigate('/checkout')
|
window.location.assign('https://erpskrip.id/pembayaran-pelanggan')
|
||||||
},
|
},
|
||||||
toStatus(orderId: string, method?: string) {
|
toStatus(orderId: string, method?: string) {
|
||||||
const qs = method ? `?m=${encodeURIComponent(method)}` : ''
|
const qs = method ? `?m=${encodeURIComponent(method)}` : ''
|
||||||
|
|
@ -14,4 +14,4 @@ export function usePaymentNavigation() {
|
||||||
navigate('/history')
|
navigate('/history')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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
|
const html = document.documentElement
|
||||||
try {
|
try {
|
||||||
|
|
@ -9,11 +9,9 @@ import './styles/globals.css'
|
||||||
html.classList.remove('dark')
|
html.classList.remove('dark')
|
||||||
}
|
}
|
||||||
document.body.classList.remove('dark')
|
document.body.classList.remove('dark')
|
||||||
// Hint browsers to prefer light color scheme
|
|
||||||
html.style.colorScheme = 'light'
|
html.style.colorScheme = 'light'
|
||||||
html.setAttribute('data-theme', 'light')
|
html.setAttribute('data-theme', 'light')
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
import { AppRouter } from './app/router'
|
import { AppRouter } from './app/router'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function InitPage() {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-white overflow-hidden flex items-center justify-center p-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<img src="/simaya.png" alt="Simaya" className="h-12 w-12 md:h-16 md:w-16" />
|
||||||
|
<div className="text-black">
|
||||||
|
<div className="text-xl font-semibold leading-none">Simaya Midtrans</div>
|
||||||
|
<div className="text-sm leading-none">Payment Service</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@ export function NotFoundPage() {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h1 className="text-xl font-semibold">Halaman tidak ditemukan</h1>
|
<h1 className="text-xl font-semibold">Halaman tidak ditemukan</h1>
|
||||||
<p className="text-sm text-black/70">Periksa URL atau kembali ke checkout.</p>
|
<p className="text-sm text-black/70">Periksa URL atau kembali ke checkout.</p>
|
||||||
<Link to="/checkout" className="text-brand-600 underline">Kembali ke Checkout</Link>
|
{/* <Link to="/checkout" className="text-brand-600 underline">Kembali ke Checkout</Link> */}
|
||||||
|
<Link to="/" className="text-brand-600 underline">Kembali</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ export function PayPage() {
|
||||||
const { token } = useParams()
|
const { token } = useParams()
|
||||||
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() + 30 * 60 * 1000)
|
const [expireAt, setExpireAt] = useState<number>(Date.now() + 24 * 60 * 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<BankKey | null>(null)
|
||||||
|
|
@ -39,7 +39,7 @@ export function PayPage() {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setOrderId(payload.order_id)
|
setOrderId(payload.order_id)
|
||||||
setAmount(payload.nominal)
|
setAmount(payload.nominal)
|
||||||
setExpireAt(payload.expire_at ?? Date.now() + 30 * 60 * 1000)
|
setExpireAt(payload.expire_at ?? Date.now() + 24 * 60 * 60 * 1000)
|
||||||
setAllowedMethods(payload.allowed_methods)
|
setAllowedMethods(payload.allowed_methods)
|
||||||
setError(null)
|
setError(null)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -53,7 +53,7 @@ export function PayPage() {
|
||||||
}
|
}
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
const merchantName = useMemo(() => 'Simaya Retail', [])
|
const merchantName = useMemo(() => '', [])
|
||||||
|
|
||||||
const isExpired = expireAt ? Date.now() > expireAt : false
|
const isExpired = expireAt ? Date.now() > expireAt : false
|
||||||
const enabledMap: Record<PaymentMethod, boolean> = useMemo(() => {
|
const enabledMap: Record<PaymentMethod, boolean> = useMemo(() => {
|
||||||
|
|
@ -88,16 +88,17 @@ export function PayPage() {
|
||||||
>
|
>
|
||||||
<div className="space-y-4 px-4 py-6">
|
<div className="space-y-4 px-4 py-6">
|
||||||
<Alert title={title}>{msg}</Alert>
|
<Alert title={title}>{msg}</Alert>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => { try { window.location.reload() } catch { } }}
|
onClick={() => { try { window.location.reload() } catch { } }}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
Muat ulang
|
Muat ulang
|
||||||
</Button>
|
</Button>
|
||||||
<a
|
<a
|
||||||
href="mailto:retailimaya@gmail.com?subject=Permintaan%20Link%20Pembayaran&body=Order%20ID:%20"
|
href="mailto:retailimaya@gmail.com?subject=Permintaan%20Link%20Pembayaran&body=Order%20ID:%20"
|
||||||
className="inline-flex items-center px-3 py-2 rounded border bg-gray-800 !text-white hover:!text-white focus:!text-white visited:!text-white active:!text-white"
|
className="inline-flex items-center px-3 py-2 rounded border bg-gray-800 !text-white hover:!text-white focus:!text-white visited:!text-white active:!text-white w-full sm:w-auto justify-center"
|
||||||
>
|
>
|
||||||
Hubungi Admin
|
Hubungi Admin
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -123,7 +124,6 @@ export function PayPage() {
|
||||||
onSelect={(m) => {
|
onSelect={(m) => {
|
||||||
setSelectedMethod(m as Method)
|
setSelectedMethod(m as Method)
|
||||||
if (m === 'bank_transfer' || m === 'cstore') {
|
if (m === 'bank_transfer' || m === 'cstore') {
|
||||||
// Panel pemilihan bank/toko akan muncul di bawah item, dan lanjut ke step 3 setelah memilih
|
|
||||||
} else if (m === 'cpay') {
|
} else if (m === 'cpay') {
|
||||||
try {
|
try {
|
||||||
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
window.open('https://play.google.com/store/apps/details?id=com.cifo.walanja', '_blank')
|
||||||
|
|
@ -148,7 +148,7 @@ export function PayPage() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-gray-600">Pilih bank untuk membuat Virtual Account</div>
|
<div className="text-xs text-gray-600">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-2 md: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) => (
|
||||||
<button
|
<button
|
||||||
key={bk}
|
key={bk}
|
||||||
|
|
@ -158,7 +158,7 @@ export function PayPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center overflow-hidden hover:bg-gray-100"
|
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()}`}
|
aria-label={`Pilih bank ${bk.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
<BankLogo bank={bk} />
|
<BankLogo bank={bk} />
|
||||||
|
|
@ -179,7 +179,8 @@ export function PayPage() {
|
||||||
<div className="space-y-2" aria-live="polite">
|
<div className="space-y-2" aria-live="polite">
|
||||||
<div className="text-xs text-gray-600">Pilih toko untuk membuat kode pembayaran</div>
|
<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' : ''}`}>
|
<div className={`grid grid-cols-2 gap-2 ${isBusy ? 'pointer-events-none opacity-60' : ''}`}>
|
||||||
{(['alfamart', 'indomaret'] as const).map((st) => (
|
{/* {(['alfamart', 'indomaret'] as const).map((st) => ( */}
|
||||||
|
{(['alfamart'] as const).map((st) => (
|
||||||
<button
|
<button
|
||||||
key={st}
|
key={st}
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -188,7 +189,7 @@ export function PayPage() {
|
||||||
setIsBusy(true)
|
setIsBusy(true)
|
||||||
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
setTimeout(() => { setCurrentStep(3); setIsBusy(false) }, 300)
|
||||||
}}
|
}}
|
||||||
className="rounded border border-gray-300 bg-white p-2 flex items-center justify-center hover:bg-gray-100"
|
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()}`}
|
aria-label={`Pilih toko ${st.toUpperCase()}`}
|
||||||
>
|
>
|
||||||
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
{st === 'alfamart' ? <LogoAlfamart /> : <LogoIndomaret />}
|
||||||
|
|
@ -245,4 +246,4 @@ export function PayPage() {
|
||||||
</div>
|
</div>
|
||||||
</PaymentSheet>
|
</PaymentSheet>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export function PaymentStatusPage() {
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
<Button onClick={() => nav.toHistory()}>Lihat Riwayat</Button>
|
<Button onClick={() => nav.toHistory()}>Lihat Riwayat</Button>
|
||||||
<Button variant="secondary" onClick={() => nav.toCheckout()}>Kembali ke Checkout</Button>
|
<Button variant="secondary" onClick={() => nav.toCheckout()}>Kembali</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!Env.API_BASE_URL && (
|
{!Env.API_BASE_URL && (
|
||||||
|
|
|
||||||
|
|
@ -129,22 +129,22 @@ export async function getPaymentLinkPayload(token: string): Promise<PaymentLinkP
|
||||||
Logger.info('paymentlink.resolve', { tokenLen: token.length })
|
Logger.info('paymentlink.resolve', { tokenLen: token.length })
|
||||||
return data as PaymentLinkPayload
|
return data as PaymentLinkPayload
|
||||||
}
|
}
|
||||||
// Fallback when API base not set or resolver unavailable
|
|
||||||
// Try a best-effort decode of base64(JSON) payload; if fails, use defaults
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(atob(token))
|
const json = JSON.parse(atob(token))
|
||||||
return {
|
return {
|
||||||
order_id: json.order_id || token,
|
order_id: json.order_id || token,
|
||||||
nominal: Number(json.nominal) || 150000,
|
nominal: Number(json.nominal) || 150000,
|
||||||
customer: json.customer || {},
|
customer: json.customer || {},
|
||||||
expire_at: json.expire_at || Date.now() + 30 * 60 * 1000,
|
expire_at: json.expire_at || Date.now() + 24 * 60 * 60 * 1000
|
||||||
|
,
|
||||||
allowed_methods: json.allowed_methods || undefined,
|
allowed_methods: json.allowed_methods || undefined,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
order_id: token,
|
order_id: token,
|
||||||
nominal: 150000,
|
nominal: 150000,
|
||||||
expire_at: Date.now() + 30 * 60 * 1000,
|
expire_at: Date.now() + 24 * 60 * 60 * 1000
|
||||||
|
,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue