/* ============================================================= CoreIQ Office — Payments (Pay by link) Collect a payment from a customer by sending them a Stripe checkout link (SMS or email); watch it go from Awaiting → Paid in real time; refund from the history. Funds route to THIS store's Stripe Connect account — the API resolves the pharmacy from the signed-in tenant, the browser never names it. No-fake-data: every number comes from /payments/* (the Connect service). If payments aren't set up, we show the onboarding card. ============================================================= */ const { useState: payUseState, useEffect: payUseEffect, useRef: payUseRef } = React; const payMoney = (n) => n == null ? "—" : "$" + Number(n).toLocaleString("en-AU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const PAY_STATUS_STYLE = { paid: { bg: "#e6f6ec", fg: "#137333", label: "Paid" }, pending: { bg: "#fef7e0", fg: "#8a6d00", label: "Awaiting" }, cancelled: { bg: "#f1f3f4", fg: "#5f6368", label: "Cancelled" }, expired: { bg: "#f1f3f4", fg: "#5f6368", label: "Expired" }, refunded: { bg: "#e8eaed", fg: "#3c4043", label: "Refunded" }, }; function PayStatusPill({ status }) { const s = PAY_STATUS_STYLE[status] || { bg: "#f1f3f4", fg: "#5f6368", label: status || "—" }; return ( {s.label} ); } function PaymentsScreen() { const { pushToast } = window.useOffice(); // Onboarding status: null = loading, {chargesEnabled,...} once known. const [connect, setConnect] = payUseState(null); const [connectErr, setConnectErr] = payUseState(null); // Collect-payment form. const [amount, setAmount] = payUseState(""); const [description, setDescription] = payUseState(""); const [name, setName] = payUseState(""); const [method, setMethod] = payUseState("sms"); // 'sms' | 'email' const [contact, setContact] = payUseState(""); const [sending, setSending] = payUseState(false); // The link we just created (so we can show its live status). const [active, setActive] = payUseState(null); // {sessionId, paymentUrl, status} // Transaction history. const [txns, setTxns] = payUseState(null); // null = loading const pollRef = payUseRef(null); const loadConnect = () => { setConnect(null); setConnectErr(null); window.OfficeAPI.paymentConnectStatus() .then(setConnect) .catch((e) => setConnectErr((e && e.message) || String(e))); }; const loadTxns = () => window.OfficeAPI.listPaymentTransactions({ limit: 25 }) .then((r) => setTxns((r && r.transactions) || [])) .catch(() => setTxns([])); payUseEffect(() => { loadConnect(); loadTxns(); return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, []); const isActive = connect && connect.stripeOnboardingStatus === "active" && connect.chargesEnabled; // Business details are filled server-side from the store's own records; // the click just fetches a fresh Stripe-hosted onboarding link. Stripe links // are single-use and short-lived, so open it immediately. const startOnboarding = () => { window.OfficeAPI.startPaymentOnboarding({}) .then((r) => { if (r && r.onboardingUrl) { window.open(r.onboardingUrl, "_blank", "noopener"); pushToast && pushToast("Complete onboarding in the new tab, then click Refresh."); } }) .catch((e) => pushToast && pushToast("Onboarding failed: " + ((e && e.message) || e))); }; const startPolling = (sessionId) => { if (pollRef.current) clearInterval(pollRef.current); pollRef.current = setInterval(() => { window.OfficeAPI.listPaymentTransactions({ limit: 25 }) .then((r) => { const list = (r && r.transactions) || []; setTxns(list); const t = list.find((x) => x.sessionId === sessionId); if (t && t.status !== "pending") { setActive((prev) => (prev ? { ...prev, status: t.status } : prev)); if (["paid", "cancelled", "expired", "refunded"].includes(t.status)) { clearInterval(pollRef.current); pollRef.current = null; if (t.status === "paid") pushToast && pushToast("Payment received — " + payMoney(t.amount)); } } }) .catch(() => {}); }, 5000); }; const send = () => { const amt = Number(amount); if (!amt || amt <= 0) return pushToast && pushToast("Enter a positive amount."); if (!name.trim()) return pushToast && pushToast("Enter the customer's name."); if (!contact.trim()) return pushToast && pushToast(method === "sms" ? "Enter a mobile number." : "Enter an email."); setSending(true); setActive(null); window.OfficeAPI.createPaymentLink({ amount: amt, description: description.trim() || undefined, patientName: name.trim(), deliveryMethod: method, patientPhone: method === "sms" ? contact.trim() : undefined, patientEmail: method === "email" ? contact.trim() : undefined, }) .then((r) => { setActive({ sessionId: r.sessionId, paymentUrl: r.paymentUrl, status: "pending", amount: amt, name: name.trim() }); startPolling(r.sessionId); loadTxns(); pushToast && pushToast(method === "sms" ? "Payment link sent by SMS." : "Payment link sent by email."); }) .catch((e) => pushToast && pushToast("Couldn't send: " + ((e && e.message) || e))) .finally(() => setSending(false)); }; const clearForm = () => { setAmount(""); setDescription(""); setName(""); setContact(""); setActive(null); if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; const refund = (t) => { if (!window.confirm("Refund " + payMoney(t.amount) + " to " + (t.patientName || "the customer") + "?")) return; window.OfficeAPI.refundPayment(t.sessionId, undefined, "requested_by_customer") .then(() => { pushToast && pushToast("Refund issued."); loadTxns(); }) .catch((e) => pushToast && pushToast("Refund failed: " + ((e && e.message) || e))); }; const input = { width: "100%", padding: "9px 11px", border: "1px solid #dadce0", borderRadius: 8, fontSize: 14, boxSizing: "border-box" }; const label = { fontSize: 12, fontWeight: 600, color: "#5f6368", marginBottom: 4, display: "block" }; return (
This store isn't set up to collect card payments yet. Complete a one-time Stripe onboarding (bank account + ID) to start sending payment links. {connect.stripeOnboardingStatus === "pending" || connect.stripeOnboardingStatus === "incomplete" ? " Onboarding was started but isn't finished." : ""}
| When | Customer | Amount | Status | |
|---|---|---|---|---|
| {t.createdAt ? new Date(t.createdAt).toLocaleString("en-AU", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : "—"} | {t.patientName || "—"} | {payMoney(t.amount)} | {t.status === "paid" && ( )} |