Rework billing

This commit is contained in:
Jon Staab
2026-04-07 14:40:48 -07:00
parent 65dfcaeb6c
commit 0980523a50
33 changed files with 1589 additions and 318 deletions
+28 -31
View File
@@ -11,8 +11,8 @@ type ApiOk<T> = {
code: string
}
type BillingInput = {
nwc_url: string
type UpdateTenantInput = {
nwc_url?: string
}
type AuthCache = {
@@ -34,7 +34,8 @@ export class ApiError extends Error {
export type Plan = {
id: string
name: string
sats: number
amount: number
stripe_price_id: string | null
members: number | null
blossom: boolean
livekit: boolean
@@ -50,6 +51,8 @@ export type Relay = {
plan: PlanId
status: string
sync_error: string
stripe_subscription_item_id: string | null
synced: number
info_name: string
info_icon: string
info_description: string
@@ -97,28 +100,18 @@ export type Tenant = {
pubkey: string
nwc_url: string
created_at: number
billing_anchor: number
}
export type InvoiceItem = {
id: string
invoice: string
relay: string
sats: number
stripe_customer_id: string
stripe_subscription_id: string | null
past_due_at: number | null
nwc_error: string | null
}
export type Invoice = {
id: string
tenant: string
status: string
items: InvoiceItem[]
created_at: number
attempted_at: number
error: string
closed_at: number
sent_at: number
paid_at: number
bolt11: string
amount_due: number
currency: string
hosted_invoice_url: string
period_start: number
period_end: number
}
@@ -222,10 +215,6 @@ export function getTenant(pubkey: string) {
return callApi<undefined, Tenant>("GET", `/tenants/${pubkey}`)
}
export function createTenant() {
return callApi<undefined, Tenant>("POST", "/tenants")
}
export function listTenantRelays(pubkey: string) {
return callApi<undefined, Relay[]>("GET", `/tenants/${pubkey}/relays`)
}
@@ -234,8 +223,8 @@ export function listTenantInvoices(pubkey: string) {
return callApi<undefined, Invoice[]>("GET", `/tenants/${pubkey}/invoices`)
}
export function updateTenantBilling(pubkey: string, billing: BillingInput) {
return callApi<BillingInput, BillingInput>("PUT", `/tenants/${pubkey}/billing`, billing)
export function updateTenant(pubkey: string, input: UpdateTenantInput) {
return callApi<UpdateTenantInput, Tenant>("PUT", `/tenants/${pubkey}`, input)
}
export function listRelays() {
@@ -247,7 +236,19 @@ export function getRelay(id: string) {
}
export function listRelayActivity(id: string) {
return callApi<undefined, Activity[]>("GET", `/relays/${id}/activity`)
return callApi<undefined, { activity: Activity[] }>("GET", `/relays/${id}/activity`)
}
export function reactivateRelay(id: string) {
return callApi<undefined, void>("POST", `/relays/${id}/reactivate`)
}
export function createPortalSession(pubkey: string) {
return callApi<undefined, { url: string }>("GET", `/tenants/${pubkey}/stripe/session`)
}
export function getInvoiceBolt11(invoiceId: string) {
return callApi<undefined, { bolt11: string }>("GET", `/invoices/${invoiceId}/bolt11`)
}
export function createRelay(input: CreateRelayInput) {
@@ -262,10 +263,6 @@ export function deactivateRelay(id: string) {
return callApi<undefined, void>("POST", `/relays/${id}/deactivate`)
}
export function listInvoices() {
return callApi<undefined, Invoice[]>("GET", "/invoices")
}
export function getInvoice(id: string) {
return callApi<undefined, Invoice>("GET", `/invoices/${id}`)
}
+13 -11
View File
@@ -7,18 +7,17 @@ import { map, of } from "rxjs"
import {
createRelay,
deactivateRelay,
reactivateRelay,
getRelay,
getTenant,
listRelayActivity,
listRelays,
listTenantInvoices,
listTenantRelays,
listTenants,
updateRelay,
updateTenantBilling,
updateTenant,
type Activity,
type CreateRelayInput,
type Invoice,
type Relay,
type Tenant,
type UpdateRelayInput,
@@ -87,11 +86,12 @@ export const useTenant = () => createResource(() => getTenant(account()!.pubkey)
export const useTenantRelays = () => createResource(() => listTenantRelays(account()!.pubkey))
export const useTenantInvoices = () => createResource(() => listTenantInvoices(account()!.pubkey))
export const useRelay = (relayId: () => string) => createResource(relayId, getRelay)
export const useRelayActivity = (relayId: () => string) => createResource(relayId, listRelayActivity)
export const useRelayActivity = (relayId: () => string) => createResource(relayId, async (id) => {
const result = await listRelayActivity(id)
return result.activity
})
export const useAdminTenants = () => createResource(listTenants)
@@ -122,7 +122,7 @@ export const createRelayForActiveTenant = (input: CreateRelayInput) => {
return createRelay({...defaults, ...input, ...overrides})
}
export const updateActiveTenantBilling = (nwc_url: string) => updateTenantBilling(account()!.pubkey, { nwc_url })
export const updateActiveTenant = (input: { nwc_url?: string }) => updateTenant(account()!.pubkey, input)
export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input)
@@ -130,9 +130,11 @@ export const updateRelayPlanById = (id: string, plan: string) => updateRelay(id,
export const deactivateRelayById = (id: string) => deactivateRelay(id)
export async function checkPendingInvoice(): Promise<Invoice | undefined> {
const invoices = await listTenantInvoices(account()!.pubkey)
return invoices.find(inv => inv.status === "pending")
export const reactivateRelayById = (id: string) => reactivateRelay(id)
export async function tenantNeedsPaymentSetup(): Promise<boolean> {
const tenant = await getTenant(account()!.pubkey)
return !tenant.nwc_url && !tenant.stripe_subscription_id
}
export async function getRelayMembers(url: string) {
@@ -145,4 +147,4 @@ export async function getRelayMembers(url: string) {
}
}
export type { Activity, Invoice, Relay, Tenant }
export type { Activity, Relay, Tenant }
+18 -5
View File
@@ -1,5 +1,5 @@
import { createSignal } from "solid-js"
import { updateRelayById, deactivateRelayById, checkPendingInvoice, type Invoice, type Relay } from "@/lib/hooks"
import { updateRelayById, deactivateRelayById, reactivateRelayById, tenantNeedsPaymentSetup, type Relay } from "@/lib/hooks"
import { setToastMessage } from "@/components/Toast"
import type { PlanId } from "@/lib/api"
@@ -30,7 +30,7 @@ export default function useRelayToggles(
{ refetch, mutate }: RelayActions,
) {
const [busy, setBusy] = createSignal(false)
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
const [needsPaymentSetup, setNeedsPaymentSetup] = createSignal(false)
async function updateRelay(next: Relay, previous: Relay) {
mutate(next)
@@ -63,6 +63,19 @@ export default function useRelayToggles(
}
}
async function handleReactivate() {
if (busy()) return
setBusy(true)
try {
await reactivateRelayById(relayId())
await refetch()
} catch (e) {
setToastMessage(e instanceof Error ? e.message : "Failed to reactivate relay")
} finally {
setBusy(false)
}
}
async function handleUpdatePlan(plan: PlanId) {
const current = relay()
if (!current) return
@@ -88,8 +101,8 @@ export default function useRelayToggles(
}
if (plan !== "free") {
const invoice = await checkPendingInvoice()
if (invoice) setPendingInvoice(invoice)
const needs = await tenantNeedsPaymentSetup()
if (needs) setNeedsPaymentSetup(true)
}
}
@@ -103,5 +116,5 @@ export default function useRelayToggles(
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan !== "free"),
}
return { busy, handleDeactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), toggles }
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, needsPaymentSetup, clearNeedsPaymentSetup: () => setNeedsPaymentSetup(false), toggles }
}