Fix small bugs
This commit is contained in:
@@ -15,12 +15,14 @@ const invoiceStatusStyles: Record<string, string> = {
|
|||||||
type AdminInvoiceListItemProps = {
|
type AdminInvoiceListItemProps = {
|
||||||
invoice: Invoice
|
invoice: Invoice
|
||||||
href: string
|
href: string
|
||||||
|
showTenant?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminInvoiceListItem(props: AdminInvoiceListItemProps) {
|
export default function AdminInvoiceListItem(props: AdminInvoiceListItemProps) {
|
||||||
// Resolve the owning tenant's profile from the event store. AdminInvoiceList
|
// Resolve the owning tenant's profile from the event store. The list that
|
||||||
// primes these profiles in one batch, so this subscription does not prime.
|
// passes `showTenant` is responsible for priming these profiles in one batch,
|
||||||
const metadata = useProfileMetadata(() => props.invoice.tenant_pubkey, { prime: false })
|
// so this subscription does not prime on its own.
|
||||||
|
const metadata = useProfileMetadata(() => (props.showTenant ? props.invoice.tenant_pubkey : undefined), { prime: false })
|
||||||
|
|
||||||
const status = () => invoiceStatus(props.invoice)
|
const status = () => invoiceStatus(props.invoice)
|
||||||
|
|
||||||
@@ -31,19 +33,21 @@ export default function AdminInvoiceListItem(props: AdminInvoiceListItemProps) {
|
|||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="font-medium text-gray-900">{formatUsd(props.invoice.amount)}</p>
|
<p class="font-medium text-gray-900">{formatUsd(props.invoice.amount)}</p>
|
||||||
<p class="text-xs text-gray-500">{formatPeriod(props.invoice.period_start, props.invoice.period_end)}</p>
|
<p class="text-xs text-gray-500">{formatPeriod(props.invoice.period_start, props.invoice.period_end)}</p>
|
||||||
<div class="mt-1.5 flex items-center gap-2">
|
<Show when={props.showTenant}>
|
||||||
<Show
|
<div class="mt-1.5 flex items-center gap-2">
|
||||||
when={getProfilePicture(metadata())}
|
<Show
|
||||||
fallback={
|
when={getProfilePicture(metadata())}
|
||||||
<div class="h-5 w-5 flex-shrink-0 rounded-full bg-gray-200 flex items-center justify-center text-[10px] text-gray-500">
|
fallback={
|
||||||
{((metadata()?.name || metadata()?.display_name) || props.invoice.tenant_pubkey).slice(0, 1).toUpperCase()}
|
<div class="h-5 w-5 flex-shrink-0 rounded-full bg-gray-200 flex items-center justify-center text-[10px] text-gray-500">
|
||||||
</div>
|
{((metadata()?.name || metadata()?.display_name) || props.invoice.tenant_pubkey).slice(0, 1).toUpperCase()}
|
||||||
}
|
</div>
|
||||||
>
|
}
|
||||||
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
>
|
||||||
</Show>
|
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
||||||
<span class="text-xs text-gray-500 truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(props.invoice.tenant_pubkey)}</span>
|
</Show>
|
||||||
</div>
|
<span class="text-xs text-gray-500 truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(props.invoice.tenant_pubkey)}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<span class={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${invoiceStatusStyles[status()] ?? "bg-gray-100 text-gray-500 border-gray-200"}`}>
|
<span class={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${invoiceStatusStyles[status()] ?? "bg-gray-100 text-gray-500 border-gray-200"}`}>
|
||||||
{status()}
|
{status()}
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
import { A } from "@solidjs/router"
|
import { A } from "@solidjs/router"
|
||||||
|
import { Show } from "solid-js"
|
||||||
|
|
||||||
type BackLinkProps = {
|
type BackLinkProps = {
|
||||||
href: string
|
// Omit to pop the previous history entry instead of navigating to a fixed
|
||||||
|
// route — for pages reachable from several places, "back" should land the
|
||||||
|
// user wherever they came from.
|
||||||
|
href?: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BackLink(props: BackLinkProps) {
|
export default function BackLink(props: BackLinkProps) {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-2 mb-6">
|
<div class="flex items-center gap-2 mb-6">
|
||||||
<A href={props.href} class="text-gray-500 hover:text-gray-700">← {props.label}</A>
|
<Show
|
||||||
|
when={props.href}
|
||||||
|
fallback={
|
||||||
|
<button type="button" onClick={() => history.back()} class="text-gray-500 hover:text-gray-700">
|
||||||
|
← {props.label}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(href) => <A href={href()} class="text-gray-500 hover:text-gray-700">← {props.label}</A>}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
type UpdateRelayInput,
|
type UpdateRelayInput,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { autopayConfigured } from "@/lib/paymentMethod"
|
import { autopayConfigured } from "@/lib/paymentMethod"
|
||||||
|
import { decidePostPaidFlow, type PaidFlowDecision } from "@/lib/relayPlanFlow"
|
||||||
import { account, eventStore, pool } from "@/lib/state"
|
import { account, eventStore, pool } from "@/lib/state"
|
||||||
import { useNostr } from "@/lib/nostr"
|
import { useNostr } from "@/lib/nostr"
|
||||||
|
|
||||||
@@ -155,6 +156,8 @@ export const useAdminTenant = (pubkey: () => string) => createResource(pubkey, g
|
|||||||
|
|
||||||
export const useAdminTenantRelays = (pubkey: () => string) => createResource(pubkey, listTenantRelays)
|
export const useAdminTenantRelays = (pubkey: () => string) => createResource(pubkey, listTenantRelays)
|
||||||
|
|
||||||
|
export const useAdminTenantInvoices = (pubkey: () => string) => createResource(pubkey, listTenantInvoices)
|
||||||
|
|
||||||
export const createRelayForActiveTenant = (input: CreateRelayInput) => {
|
export const createRelayForActiveTenant = (input: CreateRelayInput) => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
info_name: "",
|
info_name: "",
|
||||||
@@ -199,5 +202,16 @@ export async function getLatestOpenInvoice(): Promise<Invoice | null> {
|
|||||||
return open[0] ?? null
|
return open[0] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve what to do after a paid create/upgrade succeeds: a tenant that already
|
||||||
|
// has autopay configured just navigates, otherwise we surface the freshly
|
||||||
|
// materialized open invoice to pay directly (or fall back to payment setup when
|
||||||
|
// none is available). Shared by RelayNew, Home's signup-and-create path, and the
|
||||||
|
// plan-upgrade toggle so the post-paid ladder stays identical across all three.
|
||||||
|
export async function resolvePostPaidFlow(): Promise<PaidFlowDecision> {
|
||||||
|
const needsSetup = await tenantNeedsPaymentSetup()
|
||||||
|
const invoice = needsSetup ? await getLatestOpenInvoice() : null
|
||||||
|
return decidePostPaidFlow({ needsSetup, invoice })
|
||||||
|
}
|
||||||
|
|
||||||
export type { Activity, Invoice, Relay, Tenant }
|
export type { Activity, Invoice, Relay, Tenant }
|
||||||
export type { ProfileContent }
|
export type { ProfileContent }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { updateRelayById, deactivateRelayById, reactivateRelayById, getLatestOpenInvoice, tenantNeedsPaymentSetup, type Relay } from "@/lib/hooks"
|
import { updateRelayById, deactivateRelayById, reactivateRelayById, resolvePostPaidFlow, type Relay } from "@/lib/hooks"
|
||||||
import { setToastMessage } from "@/lib/state"
|
import { setToastMessage } from "@/lib/state"
|
||||||
import { applyPlanToRelay, decidePostPaidFlow, planUpdatePayload, toggleField } from "@/lib/relayPlanFlow"
|
import { applyPlanToRelay, planUpdatePayload, toggleField } from "@/lib/relayPlanFlow"
|
||||||
import type { Invoice, PlanId } from "@/lib/api"
|
import type { Invoice, PlanId } from "@/lib/api"
|
||||||
|
|
||||||
type RelayResource = {
|
type RelayResource = {
|
||||||
@@ -85,12 +85,10 @@ export default function useRelayToggles(
|
|||||||
|
|
||||||
if (plan_id === "free") return
|
if (plan_id === "free") return
|
||||||
|
|
||||||
// Materialize the invoice for this upgrade (no collection, no DM) so we can
|
// Paid upgrades materialize an open invoice; resolvePostPaidFlow reconciles
|
||||||
// prompt the tenant to pay it directly. listTenantInvoices reconciles first,
|
// and decides whether to prompt the tenant to pay it, set up a payment method,
|
||||||
// so a just-created invoice is visible here.
|
// or do nothing when autopay is already configured.
|
||||||
const needsSetup = await tenantNeedsPaymentSetup()
|
const decision = await resolvePostPaidFlow()
|
||||||
const invoice = needsSetup ? await getLatestOpenInvoice() : null
|
|
||||||
const decision = decidePostPaidFlow({ needsSetup, invoice })
|
|
||||||
switch (decision.kind) {
|
switch (decision.kind) {
|
||||||
case "pay_invoice":
|
case "pay_invoice":
|
||||||
setPendingInvoice(decision.invoice)
|
setPendingInvoice(decision.invoice)
|
||||||
|
|||||||
+72
-10
@@ -5,9 +5,12 @@ import ExternalLinkIcon from "@/components/ExternalLinkIcon"
|
|||||||
import PricingTable from "@/components/PricingTable"
|
import PricingTable from "@/components/PricingTable"
|
||||||
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
|
import PaymentDialog from "@/components/PaymentDialog"
|
||||||
|
import PaymentSetup from "@/components/PaymentSetup"
|
||||||
import Login from "@/views/Login"
|
import Login from "@/views/Login"
|
||||||
import { createRelayForActiveTenant } from "@/lib/hooks"
|
import { createRelayForActiveTenant, resolvePostPaidFlow } from "@/lib/hooks"
|
||||||
import { account } from "@/lib/state"
|
import type { Invoice } from "@/lib/api"
|
||||||
|
import { account, refetchBilling, setToastMessage } from "@/lib/state"
|
||||||
import FlotillaLogo from "@/assets/flotilla-logo.svg"
|
import FlotillaLogo from "@/assets/flotilla-logo.svg"
|
||||||
import NostordLogo from "@/assets/nostord-logo.svg"
|
import NostordLogo from "@/assets/nostord-logo.svg"
|
||||||
|
|
||||||
@@ -17,6 +20,9 @@ export default function Home() {
|
|||||||
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
const [showLoginModal, setShowLoginModal] = createSignal(false)
|
||||||
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
|
||||||
const [initialPlanId, setInitialPlanId] = createSignal<RelayFormValues["plan_id"]>("free")
|
const [initialPlanId, setInitialPlanId] = createSignal<RelayFormValues["plan_id"]>("free")
|
||||||
|
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
|
||||||
|
const [paymentSetupOpen, setPaymentSetupOpen] = createSignal(false)
|
||||||
|
let createdRelayId = ""
|
||||||
|
|
||||||
function openRelayModal(planId: RelayFormValues["plan_id"] = "free") {
|
function openRelayModal(planId: RelayFormValues["plan_id"] = "free") {
|
||||||
setInitialPlanId(planId)
|
setInitialPlanId(planId)
|
||||||
@@ -24,25 +30,67 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onRelayFormSubmit(values: RelayFormValues) {
|
async function onRelayFormSubmit(values: RelayFormValues) {
|
||||||
if (account()) {
|
// Not signed in yet: stash the draft and send them through login. The relay
|
||||||
const relay = await createRelayForActiveTenant(values)
|
// (and any payment prompt) is created in onAuthenticated once the session and
|
||||||
|
// its tenant exist, so signing up and creating a paid relay in one go still
|
||||||
navigate(`/relays/${relay.id}`)
|
// surfaces the invoice.
|
||||||
} else {
|
if (!account()) {
|
||||||
setDraftRelay(values)
|
setDraftRelay(values)
|
||||||
setShowRelayModal(false)
|
setShowRelayModal(false)
|
||||||
setShowLoginModal(true)
|
setShowLoginModal(true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relay = await createRelayForActiveTenant(values)
|
||||||
|
createdRelayId = relay.id
|
||||||
|
setShowRelayModal(false)
|
||||||
|
|
||||||
|
// Paid plans materialize an open invoice on create. A just-signed-up tenant
|
||||||
|
// has no payment method yet, so open the payment dialog here instead of
|
||||||
|
// dropping them on the relay page with no prompt (the shared dashboard banner
|
||||||
|
// only catches up once they navigate and its billing reads refetch).
|
||||||
|
if (values.plan_id !== "free") {
|
||||||
|
void refetchBilling()
|
||||||
|
const decision = await resolvePostPaidFlow()
|
||||||
|
if (decision.kind === "pay_invoice") {
|
||||||
|
setPendingInvoice(decision.invoice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (decision.kind === "setup") {
|
||||||
|
setPaymentSetupOpen(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`/relays/${relay.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAuthenticated() {
|
async function onAuthenticated() {
|
||||||
|
setShowLoginModal(false)
|
||||||
const relay = draftRelay()
|
const relay = draftRelay()
|
||||||
|
setDraftRelay(undefined)
|
||||||
|
|
||||||
if (relay) {
|
if (!relay) {
|
||||||
onRelayFormSubmit(relay)
|
|
||||||
} else {
|
|
||||||
navigate("/relays")
|
navigate("/relays")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onRelayFormSubmit(relay)
|
||||||
|
} catch (e) {
|
||||||
|
setToastMessage(e instanceof Error ? e.message : "Failed to create relay")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInvoiceClose() {
|
||||||
|
setPendingInvoice(undefined)
|
||||||
|
setPaymentSetupOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSetupClose() {
|
||||||
|
setPaymentSetupOpen(false)
|
||||||
|
void refetchBilling()
|
||||||
|
navigate(`/relays/${createdRelayId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -384,6 +432,20 @@ export default function Home() {
|
|||||||
onAuthenticated={onAuthenticated}
|
onAuthenticated={onAuthenticated}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Show when={pendingInvoice()}>
|
||||||
|
{(inv) => (
|
||||||
|
<PaymentDialog
|
||||||
|
invoice={inv()}
|
||||||
|
open={true}
|
||||||
|
onClose={handleInvoiceClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<PaymentSetup
|
||||||
|
open={paymentSetupOpen()}
|
||||||
|
onClose={handleSetupClose}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function AdminInvoiceDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<BackLink href="/admin/invoices" label="Invoices" />
|
<BackLink label="Back" />
|
||||||
<ResourceState loading={loading()} error={invoice.error} loadingText="Loading invoice..." errorText="Failed to load invoice." class="mb-4" />
|
<ResourceState loading={loading()} error={invoice.error} loadingText="Loading invoice..." errorText="Failed to load invoice." class="mb-4" />
|
||||||
<Show when={!loading() && invoice()}>
|
<Show when={!loading() && invoice()}>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function AdminInvoiceList() {
|
|||||||
<Show when={filtered().length > 0} fallback={<p class="py-20 text-center text-gray-500">No invoices found.</p>}>
|
<Show when={filtered().length > 0} fallback={<p class="py-20 text-center text-gray-500">No invoices found.</p>}>
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
<For each={filtered()}>
|
<For each={filtered()}>
|
||||||
{(invoice) => <AdminInvoiceListItem invoice={invoice} href={`/admin/invoices/${invoice.id}`} />}
|
{(invoice) => <AdminInvoiceListItem invoice={invoice} href={`/admin/invoices/${invoice.id}`} showTenant />}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function AdminRelayDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<BackLink href="/admin/relays" label="Relays" />
|
<BackLink label="Back" />
|
||||||
<ResourceState loading={loading()} error={relay.error} loadingText="Loading relay..." errorText="Failed to load relay." class="mb-4" />
|
<ResourceState loading={loading()} error={relay.error} loadingText="Loading relay..." errorText="Failed to load relay." class="mb-4" />
|
||||||
<Show when={!loading() && relay()}>
|
<Show when={!loading() && relay()}>
|
||||||
{(r) => (
|
{(r) => (
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { getProfilePicture } from "applesauce-core/helpers/profile"
|
|||||||
import BackLink from "@/components/BackLink"
|
import BackLink from "@/components/BackLink"
|
||||||
import PageContainer from "@/components/PageContainer"
|
import PageContainer from "@/components/PageContainer"
|
||||||
import RelayListItem from "@/components/RelayListItem"
|
import RelayListItem from "@/components/RelayListItem"
|
||||||
|
import AdminInvoiceListItem from "@/components/AdminInvoiceListItem"
|
||||||
import ResourceState from "@/components/ResourceState"
|
import ResourceState from "@/components/ResourceState"
|
||||||
import useMinLoading from "@/lib/useMinLoading"
|
import useMinLoading from "@/lib/useMinLoading"
|
||||||
import { useAdminTenant, useAdminTenantRelays, useProfileMetadata } from "@/lib/hooks"
|
import { useAdminTenant, useAdminTenantInvoices, useAdminTenantRelays, useProfileMetadata } from "@/lib/hooks"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
import { shortenPubkey } from "@/lib/pubkey"
|
||||||
|
|
||||||
export default function AdminTenantDetail() {
|
export default function AdminTenantDetail() {
|
||||||
@@ -14,7 +15,8 @@ export default function AdminTenantDetail() {
|
|||||||
const tenantId = () => params.id ?? ""
|
const tenantId = () => params.id ?? ""
|
||||||
const [tenant] = useAdminTenant(tenantId)
|
const [tenant] = useAdminTenant(tenantId)
|
||||||
const [relays] = useAdminTenantRelays(tenantId)
|
const [relays] = useAdminTenantRelays(tenantId)
|
||||||
const loading = useMinLoading(() => tenant.loading || relays.loading)
|
const [invoices] = useAdminTenantInvoices(tenantId)
|
||||||
|
const loading = useMinLoading(() => tenant.loading || relays.loading || invoices.loading)
|
||||||
const metadata = useProfileMetadata(tenantId)
|
const metadata = useProfileMetadata(tenantId)
|
||||||
|
|
||||||
const churnedLabel = () => {
|
const churnedLabel = () => {
|
||||||
@@ -25,7 +27,7 @@ export default function AdminTenantDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<BackLink href="/admin/tenants" label="Tenants" />
|
<BackLink label="Back" />
|
||||||
<div class="flex items-center gap-4 mb-6 py-2">
|
<div class="flex items-center gap-4 mb-6 py-2">
|
||||||
<Show
|
<Show
|
||||||
when={getProfilePicture(metadata())}
|
when={getProfilePicture(metadata())}
|
||||||
@@ -42,7 +44,7 @@ export default function AdminTenantDetail() {
|
|||||||
<p class="text-xs text-gray-500 break-all">{tenantId()}</p>
|
<p class="text-xs text-gray-500 break-all">{tenantId()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResourceState loading={loading()} error={tenant.error || relays.error} loadingText="Loading tenant..." errorText="Failed to load tenant." class="mb-4" />
|
<ResourceState loading={loading()} error={tenant.error || relays.error || invoices.error} loadingText="Loading tenant..." errorText="Failed to load tenant." class="mb-4" />
|
||||||
<Show when={!loading()}>
|
<Show when={!loading()}>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
@@ -96,6 +98,16 @@ export default function AdminTenantDetail() {
|
|||||||
</ul>
|
</ul>
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">Invoices</h2>
|
||||||
|
<Show when={(invoices()?.length ?? 0) > 0} fallback={<p class="text-gray-500">No invoices.</p>}>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<For each={invoices() ?? []}>
|
||||||
|
{(invoice) => <AdminInvoiceListItem invoice={invoice} href={`/admin/invoices/${invoice.id}`} />}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function RelayDetail() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<BackLink href="/relays" label="Relays" />
|
<BackLink label="Back" />
|
||||||
<ResourceState loading={loading()} error={relay.error} loadingText="Loading relay..." errorText="Failed to load relay." class="mb-4" />
|
<ResourceState loading={loading()} error={relay.error} loadingText="Loading relay..." errorText="Failed to load relay." class="mb-4" />
|
||||||
<Show when={!loading() && relay()}>
|
<Show when={!loading() && relay()}>
|
||||||
{(r) => (
|
{(r) => (
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import PageContainer from "@/components/PageContainer"
|
|||||||
import PaymentDialog from "@/components/PaymentDialog"
|
import PaymentDialog from "@/components/PaymentDialog"
|
||||||
import PaymentSetup from "@/components/PaymentSetup"
|
import PaymentSetup from "@/components/PaymentSetup"
|
||||||
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
||||||
import { createRelayForActiveTenant, getLatestOpenInvoice, tenantNeedsPaymentSetup } from "@/lib/hooks"
|
import { createRelayForActiveTenant, resolvePostPaidFlow } from "@/lib/hooks"
|
||||||
import type { Invoice } from "@/lib/api"
|
import type { Invoice } from "@/lib/api"
|
||||||
import { decidePostPaidFlow } from "@/lib/relayPlanFlow"
|
|
||||||
import { refetchBilling, setBillingFlowActive } from "@/lib/state"
|
import { refetchBilling, setBillingFlowActive } from "@/lib/state"
|
||||||
|
|
||||||
export default function RelayNew() {
|
export default function RelayNew() {
|
||||||
@@ -38,12 +37,10 @@ export default function RelayNew() {
|
|||||||
void refetchBilling()
|
void refetchBilling()
|
||||||
|
|
||||||
if (paid) {
|
if (paid) {
|
||||||
// Materialize the invoice for this change (no collection, no DM) so we
|
// Paid plans materialize an open invoice on create; resolvePostPaidFlow
|
||||||
// can prompt the tenant to pay it directly. listTenantInvoices reconciles
|
// reconciles and decides whether to prompt the tenant to pay it, set up a
|
||||||
// first, so a just-created invoice is visible here.
|
// payment method, or just navigate when autopay is already configured.
|
||||||
const needsSetup = await tenantNeedsPaymentSetup()
|
const decision = await resolvePostPaidFlow()
|
||||||
const invoice = needsSetup ? await getLatestOpenInvoice() : null
|
|
||||||
const decision = decidePostPaidFlow({ needsSetup, invoice })
|
|
||||||
switch (decision.kind) {
|
switch (decision.kind) {
|
||||||
case "pay_invoice":
|
case "pay_invoice":
|
||||||
setPendingInvoice(decision.invoice)
|
setPendingInvoice(decision.invoice)
|
||||||
|
|||||||
Reference in New Issue
Block a user