/* Multi-step Xendit renewal checkout flow */ const { useState: useStateCheckout, useMemo: useMemoCheckout } = React; const DURATIONS = [ { id: 1, label: "1 Bulan", months: 1 }, { id: 3, label: "3 Bulan", months: 3, badge: "Hemat 3%", discount: 0.03 }, { id: 6, label: "6 Bulan", months: 6, badge: "Hemat 5%", discount: 0.05 }, { id: 12, label: "12 Bulan", months: 12, badge: "Hemat 10%", discount: 0.10, recommended: true }, ]; const PAYMENT_METHODS = [ { id:"bca", group:"va", label:"BCA Virtual Account", fee:4000, iconBg:"#0060AF", iconText:"BCA" }, { id:"mandiri", group:"va", label:"Mandiri Virtual Account", fee:4000, iconBg:"#003D7A", iconText:"M" }, { id:"bni", group:"va", label:"BNI Virtual Account", fee:4000, iconBg:"#F46524", iconText:"BNI" }, { id:"bri", group:"va", label:"BRI Virtual Account", fee:4000, iconBg:"#005CAB", iconText:"BRI" }, { id:"qris", group:"qris", label:"QRIS", fee:0, iconBg:"#1A1A1A", iconText:"QR" }, { id:"ovo", group:"ewallet", label:"OVO", fee:0, iconBg:"#4C2A85", iconText:"O" }, { id:"gopay", group:"ewallet", label:"GoPay", fee:0, iconBg:"#00AED6", iconText:"G" }, { id:"dana", group:"ewallet", label:"DANA", fee:0, iconBg:"#118EEA", iconText:"D" }, { id:"shopeepay", group:"ewallet", label:"ShopeePay", fee:0, iconBg:"#EE4D2D", iconText:"S" }, ]; function CheckoutView() { const { route, machines, navigate, client, toast } = useApp(); const m = machines.find(x => x.id === route.id) || machines[0]; const [step, setStep] = useStateCheckout(1); const [duration, setDuration] = useStateCheckout(DURATIONS[2]); const [addons, setAddons] = useStateCheckout({ cloud: m.addons.cloud, refill: m.addons.refill, cloudApi: m.addons.cloudApi, cloudYearly: true, }); const [method, setMethod] = useStateCheckout(null); // Pricing const calc = useMemoCheckout(() => { const cloudRate = addons.cloudYearly ? 350000 : 400000; const monthlyItems = [ { label:"Sewa Pokok", desc:m.model, qty:duration.months, unit:m.rates.sewa, total:duration.months*m.rates.sewa, required:true }, ...(addons.cloud ? [{ label:"Cloud Server / Dashboard", desc:addons.cloudYearly?"tahunan":"bulanan", qty:duration.months, unit:cloudRate, total:duration.months*cloudRate }] : []), ...(addons.refill ? [{ label:"Jasa Refill", desc:"max 10x/bln", qty:duration.months, unit:1500000, total:duration.months*1500000 }] : []), ...(addons.cloudApi ? [{ label:"Cloud API", desc:"integrasi data", qty:duration.months, unit:100000, total:duration.months*100000 }] : []), ]; const subtotal = monthlyItems.reduce((a,b) => a+b.total, 0); const discount = Math.round(subtotal * (duration.discount || 0)); const afterDisc = subtotal - discount; const ppn = Math.round(afterDisc * 0.11); const adminFee = method?.fee || 0; const total = afterDisc + ppn + adminFee; return { items: monthlyItems, subtotal, discount, afterDisc, ppn, adminFee, total }; }, [duration, addons, method, m]); // Compute new end date const newEnd = useMemoCheckout(() => { const d = new Date(m.contractEnd); d.setMonth(d.getMonth() + duration.months); return d.toISOString().split("T")[0]; }, [duration, m]); return (
{step === 1 && setStep(2)} calc={calc}/>} {step === 2 && setStep(1)} onNext={() => setStep(3)}/>} {step === 3 && setStep(2)} onPay={() => { if (!method) { toast("Pilih metode pembayaran dulu.", "warning"); return; } navigate({ name: "payment-success", id: m.id, method: method.label, total: calc.total, newEnd }); }}/>}
); } function Stepper({ step }) { const steps = [ { id:1, label:"Pilih Durasi" }, { id:2, label:"Ringkasan" }, { id:3, label:"Pembayaran" }, ]; return (
{steps.map((s, i) => (
s.id ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-500" }`}> s.id ? "bg-emerald-600 text-white" : step===s.id?"bg-white text-[#FF2D55]":"bg-white text-stone-500"}`}> {step > s.id ? : s.id} {s.label}
{i < steps.length - 1 &&
s.id ? "bg-emerald-300" : "bg-stone-200"}`}>
}
))}
); } /* STEP 1 — Pilih durasi + add-ons */ function Step1({ m, duration, setDuration, addons, setAddons, onNext, calc }) { return (
Step 1

Pilih Durasi Perpanjangan

Semakin lama, semakin hemat.

{DURATIONS.map(d => { const active = duration.id === d.id; return ( ); })}
Add-on

Layanan Tambahan

Pilih sesuai kebutuhan operasional Anda.

setAddons({...addons, cloud:v})} icon="cpu" title="Cloud Server / Dashboard" desc="Real-time monitoring penjualan, stok, dan status mesin." price={addons.cloudYearly?350000:400000} extra={
{[{id:true,l:"Tahunan",p:"Rp 350k/bln"},{id:false,l:"Bulanan",p:"Rp 400k/bln"}].map(o => ( ))}
} /> setAddons({...addons, refill:v})} icon="shoppingBag" title="Jasa Refill" desc="Tim kami yang refill produk. Maksimal 10× per bulan." price={1500000} /> setAddons({...addons, cloudApi:v})} icon="zap" title="Cloud API" desc="Integrasi data penjualan ke sistem Anda." price={100000} />
); } function AddonRow({ checked, onChange, icon, title, desc, price, extra }) { return ( ); } /* Sidebar summary */ function SummarySidebar({ m, duration, calc, onNext, ctaLabel }) { return (
Ringkasan
Total Bayar
Subtotal ({duration.months} bln){formatRp(calc.subtotal)}
{calc.discount > 0 && (
Diskon durasi−{formatRp(calc.discount)}
)}
PPN 11%{formatRp(calc.ppn)}
{calc.adminFee > 0 && (
Biaya admin{formatRp(calc.adminFee)}
)}
Total
{formatRp(calc.total)}
untuk {duration.months} bulan
Pembayaran aman via Xendit
); } /* STEP 2 — Ringkasan order */ function Step2({ m, duration, addons, calc, newEnd, onBack, onNext }) { return (
Step 2

Ringkasan Order

Periksa detail sebelum melanjutkan ke pembayaran.

{m.model}
{m.id}
{m.location}
Periode baru
{formatDateID(m.contractEnd)}
s/d
{formatDateID(newEnd)}
Item
Qty
Harga
Total
{calc.items.map((it, i) => (
{it.label}
{it.desc}
×{it.qty}
{formatRp(it.unit)}
{formatRp(it.total)}
))}
Subtotal{formatRp(calc.subtotal)}
{calc.discount > 0 &&
Diskon {Math.round((duration.discount||0)*100)}%−{formatRp(calc.discount)}
}
PPN 11%{formatRp(calc.ppn)}
Total {formatRp(calc.total - (calc.adminFee||0))}
+ biaya admin tergantung metode pembayaran
Konfirmasi

Siap bayar?

Pilih metode pembayaran di langkah berikutnya.

Invoice akan dibuat via Xendit dan dikirim ke email Anda.
); } /* STEP 3 — Pembayaran */ function Step3({ m, duration, addons, calc, method, setMethod, client, onBack, onPay }) { const groups = [ { id:"va", label:"Transfer Virtual Account", desc:"Bayar via ATM, mobile/internet banking." }, { id:"qris", label:"QRIS", desc:"Scan dengan aplikasi e-wallet atau mobile banking." }, { id:"ewallet", label:"E-Wallet", desc:"OVO, GoPay, DANA, ShopeePay." }, ]; return (
Step 3

Metode Pembayaran

Powered by Xendit. Aman, instan, dan otomatis update status.

{groups.map(g => (
{g.label}
{g.desc}
{PAYMENT_METHODS.filter(pm => pm.group === g.id).map(pm => { const active = method?.id === pm.id; return ( ); })}
))}
Xendit Invoice Payload (dev)
{`POST https://api.xendit.co/v2/invoices

{
  "external_id": "yappari-${m.id}-${Date.now()}",
  "amount": ${calc.total},
  "currency": "IDR",
  "description": "Perpanjangan ${duration.label} — ${m.model}",
  "invoice_duration": 86400,
  "customer": {
    "given_names": "${client.name}",
    "email": "${client.email}",
    "mobile_number": "${client.phone}"
  },
  "payment_methods": [${method ? `"${method.id.toUpperCase()}"` : "..."}]
}`}
Ringkasan Final
Total
{m.id} × {duration.months} bln{formatRp(calc.subtotal)}
{calc.discount > 0 &&
Diskon−{formatRp(calc.discount)}
}
PPN 11%{formatRp(calc.ppn)}
{calc.adminFee > 0 &&
Admin {method?.label}{formatRp(calc.adminFee)}
}
Total bayar
{formatRp(calc.total)}
Anda akan diarahkan ke halaman pembayaran Xendit.
); } /* ===== PAYMENT SUCCESS ===== */ function PaymentSuccessView() { const { route, navigate } = useApp(); return (
Pembayaran Berhasil

Terima kasih!

Kontrak mesin {route.id} berhasil diperpanjang sampai {formatDateID(route.newEnd)}.

Metode
{route.method}
Total bayar
{formatRp(route.total)}
Invoice ID
yappari-{route.id}-{Date.now().toString().slice(-6)}
Status
Lunas

Email konfirmasi sudah dikirim. Receipt PDF tersedia di Detail Mesin.

); } /* ===== PAYMENT FAILED ===== */ function PaymentFailedView() { const { route, navigate } = useApp(); return (
Pembayaran Gagal

Mohon dicoba lagi.

Pembayaran tidak berhasil diproses. Coba metode lain atau hubungi tim kami.

); } Object.assign(window, { CheckoutView, PaymentSuccessView, PaymentFailedView });