Compare commits

..

3 Commits

Author SHA1 Message Date
userAdityaa 0558408388 feat: encourage payment setup for paid relays without making it required 2026-04-21 02:12:19 +05:45
userAdityaa d209353abd docs: document delinquent relay status across spec (#35)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-20 18:14:14 +00:00
userAdityaa 08c9a2920b feat: display relay provisioning errors in UI (#39)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-20 18:08:47 +00:00
5 changed files with 67 additions and 10 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ Notes:
- Serves `POST /relays/:id/deactivate` - Serves `POST /relays/:id/deactivate`
- Authorizes admin or relay owner - Authorizes admin or relay owner
- If relay is already inactive, return a `400` with `code=relay-is-inactive` - If relay status is `inactive` or `delinquent`, return a `400` with `code=relay-is-inactive`
- Call `command.deactivate_relay` - Call `command.deactivate_relay`
- Return `data` is empty - Return `data` is empty
+4 -4
View File
@@ -28,7 +28,7 @@ Manages the Stripe subscription and subscription items for a relay's tenant. Onl
- 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`**: 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 `active` and on a paid plan**: - **If relay is `active` and on a paid plan**:
- **Ensure subscription exists**: If the tenant has no `stripe_subscription_id`, create a Stripe subscription for the customer with `collection_method: "charge_automatically"` and the relay's price as the first item. Save the subscription ID via `command.set_tenant_subscription` and the item ID via `command.set_relay_subscription_item`. Return early. - **Ensure subscription exists**: If the tenant has no `stripe_subscription_id`, create a Stripe subscription for the customer with `collection_method: "charge_automatically"` and the relay's price as the first item. Save the subscription ID via `command.set_tenant_subscription` and the item ID via `command.set_relay_subscription_item`. Return early.
- **Sync the subscription item**: If the tenant already has a subscription, create or update the relay's Stripe subscription item to the plan's `stripe_price_id` via the Stripe API, then call `command.set_relay_subscription_item`. - **Sync the subscription item**: If the tenant already has a subscription, create or update the relay's Stripe subscription item to the plan's `stripe_price_id` via the Stripe API, then call `command.set_relay_subscription_item`.
@@ -85,7 +85,7 @@ Skip invoices with `amount_due` of 0.
- Look up tenant by `stripe_customer_id` - Look up tenant by `stripe_customer_id`
- If tenant has `past_due_at` set: - If tenant has `past_due_at` set:
- Clear `past_due_at` via `command.clear_tenant_past_due` - Clear `past_due_at` via `command.clear_tenant_past_due`
- Find all `inactive` relays for the tenant that were deactivated due to non-payment (i.e. relays on paid plans that are inactive) - Find all `delinquent` relays on paid plans for the tenant (relays marked delinquent by the billing system due to non-payment)
- Reactivate each one via `command.activate_relay` - Reactivate each one via `command.activate_relay`
## `fn handle_invoice_payment_failed(&self, invoice: &Invoice)` ## `fn handle_invoice_payment_failed(&self, invoice: &Invoice)`
@@ -98,7 +98,7 @@ Skip invoices with `amount_due` of 0.
## `fn handle_invoice_overdue(&self, invoice: &Invoice)` ## `fn handle_invoice_overdue(&self, invoice: &Invoice)`
- Look up tenant by `stripe_customer_id` - Look up tenant by `stripe_customer_id`
- Deactivate all active relays on paid plans via `command.deactivate_relay` - Mark all active relays on paid plans as delinquent via `command.mark_relay_delinquent` (sets status to `delinquent`, distinct from user-initiated `deactivate_relay`)
- Send a DM via `robot.send_dm` notifying the tenant that their paid relays have been deactivated due to non-payment - Send a DM via `robot.send_dm` notifying the tenant that their paid relays have been deactivated due to non-payment
## `fn handle_subscription_updated(&self, subscription: &Subscription)` ## `fn handle_subscription_updated(&self, subscription: &Subscription)`
@@ -106,7 +106,7 @@ Skip invoices with `amount_due` of 0.
- Look up tenant by `stripe_customer_id` - Look up tenant by `stripe_customer_id`
- If subscription status is `canceled` or `unpaid`: - If subscription status is `canceled` or `unpaid`:
- Clear `stripe_subscription_id` via `command.clear_tenant_subscription` - Clear `stripe_subscription_id` via `command.clear_tenant_subscription`
- Deactivate all active paid relays for the tenant via `command.deactivate_relay` - Mark all active paid relays as delinquent via `command.mark_relay_delinquent`
## `fn handle_subscription_deleted(&self, subscription: &Subscription)` ## `fn handle_subscription_deleted(&self, subscription: &Subscription)`
+8
View File
@@ -44,6 +44,14 @@ Notes:
- Sets relay status to `inactive` - Sets relay status to `inactive`
- Logs activity as `(deactivate_relay, relay_id)` - Logs activity as `(deactivate_relay, relay_id)`
- Used for user/admin-initiated deactivation only
## `pub fn mark_relay_delinquent(&self, relay: &Relay) -> Result<()>`
- Sets relay status to `delinquent`
- Logs activity as `(deactivate_relay, relay_id)`
- Used exclusively by the billing system when a relay's subscription becomes past due
- `delinquent` relays are automatically reactivated via `activate_relay` when payment is received
## `pub fn activate_relay(&self, relay: &Relay) -> Result<()>` ## `pub fn activate_relay(&self, relay: &Relay) -> Result<()>`
+1 -1
View File
@@ -69,7 +69,7 @@ A relay is a nostr relay owned by a `tenant` and hosted by the attached zooid in
- `subdomain` - the relay's subdomain - `subdomain` - the relay's subdomain
- `plan` - the relay's plan - `plan` - the relay's plan
- `stripe_subscription_item_id` (nullable) - the Stripe subscription item id. Only set for relays on paid plans. - `stripe_subscription_item_id` (nullable) - the Stripe subscription item id. Only set for relays on paid plans.
- `status` - `active|inactive`. Only `active` relays count toward billing. - `status` - one of `active|inactive|delinquent`. Only `active` relays count toward billing. `delinquent` is set by the billing system when a relay's subscription becomes past due; `inactive` is set when a user or admin manually deactivates a relay.
- `synced` - whether the relay has been successfully synced to zooid at least once. - `synced` - whether the relay has been successfully synced to zooid at least once.
- `sync_error` - a string indicating any errors encountered when synchronizing. - `sync_error` - a string indicating any errors encountered when synchronizing.
- `info_name` - the relay's name - `info_name` - the relay's name
+53 -4
View File
@@ -1,5 +1,5 @@
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js" import { createMemo, createResource, createSignal, 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 PaymentSetup from "@/components/PaymentSetup" import PaymentSetup from "@/components/PaymentSetup"
@@ -7,8 +7,9 @@ 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 { getRelayMembers, useRelay, useRelayActivity } from "@/lib/hooks" import { getRelayMembers, useRelay, useRelayActivity, useTenant } 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,6 +24,21 @@ export default function RelayDetail() {
const [activity] = useRelayActivity(relayId) const [activity] = useRelayActivity(relayId)
const { busy, handleDeactivate, handleReactivate, handleUpdatePlan, needsPaymentSetup, clearNeedsPaymentSetup, 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 [paymentBannerDismissed, setPaymentBannerDismissed] = createSignal(false)
const showPaymentNudge = createMemo(() => {
if (paymentBannerDismissed()) return false
const r = relay()
if (!r) return false
const plan = plans().find(p => p.id === r.plan)
if (!plan || plan.amount === 0) return false
const t = tenant()
if (!t) return false
return !t.nwc_url && !t.stripe_subscription_id
})
return ( return (
<PageContainer> <PageContainer>
<BackLink href="/relays" label="Relays" /> <BackLink href="/relays" label="Relays" />
@@ -30,6 +46,35 @@ 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 method recommended</p>
<p class="text-sm text-amber-700 mt-1">
This relay is on a paid plan. Invoices are due at end of the billing period, you can pay manually or set up automatic payments now.
</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<button
type="button"
onClick={() => setPaymentSetupOpen(true)}
class="text-sm font-medium text-amber-800 underline hover:text-amber-900 whitespace-nowrap"
>
Set up payment
</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}
@@ -46,8 +91,12 @@ export default function RelayDetail() {
)} )}
</Show> </Show>
<PaymentSetup <PaymentSetup
open={needsPaymentSetup()} open={needsPaymentSetup() || paymentSetupOpen()}
onClose={clearNeedsPaymentSetup} onClose={() => {
clearNeedsPaymentSetup()
setPaymentSetupOpen(false)
void refetchTenant()
}}
/> />
</PageContainer> </PageContainer>
) )