forked from coracle/caravel
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c60139965b |
+1
-14
@@ -26,8 +26,6 @@ Members:
|
|||||||
|
|
||||||
Manages the Stripe subscription and subscription items for a relay's tenant. Only paid (non-free) relays interact with Stripe. Free-only tenants have no subscription. Must be idempotent.
|
Manages the Stripe subscription and subscription items for a relay's tenant. Only paid (non-free) relays interact with Stripe. Free-only tenants have no subscription. Must be idempotent.
|
||||||
|
|
||||||
Stripe uses **pay-in-advance** by default: when a subscription is first created, Stripe immediately generates an open invoice for the current period. The `invoice.created` webhook fires shortly after and `handle_invoice_created` attempts payment.
|
|
||||||
|
|
||||||
- Fetch the relay and tenant associated with the `activity`
|
- Fetch the relay and tenant associated with the `activity`
|
||||||
- **If relay plan is `free`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
- **If relay plan is `free`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
||||||
- **If relay is `inactive` or `delinquent`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
- **If relay is `inactive` or `delinquent`**: if the relay has a `stripe_subscription_item_id`, delete it via the Stripe API and call `command.delete_relay_subscription_item`. Then check cleanup (below). Return early.
|
||||||
@@ -68,20 +66,9 @@ Stripe uses **pay-in-advance** by default: when a subscription is first created,
|
|||||||
- Creates a Stripe Customer Portal session for the given customer
|
- Creates a Stripe Customer Portal session for the given customer
|
||||||
- Returns the portal session URL
|
- Returns the portal session URL
|
||||||
|
|
||||||
## `pub async fn pay_outstanding_nwc_invoices(&self, tenant: &Tenant) -> Result<()>`
|
|
||||||
|
|
||||||
Called when a tenant first sets their NWC URL (via `PUT /tenants/:pubkey`). Attempts to pay any currently open invoices for the tenant using their NWC wallet, so that invoices created before NWC was configured are not left unpaid.
|
|
||||||
|
|
||||||
- If `tenant.nwc_url` is empty, return early.
|
|
||||||
- List all Stripe invoices for `tenant.stripe_customer_id` via `stripe_list_invoices`.
|
|
||||||
- For each invoice with `status == "open"` and `amount_due > 0`:
|
|
||||||
- Attempt NWC payment via `nwc_pay_invoice`.
|
|
||||||
- On success: call `stripe_pay_invoice_out_of_band` and `command.clear_tenant_nwc_error`.
|
|
||||||
- On failure: call `command.set_tenant_nwc_error` and log the error; continue to the next invoice.
|
|
||||||
|
|
||||||
## `fn handle_invoice_created(&self, invoice: &Invoice)`
|
## `fn handle_invoice_created(&self, invoice: &Invoice)`
|
||||||
|
|
||||||
Attempts to pay a new subscription invoice. Because Stripe defaults to pay-in-advance, this webhook fires immediately when a subscription is created (i.e. when a paid relay is added or a plan is upgraded). Payment priority:
|
Attempts to pay a new subscription invoice. Payment priority:
|
||||||
|
|
||||||
1. **NWC auto-pay**: If the tenant has a `nwc_url`:
|
1. **NWC auto-pay**: If the tenant has a `nwc_url`:
|
||||||
- Create a bolt11 Lightning invoice for the invoice amount using `self.nwc_url` (the receiving/system wallet)
|
- Create a bolt11 Lightning invoice for the invoice amount using `self.nwc_url` (the receiving/system wallet)
|
||||||
|
|||||||
+1
-18
@@ -945,29 +945,12 @@ async fn update_tenant(
|
|||||||
state.api.require_admin_or_tenant(&auth, &pubkey)?;
|
state.api.require_admin_or_tenant(&auth, &pubkey)?;
|
||||||
let mut tenant = state.api.get_tenant_or_404(&pubkey).await?;
|
let mut tenant = state.api.get_tenant_or_404(&pubkey).await?;
|
||||||
|
|
||||||
let nwc_previously_empty = tenant.nwc_url.is_empty();
|
|
||||||
if let Some(nwc_url) = payload.nwc_url {
|
if let Some(nwc_url) = payload.nwc_url {
|
||||||
tenant.nwc_url = nwc_url;
|
tenant.nwc_url = nwc_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
match state.api.command.update_tenant(&tenant).await {
|
match state.api.command.update_tenant(&tenant).await {
|
||||||
Ok(()) => {
|
Ok(()) => Ok(ok(StatusCode::OK, tenant)),
|
||||||
// When NWC is first connected, attempt to pay any outstanding open invoices.
|
|
||||||
if nwc_previously_empty && !tenant.nwc_url.is_empty() {
|
|
||||||
let billing = state.api.billing.clone();
|
|
||||||
let tenant_clone = tenant.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = billing.pay_outstanding_nwc_invoices(&tenant_clone).await {
|
|
||||||
tracing::error!(
|
|
||||||
error = %e,
|
|
||||||
pubkey = %tenant_clone.pubkey,
|
|
||||||
"pay_outstanding_nwc_invoices failed after NWC setup"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(ok(StatusCode::OK, tenant))
|
|
||||||
}
|
|
||||||
Err(e) => Ok(err(
|
Err(e) => Ok(err(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"internal",
|
"internal",
|
||||||
|
|||||||
@@ -599,62 +599,6 @@ impl Billing {
|
|||||||
Ok(invoice_response.invoice)
|
Ok(invoice_response.invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pay_outstanding_nwc_invoices(
|
|
||||||
&self,
|
|
||||||
tenant: &crate::models::Tenant,
|
|
||||||
) -> Result<()> {
|
|
||||||
if tenant.nwc_url.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let invoices = self
|
|
||||||
.stripe_list_invoices(&tenant.stripe_customer_id)
|
|
||||||
.await?;
|
|
||||||
let invoices_arr = invoices.as_array().cloned().unwrap_or_default();
|
|
||||||
|
|
||||||
for invoice in &invoices_arr {
|
|
||||||
let status = invoice["status"].as_str().unwrap_or_default();
|
|
||||||
let amount_due = invoice["amount_due"].as_i64().unwrap_or(0);
|
|
||||||
let invoice_id = invoice["id"].as_str().unwrap_or_default();
|
|
||||||
let currency = invoice["currency"].as_str().unwrap_or("usd");
|
|
||||||
|
|
||||||
if status != "open" || amount_due == 0 || invoice_id.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self
|
|
||||||
.nwc_pay_invoice(amount_due, currency, &tenant.nwc_url)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => {
|
|
||||||
if let Err(e) = self.stripe_pay_invoice_out_of_band(invoice_id).await {
|
|
||||||
tracing::error!(
|
|
||||||
error = %e,
|
|
||||||
invoice_id,
|
|
||||||
"failed to mark invoice paid out of band"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let _ = self.command.clear_tenant_nwc_error(&tenant.pubkey).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("{e}");
|
|
||||||
tracing::error!(
|
|
||||||
error = %e,
|
|
||||||
invoice_id,
|
|
||||||
"nwc payment failed for outstanding invoice"
|
|
||||||
);
|
|
||||||
let _ = self
|
|
||||||
.command
|
|
||||||
.set_tenant_nwc_error(&tenant.pubkey, &error_msg)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn stripe_create_portal_session(&self, customer_id: &str) -> Result<String> {
|
pub async fn stripe_create_portal_session(&self, customer_id: &str) -> Result<String> {
|
||||||
let resp = self
|
let resp = self
|
||||||
.http
|
.http
|
||||||
@@ -861,16 +805,6 @@ impl Billing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_btc_spot_price(&self, currency: &str) -> Result<f64> {
|
async fn fetch_btc_spot_price(&self, currency: &str) -> Result<f64> {
|
||||||
// BTC_PRICE_USD_OVERRIDE bypasses the external price API entirely.
|
|
||||||
// Set this to a fixed USD/BTC rate (e.g. "97000") for local testing
|
|
||||||
// when Coinbase is unreachable. Remove or leave unset in production.
|
|
||||||
if let Ok(val) = std::env::var("BTC_PRICE_USD_OVERRIDE") {
|
|
||||||
let price = val
|
|
||||||
.trim()
|
|
||||||
.parse::<f64>()
|
|
||||||
.map_err(|e| anyhow!("invalid BTC_PRICE_USD_OVERRIDE: {e}"))?;
|
|
||||||
return Ok(price);
|
|
||||||
}
|
|
||||||
fetch_btc_spot_price_from_base(&self.http, &self.btc_quote_api_base, currency).await
|
fetch_btc_spot_price_from_base(&self.http, &self.btc_quote_api_base, currency).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
const [payError, setPayError] = createSignal("")
|
const [payError, setPayError] = createSignal("")
|
||||||
const [showSetup, setShowSetup] = createSignal(false)
|
const [showSetup, setShowSetup] = createSignal(false)
|
||||||
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
||||||
const [setupSaved, setSetupSaved] = createSignal(false)
|
|
||||||
|
|
||||||
async function loadBolt11() {
|
async function loadBolt11() {
|
||||||
if (!props.invoice.id) return
|
if (!props.invoice.id) return
|
||||||
@@ -161,15 +160,6 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="text-center pt-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPaymentSetup(true)}
|
|
||||||
class="text-sm text-blue-600 hover:text-blue-700"
|
|
||||||
>
|
|
||||||
Set up payment method instead
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -238,14 +228,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
<PaymentSetup
|
<PaymentSetup
|
||||||
open={showPaymentSetup()}
|
open={showPaymentSetup()}
|
||||||
onClose={() => {
|
onClose={() => setShowPaymentSetup(false)}
|
||||||
setShowPaymentSetup(false)
|
|
||||||
if (setupSaved()) {
|
|
||||||
setSetupSaved(false)
|
|
||||||
props.onClose()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onSaved={() => setSetupSaved(true)}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ type Tab = "nwc" | "card"
|
|||||||
type PaymentSetupProps = {
|
type PaymentSetupProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSaved?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PaymentSetup(props: PaymentSetupProps) {
|
export default function PaymentSetup(props: PaymentSetupProps) {
|
||||||
@@ -28,7 +27,6 @@ export default function PaymentSetup(props: PaymentSetupProps) {
|
|||||||
try {
|
try {
|
||||||
await updateActiveTenant({ nwc_url: url })
|
await updateActiveTenant({ nwc_url: url })
|
||||||
setSaved(true)
|
setSaved(true)
|
||||||
props.onSaved?.()
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "Failed to save wallet connection")
|
setError(e instanceof Error ? e.message : "Failed to save wallet connection")
|
||||||
} finally {
|
} finally {
|
||||||
@@ -66,7 +64,7 @@ export default function PaymentSetup(props: PaymentSetupProps) {
|
|||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-gray-900">Set Up Payments</h2>
|
<h2 class="text-lg font-semibold text-gray-900">Set Up Payments</h2>
|
||||||
<p class="text-sm text-gray-500 mt-1">Choose how you'd like to pay once invoices are issued for your relay.</p>
|
<p class="text-sm text-gray-500 mt-1">Choose how you'd like to pay for your relay.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -146,7 +144,7 @@ export default function PaymentSetup(props: PaymentSetupProps) {
|
|||||||
<line x1="1" y1="10" x2="23" y2="10" />
|
<line x1="1" y1="10" x2="23" y2="10" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-gray-600">Add a payment card via Stripe to enable automatic billing once invoices are issued.</p>
|
<p class="text-sm text-gray-600">Add a payment card via Stripe to enable automatic billing.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openPortal}
|
onClick={openPortal}
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ import {
|
|||||||
getTenant,
|
getTenant,
|
||||||
listRelayActivity,
|
listRelayActivity,
|
||||||
listRelays,
|
listRelays,
|
||||||
listTenantInvoices,
|
|
||||||
listTenantRelays,
|
listTenantRelays,
|
||||||
listTenants,
|
listTenants,
|
||||||
updateRelay,
|
updateRelay,
|
||||||
updateTenant,
|
updateTenant,
|
||||||
type Activity,
|
type Activity,
|
||||||
type CreateRelayInput,
|
type CreateRelayInput,
|
||||||
type Invoice,
|
|
||||||
type Relay,
|
type Relay,
|
||||||
type Tenant,
|
type Tenant,
|
||||||
type UpdateRelayInput,
|
type UpdateRelayInput,
|
||||||
@@ -139,14 +137,6 @@ export async function tenantNeedsPaymentSetup(): Promise<boolean> {
|
|||||||
return !tenant.nwc_url && !tenant.stripe_subscription_id
|
return !tenant.nwc_url && !tenant.stripe_subscription_id
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestOpenInvoice(): Promise<Invoice | null> {
|
|
||||||
const invoices = await listTenantInvoices(account()!.pubkey)
|
|
||||||
const open = invoices
|
|
||||||
.filter(inv => inv.status === "open" && inv.amount_due > 0)
|
|
||||||
.sort((a, b) => b.period_start - a.period_start)
|
|
||||||
return open[0] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRelayMembers(url: string) {
|
export async function getRelayMembers(url: string) {
|
||||||
const management = new RelayManagement(new NostrRelay(url), account()!.signer)
|
const management = new RelayManagement(new NostrRelay(url), account()!.signer)
|
||||||
|
|
||||||
@@ -157,4 +147,4 @@ export async function getRelayMembers(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Activity, Invoice, Relay, Tenant }
|
export type { Activity, Relay, Tenant }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { updateRelayById, deactivateRelayById, reactivateRelayById, getLatestOpenInvoice, type Relay } from "@/lib/hooks"
|
import { updateRelayById, deactivateRelayById, reactivateRelayById, tenantNeedsPaymentSetup, type Relay } from "@/lib/hooks"
|
||||||
import { setToastMessage } from "@/components/Toast"
|
import { setToastMessage } from "@/components/Toast"
|
||||||
import type { Invoice, PlanId } from "@/lib/api"
|
import type { PlanId } from "@/lib/api"
|
||||||
|
|
||||||
function toBool(value: number | undefined, fallback: boolean): boolean {
|
function toBool(value: number | undefined, fallback: boolean): boolean {
|
||||||
if (value === 0) return false
|
if (value === 0) return false
|
||||||
@@ -30,7 +30,7 @@ export default function useRelayToggles(
|
|||||||
{ refetch, mutate }: RelayActions,
|
{ refetch, mutate }: RelayActions,
|
||||||
) {
|
) {
|
||||||
const [busy, setBusy] = createSignal(false)
|
const [busy, setBusy] = createSignal(false)
|
||||||
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
|
const [needsPaymentSetup, setNeedsPaymentSetup] = createSignal(false)
|
||||||
|
|
||||||
async function updateRelay(next: Relay, previous: Relay) {
|
async function updateRelay(next: Relay, previous: Relay) {
|
||||||
mutate(next)
|
mutate(next)
|
||||||
@@ -101,8 +101,8 @@ export default function useRelayToggles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan !== "free") {
|
if (plan !== "free") {
|
||||||
const invoice = await getLatestOpenInvoice()
|
const needs = await tenantNeedsPaymentSetup()
|
||||||
if (invoice) setPendingInvoice(invoice)
|
if (needs) setNeedsPaymentSetup(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,5 +116,5 @@ export default function useRelayToggles(
|
|||||||
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan !== "free"),
|
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan !== "free"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), toggles }
|
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, needsPaymentSetup, clearNeedsPaymentSetup: () => setNeedsPaymentSetup(false), toggles }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { createMemo, createResource, createSignal, Show } from "solid-js"
|
import { createMemo, createResource, Show } from "solid-js"
|
||||||
import BackLink from "@/components/BackLink"
|
import BackLink from "@/components/BackLink"
|
||||||
import PageContainer from "@/components/PageContainer"
|
import PageContainer from "@/components/PageContainer"
|
||||||
import PaymentDialog from "@/components/PaymentDialog"
|
|
||||||
import PaymentSetup from "@/components/PaymentSetup"
|
import PaymentSetup from "@/components/PaymentSetup"
|
||||||
import RelayDetailCard from "@/components/RelayDetailCard"
|
import RelayDetailCard from "@/components/RelayDetailCard"
|
||||||
import ResourceState from "@/components/ResourceState"
|
import ResourceState from "@/components/ResourceState"
|
||||||
import useMinLoading from "@/components/useMinLoading"
|
import useMinLoading from "@/components/useMinLoading"
|
||||||
import ActivityFeed from "@/components/ActivityFeed"
|
import ActivityFeed from "@/components/ActivityFeed"
|
||||||
import { getLatestOpenInvoice, getRelayMembers, useRelay, useRelayActivity, useTenant } from "@/lib/hooks"
|
import { getRelayMembers, useRelay, useRelayActivity } from "@/lib/hooks"
|
||||||
import useRelayToggles from "@/lib/useRelayToggles"
|
import useRelayToggles from "@/lib/useRelayToggles"
|
||||||
import { plans } from "@/lib/state"
|
|
||||||
|
|
||||||
export default function RelayDetail() {
|
export default function RelayDetail() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -23,32 +21,7 @@ export default function RelayDetail() {
|
|||||||
const [members] = createResource(relayUrl, getRelayMembers)
|
const [members] = createResource(relayUrl, getRelayMembers)
|
||||||
const loading = useMinLoading(() => relay.loading && !relay())
|
const loading = useMinLoading(() => relay.loading && !relay())
|
||||||
const [activity] = useRelayActivity(relayId)
|
const [activity] = useRelayActivity(relayId)
|
||||||
const { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice, toggles } = useRelayToggles(relayId, relay, { refetch, mutate })
|
const { busy, handleDeactivate, handleReactivate, handleUpdatePlan, needsPaymentSetup, clearNeedsPaymentSetup, toggles } = useRelayToggles(relayId, relay, { refetch, mutate })
|
||||||
|
|
||||||
const [tenant, { refetch: refetchTenant }] = useTenant()
|
|
||||||
const [paymentSetupOpen, setPaymentSetupOpen] = createSignal(false)
|
|
||||||
const [invoiceDialogOpen, setInvoiceDialogOpen] = createSignal(false)
|
|
||||||
const [paymentBannerDismissed, setPaymentBannerDismissed] = createSignal(false)
|
|
||||||
|
|
||||||
const isPaidRelay = createMemo(() => {
|
|
||||||
const r = relay()
|
|
||||||
if (!r) return false
|
|
||||||
const plan = plans().find(p => p.id === r.plan)
|
|
||||||
return !!(plan && plan.amount > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [openInvoice, { refetch: refetchOpenInvoice }] = createResource(
|
|
||||||
isPaidRelay,
|
|
||||||
async (paid) => paid ? getLatestOpenInvoice() : null
|
|
||||||
)
|
|
||||||
|
|
||||||
const showPaymentNudge = createMemo(() => {
|
|
||||||
if (paymentBannerDismissed()) return false
|
|
||||||
if (!isPaidRelay()) return false
|
|
||||||
const t = tenant()
|
|
||||||
if (!t) return false
|
|
||||||
return !t.nwc_url
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@@ -57,44 +30,6 @@ export default function RelayDetail() {
|
|||||||
<Show when={!loading() && relay()}>
|
<Show when={!loading() && relay()}>
|
||||||
{(r) => (
|
{(r) => (
|
||||||
<div class="space-y-6 mb-6">
|
<div class="space-y-6 mb-6">
|
||||||
<Show when={showPaymentNudge()}>
|
|
||||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 flex items-start justify-between gap-4">
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-sm font-medium text-amber-800">Payment setup recommended</p>
|
|
||||||
<p class="text-sm text-amber-700 mt-1">
|
|
||||||
This relay is on a paid plan. Invoices are due when your subscription starts. Set up NWC or Stripe for automatic payments, or pay open invoices via Lightning.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 shrink-0">
|
|
||||||
<Show when={openInvoice()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setInvoiceDialogOpen(true)}
|
|
||||||
class="text-sm font-medium text-amber-800 underline hover:text-amber-900 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Pay invoice
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setPaymentSetupOpen(true)}
|
|
||||||
class="text-sm font-medium text-amber-800 underline hover:text-amber-900 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
Set up payments
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setPaymentBannerDismissed(true)}
|
|
||||||
aria-label="Dismiss"
|
|
||||||
class="text-amber-500 hover:text-amber-800 shrink-0"
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M18 6L6 18M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<RelayDetailCard
|
<RelayDetailCard
|
||||||
relay={r()}
|
relay={r()}
|
||||||
currentMembers={members.length}
|
currentMembers={members.length}
|
||||||
@@ -110,37 +45,9 @@ export default function RelayDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={pendingInvoice()}>
|
|
||||||
{(inv) => (
|
|
||||||
<PaymentDialog
|
|
||||||
invoice={inv()}
|
|
||||||
open={true}
|
|
||||||
onClose={() => {
|
|
||||||
clearPendingInvoice()
|
|
||||||
void refetchTenant()
|
|
||||||
void refetchOpenInvoice()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<Show when={openInvoice()}>
|
|
||||||
{(inv) => (
|
|
||||||
<PaymentDialog
|
|
||||||
invoice={inv()!}
|
|
||||||
open={invoiceDialogOpen()}
|
|
||||||
onClose={() => {
|
|
||||||
setInvoiceDialogOpen(false)
|
|
||||||
void refetchOpenInvoice()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<PaymentSetup
|
<PaymentSetup
|
||||||
open={paymentSetupOpen()}
|
open={needsPaymentSetup()}
|
||||||
onClose={() => {
|
onClose={clearNeedsPaymentSetup}
|
||||||
setPaymentSetupOpen(false)
|
|
||||||
void refetchTenant()
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { createSignal, Show } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import { useNavigate } from "@solidjs/router"
|
import { useNavigate } from "@solidjs/router"
|
||||||
import BackLink from "@/components/BackLink"
|
import BackLink from "@/components/BackLink"
|
||||||
import PageContainer from "@/components/PageContainer"
|
import PageContainer from "@/components/PageContainer"
|
||||||
import PaymentDialog from "@/components/PaymentDialog"
|
import PaymentSetup from "@/components/PaymentSetup"
|
||||||
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
import RelayForm, { type RelayFormValues } from "@/components/RelayForm"
|
||||||
import { createRelayForActiveTenant, getLatestOpenInvoice } from "@/lib/hooks"
|
import { createRelayForActiveTenant, tenantNeedsPaymentSetup } from "@/lib/hooks"
|
||||||
import type { Invoice } from "@/lib/api"
|
|
||||||
|
|
||||||
export default function RelayNew() {
|
export default function RelayNew() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
|
const [showPaymentSetup, setShowPaymentSetup] = createSignal(false)
|
||||||
let createdRelayId = ""
|
let createdRelayId = ""
|
||||||
|
|
||||||
async function handleSubmit(values: RelayFormValues) {
|
async function handleSubmit(values: RelayFormValues) {
|
||||||
@@ -17,9 +16,9 @@ export default function RelayNew() {
|
|||||||
createdRelayId = relay.id
|
createdRelayId = relay.id
|
||||||
|
|
||||||
if (values.plan !== "free") {
|
if (values.plan !== "free") {
|
||||||
const invoice = await getLatestOpenInvoice()
|
const needs = await tenantNeedsPaymentSetup()
|
||||||
if (invoice) {
|
if (needs) {
|
||||||
setPendingInvoice(invoice)
|
setShowPaymentSetup(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +27,7 @@ export default function RelayNew() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDialogClose() {
|
function handleDialogClose() {
|
||||||
setPendingInvoice(undefined)
|
setShowPaymentSetup(false)
|
||||||
navigate(`/relays/${createdRelayId}`)
|
navigate(`/relays/${createdRelayId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,15 +41,10 @@ export default function RelayNew() {
|
|||||||
submitLabel="Create Relay"
|
submitLabel="Create Relay"
|
||||||
submittingLabel="Creating..."
|
submittingLabel="Creating..."
|
||||||
/>
|
/>
|
||||||
<Show when={pendingInvoice()}>
|
<PaymentSetup
|
||||||
{(inv) => (
|
open={showPaymentSetup()}
|
||||||
<PaymentDialog
|
onClose={handleDialogClose}
|
||||||
invoice={inv()}
|
/>
|
||||||
open={true}
|
|
||||||
onClose={handleDialogClose}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user