Convert inline error message to toast, tweak account page
This commit is contained in:
@@ -139,11 +139,7 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="md:pl-[260px] min-h-screen pb-20 md:pb-0">
|
<div class="md:pl-[260px] min-h-screen pb-20 md:pb-0">
|
||||||
{/* Shared billing prompts on every dashboard page; the billing page
|
<BillingPrompts variant="banner" />
|
||||||
renders its own contextual (inline) variant instead. */}
|
|
||||||
<Show when={location.pathname !== "/account"}>
|
|
||||||
<BillingPrompts variant="banner" />
|
|
||||||
</Show>
|
|
||||||
<main>{props.children}</main>
|
<main>{props.children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import QRCode from "qrcode"
|
|||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import PaymentSetup from "@/components/PaymentSetup"
|
import PaymentSetup from "@/components/PaymentSetup"
|
||||||
import { CardSetupBody } from "@/components/PaymentSetupShell"
|
import { CardSetupBody } from "@/components/PaymentSetupShell"
|
||||||
|
import { setToastMessage } from "@/components/Toast"
|
||||||
import { useCardPortal } from "@/lib/usePaymentSetup"
|
import { useCardPortal } from "@/lib/usePaymentSetup"
|
||||||
import { getInvoice, getInvoiceBolt11, listInvoiceItems, type Invoice } from "@/lib/api"
|
import { getInvoice, getInvoiceBolt11, listInvoiceItems, type Invoice } from "@/lib/api"
|
||||||
import { billingTenant } from "@/lib/state"
|
import { billingTenant } from "@/lib/state"
|
||||||
@@ -26,7 +27,6 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
const [bolt11Status, setBolt11Status] = createSignal<Bolt11Status>("idle")
|
const [bolt11Status, setBolt11Status] = createSignal<Bolt11Status>("idle")
|
||||||
const [bolt11Error, setBolt11Error] = createSignal("")
|
const [bolt11Error, setBolt11Error] = createSignal("")
|
||||||
const [payStatus, setPayStatus] = createSignal<PayStatus>("idle")
|
const [payStatus, setPayStatus] = createSignal<PayStatus>("idle")
|
||||||
const [payError, setPayError] = createSignal("")
|
|
||||||
const [payMethod, setPayMethod] = createSignal<PayMethod>("lightning")
|
const [payMethod, setPayMethod] = createSignal<PayMethod>("lightning")
|
||||||
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
||||||
const [setupSaved, setSetupSaved] = createSignal(false)
|
const [setupSaved, setSetupSaved] = createSignal(false)
|
||||||
@@ -67,30 +67,35 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
void loadBolt11()
|
void loadBolt11()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The card portal lives in a shared hook, so surface its failures here by
|
||||||
|
// mirroring its error signal into the toast.
|
||||||
|
createEffect(() => {
|
||||||
|
const err = card.error()
|
||||||
|
if (err) setToastMessage(err)
|
||||||
|
})
|
||||||
|
|
||||||
function copyBolt11() {
|
function copyBolt11() {
|
||||||
void navigator.clipboard.writeText(bolt11())
|
void navigator.clipboard.writeText(bolt11())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkPayment() {
|
async function checkPayment() {
|
||||||
setPayStatus("loading")
|
setPayStatus("loading")
|
||||||
setPayError("")
|
|
||||||
try {
|
try {
|
||||||
const invoice = await getInvoice(props.invoice.id)
|
const invoice = await getInvoice(props.invoice.id)
|
||||||
if (invoice.paid_at != null) {
|
if (invoice.paid_at != null) {
|
||||||
setPayStatus("success")
|
setPayStatus("success")
|
||||||
} else {
|
} else {
|
||||||
setPayStatus("error")
|
setPayStatus("error")
|
||||||
setPayError("Payment not yet confirmed. Please try again after sending.")
|
setToastMessage("Payment not yet confirmed. Please try again after sending.")
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setPayStatus("error")
|
setPayStatus("error")
|
||||||
setPayError(e instanceof Error ? e.message : "Failed to check payment status")
|
setToastMessage(e instanceof Error ? e.message : "Failed to check payment status")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
setPayStatus("idle")
|
setPayStatus("idle")
|
||||||
setPayError("")
|
|
||||||
setBolt11Status("idle")
|
setBolt11Status("idle")
|
||||||
setBolt11Error("")
|
setBolt11Error("")
|
||||||
setBolt11("")
|
setBolt11("")
|
||||||
@@ -248,7 +253,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowPaymentSetup(true)}
|
onClick={() => setShowPaymentSetup(true)}
|
||||||
class="mt-2 text-sm font-medium text-blue-600 hover:text-blue-700"
|
class="mt-2 inline-flex items-center justify-center rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-100 transition-colors"
|
||||||
>
|
>
|
||||||
Set up automatic payments
|
Set up automatic payments
|
||||||
</button>
|
</button>
|
||||||
@@ -257,13 +262,6 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
<Show when={payError() || card.error()}>
|
|
||||||
<div class="px-6 pb-2">
|
|
||||||
<p class="text-xs text-red-600 text-center">{payError() || card.error()}</p>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div class="px-6 py-4 flex justify-between gap-3 border-t border-gray-100">
|
<div class="px-6 py-4 flex justify-between gap-3 border-t border-gray-100">
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ export function activeBillingPrompt(
|
|||||||
const methodError = tenant.nwc_error ?? tenant.stripe_error
|
const methodError = tenant.nwc_error ?? tenant.stripe_error
|
||||||
const suppressInline = opts?.suppressInline ?? false
|
const suppressInline = opts?.suppressInline ?? false
|
||||||
|
|
||||||
if (s.openInvoice && !suppressInline && (!autopayConfigured || methodError)) {
|
// Any open invoice gets a "Pay now" surface, even with autopay configured:
|
||||||
|
// autopay may not have fired yet or may have failed without setting an error,
|
||||||
|
// and the user still needs a way to pay manually. Only the inline create/upgrade
|
||||||
|
// flow (suppressInline) handles its own invoice, so defer to it there.
|
||||||
|
if (s.openInvoice && !suppressInline) {
|
||||||
return {
|
return {
|
||||||
kind: "pay_invoice",
|
kind: "pay_invoice",
|
||||||
severity: "warn",
|
severity: "warn",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
|
|||||||
import PageContainer from "@/components/PageContainer"
|
import PageContainer from "@/components/PageContainer"
|
||||||
import LoadingState from "@/components/LoadingState"
|
import LoadingState from "@/components/LoadingState"
|
||||||
import PaymentDialog from "@/components/PaymentDialog"
|
import PaymentDialog from "@/components/PaymentDialog"
|
||||||
import BillingPrompts from "@/components/BillingPrompts"
|
|
||||||
import useMinLoading from "@/components/useMinLoading"
|
import useMinLoading from "@/components/useMinLoading"
|
||||||
import { useInvoicePdf } from "@/lib/useInvoicePdf"
|
import { useInvoicePdf } from "@/lib/useInvoicePdf"
|
||||||
import PaymentSetupNWC from "@/components/PaymentSetupNWC"
|
import PaymentSetupNWC from "@/components/PaymentSetupNWC"
|
||||||
@@ -78,12 +77,6 @@ export default function Account() {
|
|||||||
return { status: "ok" }
|
return { status: "ok" }
|
||||||
})
|
})
|
||||||
|
|
||||||
// The amount to surface: the total of any open invoices, else nothing owed.
|
|
||||||
const balance = createMemo(() => {
|
|
||||||
const due = billing.balance()
|
|
||||||
return due > 0 ? { kind: "due" as const, amount: due } : { kind: "clear" as const, amount: 0 }
|
|
||||||
})
|
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
@@ -103,32 +96,16 @@ export default function Account() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
{/* Billing prompts, emphasized contextually on the billing page. */}
|
|
||||||
<BillingPrompts variant="inline" />
|
|
||||||
|
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<div class="flex items-center justify-between gap-3 mb-4">
|
<div class="flex items-center justify-between gap-3 mb-4">
|
||||||
<h2 class="text-lg font-semibold text-gray-900">Account & Billing</h2>
|
<h2 class="text-lg font-semibold text-gray-900">Payment Methods</h2>
|
||||||
<Show when={billing.tenant()}>
|
<Show when={billing.tenant()}>
|
||||||
<span class={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${accountStatusStyles[status()]}`}>
|
<span class={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${accountStatusStyles[status()]}`}>
|
||||||
{status()}
|
Your account is {status()}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current balance (relocated from the old Recurring Billing section, logic unchanged) */}
|
|
||||||
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
|
|
||||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Current balance</p>
|
|
||||||
<Show
|
|
||||||
when={balance().kind === "due"}
|
|
||||||
fallback={<p class="text-sm text-gray-600 mt-1">You're all paid up.</p>}
|
|
||||||
>
|
|
||||||
<p class="text-2xl font-bold text-gray-900 mt-0.5">${(balance().amount / 100).toFixed(2)} <span class="text-sm font-normal text-gray-500">due</span></p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Billing methods */}
|
|
||||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Billing methods</p>
|
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{/* Lightning / NWC row — CTA opens the NWC modal */}
|
{/* Lightning / NWC row — CTA opens the NWC modal */}
|
||||||
<li class="rounded-lg border border-gray-200 p-4">
|
<li class="rounded-lg border border-gray-200 p-4">
|
||||||
@@ -181,7 +158,7 @@ export default function Account() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Invoice History</h2>
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">Payment History</h2>
|
||||||
<Show when={invoicesLoading()}>
|
<Show when={invoicesLoading()}>
|
||||||
<LoadingState message="Loading invoices..." paddingClass="py-8" />
|
<LoadingState message="Loading invoices..." paddingClass="py-8" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user