Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d066379
tempoarry rmeove usage; was not a great page; move to invoice for non…
royendo Mar 20, 2026
704db52
prettier
royendo Mar 20, 2026
868c5ee
Update +layout.svelte
royendo Mar 20, 2026
6d1d9b7
Merge remote-tracking branch 'origin/main' into pricing-1/fix-org-bil…
royendo Apr 6, 2026
4a7ae2d
Migrate billing plan components to Svelte 5 runes
royendo Apr 6, 2026
10c050c
chore: format with prettier
royendo Apr 6, 2026
5de0624
Update CancelledTeamPlan.svelte
royendo Apr 6, 2026
c21121a
Merge branch 'main' into pricing-1/fix-org-billing-page
royendo Apr 9, 2026
b47204d
fisrt pass on new design
royendo Apr 9, 2026
2475972
ne wlook
royendo Apr 13, 2026
685073d
Merge branch 'main' into pricing-1/fix-org-billing-page
royendo Apr 13, 2026
6868db6
first pass
royendo Apr 13, 2026
ca641e0
second pass
royendo Apr 13, 2026
317e597
entperirse nite
royendo Apr 13, 2026
57bf2a9
prettier
royendo Apr 13, 2026
b2830fc
web code qual
royendo Apr 13, 2026
81d8a96
code qual
royendo Apr 13, 2026
11a59a7
Remove unused `subscriptionQuery` to fix ESLint error
royendo Apr 13, 2026
dd5affb
Merge branch 'main' into pricing-1/fix-org-billing-page
royendo Apr 15, 2026
1d0af49
fix duplicate OLAP/AI Connector
royendo Apr 15, 2026
ce5ae8c
new design update
royendo Apr 15, 2026
8416791
nit
royendo Apr 15, 2026
f82cbde
follow up to https://github.com/rilldata/rill/pull/9088
royendo Apr 15, 2026
df0d27c
prettier, code qual
royendo Apr 15, 2026
483de69
asreq
royendo Apr 20, 2026
4b18f9f
prettier
royendo Apr 20, 2026
3356d73
Merge branch 'main' into pricing-2-org-billing-estimate-costs
royendo Apr 21, 2026
2c559fc
as req
royendo Apr 21, 2026
b970dc0
separating for visual clarity
royendo Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/tsc-with-whitelist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ web-admin/src/routes/[organization]/-/settings/+layout.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/billing/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/billing/payment/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/billing/estimate/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/billing/upgrade/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/settings/usage/+page.ts: error TS2307
web-admin/src/routes/[organization]/-/upgrade-callback/+page.ts: error TS2307
Expand Down
121 changes: 83 additions & 38 deletions web-admin/src/features/billing/Payment.svelte
Original file line number Diff line number Diff line change
@@ -1,67 +1,112 @@
<script lang="ts">
import {
createAdminServiceGetBillingSubscription,
createAdminServiceGetOrganization,
} from "@rilldata/web-admin/client";
import { createAdminServiceGetOrganization } from "@rilldata/web-admin/client";
import {
getPaymentIssueErrorText,
needsPaymentSetup,
} from "@rilldata/web-admin/features/billing/issues/getMessageForPaymentIssues";
import { fetchPaymentsPortalURL } from "@rilldata/web-admin/features/billing/plans/selectors";
import { useCategorisedOrganizationBillingIssues } from "@rilldata/web-admin/features/billing/selectors";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import { Button } from "@rilldata/web-common/components/button";
import CancelCircle from "@rilldata/web-common/components/icons/CancelCircle.svelte";
import { isEnterprisePlan, isManagedPlan } from "./plans/utils";

let { organization }: { organization: string } = $props();

let org = $derived(createAdminServiceGetOrganization(organization));
let subscriptionQuery = $derived(
createAdminServiceGetBillingSubscription(organization),
);
let plan = $derived($subscriptionQuery?.data?.subscription?.plan);
let categorisedIssues = $derived(
useCategorisedOrganizationBillingIssues(organization),
);
let paymentIssues = $derived($categorisedIssues.data?.payment);
let neverSubscribed = $derived(!!$categorisedIssues.data?.neverSubscribed);
let onTrial = $derived(!!$categorisedIssues.data?.trial);
let onManagedPlan = $derived(plan && isManagedPlan(plan.name));
let onEnterprisePlan = $derived(plan && isEnterprisePlan(plan.name));
// For enterprise and managed orgs, hide when payment details haven't been
// entered yet (setup done via CLI). Once set up, show the Manage button.
// neverSubscribed orgs are always hidden since the billing page is not shown.
let pendingSetup = $derived(
neverSubscribed ||
((onManagedPlan || onEnterprisePlan) &&
needsPaymentSetup(paymentIssues ?? [])),
let hasPaymentCustomer = $derived(
!!$org.data?.organization?.paymentCustomerId,
);

async function handleManagePayment() {
let paymentIssues = $derived($categorisedIssues.data?.payment);
async function handleManageCards() {
const setup = paymentIssues?.length
? needsPaymentSetup(paymentIssues)
: false;
window.open(
await fetchPaymentsPortalURL(organization, window.location.href, setup),
"_self",
"_blank",
);
}
</script>

<!-- Presence of paymentCustomerId signifies that the org's payment is managed through stripe -->
{#if !$categorisedIssues.isLoading && $org.data?.organization?.paymentCustomerId && !onTrial && !pendingSetup}
<SettingsContainer title="Payment Method">
<div class="flex flex-row items-center gap-x-1">
<section>
<h2 class="section-header">Payment methods</h2>
<div class="section-card">
<div class="card-content">
{#if paymentIssues?.length}
<CancelCircle className="text-red-600" size="14px" />
{getPaymentIssueErrorText(paymentIssues)} Please click Manage below to correct.
<div class="flex items-center gap-x-2">
<CancelCircle className="text-red-600" size="14px" />
<span class="text-sm text-red-600">
{getPaymentIssueErrorText(paymentIssues)}
</span>
</div>
{:else if hasPaymentCustomer}
<!-- TODO: Show card details (brand, last4, expiry) from Stripe API -->
<div class="flex items-center gap-x-3">
<div class="card-icon">
<svg
class="w-5 h-5 text-fg-secondary"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M2.5 4A1.5 1.5 0 001 5.5v1h18v-1A1.5 1.5 0 0017.5 4h-15zM19 8.5H1v6A1.5 1.5 0 002.5 16h15a1.5 1.5 0 001.5-1.5v-6zM3 12a1 1 0 011-1h3a1 1 0 110 2H4a1 1 0 01-1-1z"
/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-fg-primary">
Payment method on file
</p>
<p class="text-xs text-fg-tertiary">Manage your cards via Stripe</p>
</div>
</div>
{:else}
Your payment method is valid and good to go.
<span class="text-sm text-fg-tertiary">No payment method on file.</span>
{/if}
</div>
{#snippet action()}
<Button type="secondary" onClick={handleManagePayment}>Manage</Button>
{/snippet}
</SettingsContainer>
{/if}
{#if hasPaymentCustomer}
<button class="manage-btn" onclick={handleManageCards}>
Manage in Stripe
<svg
class="w-3.5 h-3.5"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path d="M2.5 9.5l7-7M4 2.5h6v6" />
</svg>
</button>
{/if}
</div>
</section>

<style lang="postcss">
.section-header {
@apply text-lg font-medium text-fg-primary mb-3;
}

.section-card {
@apply flex items-center justify-between border rounded-lg p-4 bg-surface-background;
box-shadow:
0px 1px 2px 0px rgba(0, 0, 0, 0.06),
0px 1px 3px 0px rgba(0, 0, 0, 0.1);
}

.card-content {
@apply flex items-center;
}

.card-icon {
@apply flex items-center justify-center w-8 h-8 bg-surface-subtle rounded;
}

.manage-btn {
@apply flex items-center gap-1.5 text-sm font-medium text-primary-600 border border-primary-500 rounded-sm px-4 py-2 bg-transparent cursor-pointer;
}

.manage-btn:hover {
@apply bg-primary-50;
}
</style>
68 changes: 47 additions & 21 deletions web-admin/src/features/billing/contact/BillingContactSetting.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<script lang="ts">
import ChangeBillingContactDialog from "@rilldata/web-admin/features/billing/contact/ChangeBillingContactDialog.svelte";
import { getOrganizationBillingContactUser } from "@rilldata/web-admin/features/billing/contact/selectors";
import SettingsContainer from "@rilldata/web-admin/features/organizations/settings/SettingsContainer.svelte";
import AvatarListItem from "@rilldata/web-common/components/avatar/AvatarListItem.svelte";
import { Button } from "@rilldata/web-common/components/button";

let { organization }: { organization: string } = $props();

Expand All @@ -14,30 +12,58 @@
let isUpdateBillingContactDialogOpen = $state(false);
</script>

<SettingsContainer title="Billing Contact">
<div class="flex flex-row items-center gap-x-1">
{#if $billingContactUser}
<AvatarListItem
name={$billingContactUser.displayName}
email={$billingContactUser.email}
photoUrl={$billingContactUser.photoUrl}
/>
{:else}
This org has no billing contact.
{/if}
</div>
{#snippet action()}
<Button
type="secondary"
onClick={() => (isUpdateBillingContactDialogOpen = true)}
<section>
<h2 class="section-header">Billing Contact</h2>
<div class="section-card">
<div class="card-content">
{#if $billingContactUser}
<AvatarListItem
name={$billingContactUser.displayName}
email={$billingContactUser.email}
photoUrl={$billingContactUser.photoUrl}
/>
{:else}
<span class="text-sm text-fg-tertiary"
>This org has no billing contact.</span
>
{/if}
</div>
<button
class="manage-btn"
onclick={() => (isUpdateBillingContactDialogOpen = true)}
>
Change billing contact
</Button>
{/snippet}
</SettingsContainer>
</button>
</div>
</section>

<ChangeBillingContactDialog
bind:open={isUpdateBillingContactDialogOpen}
{organization}
currentBillingContact={$billingContactUser?.email}
/>

<style lang="postcss">
.section-header {
@apply text-lg font-medium text-fg-primary mb-3;
}

.section-card {
@apply flex items-center justify-between border rounded-lg p-4 bg-surface-background;
box-shadow:
0px 1px 2px 0px rgba(0, 0, 0, 0.06),
0px 1px 3px 0px rgba(0, 0, 0, 0.1);
}

.card-content {
@apply flex items-center;
}

.manage-btn {
@apply flex items-center gap-1.5 text-sm font-medium text-primary-600 border border-primary-500 rounded-sm px-4 py-2 bg-transparent cursor-pointer;
}

.manage-btn:hover {
@apply bg-primary-50;
}
</style>
21 changes: 21 additions & 0 deletions web-admin/src/features/billing/plans/CancelledTeamPlan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
organization,
plan,
showUpgradeDialog,
billingPortalUrl,
}: {
organization: string;
plan: V1BillingPlan;
showUpgradeDialog: boolean;
billingPortalUrl: string | undefined;
} = $props();

let categorisedIssues = $derived(
Expand Down Expand Up @@ -52,6 +54,16 @@
and your subscription has ended.
{/if}
</div>
{#if billingPortalUrl}
<div>
<a
href={billingPortalUrl}
target="_blank"
rel="noreferrer noopener"
class="invoice-link">View Invoice</a
>
</div>
{/if}
{#if plan}
<!-- if there is no plan then quotas will be set to 0. It doesnt make sense to show this then -->
<PlanQuotas {organization} />
Expand All @@ -77,3 +89,12 @@
endDate={cancelledSubIssue?.metadata.subscriptionCancelled?.endDate}
/>
{/if}

<style lang="postcss">
.invoice-link {
@apply text-sm text-primary-500 no-underline mt-2 inline-block;
}
.invoice-link:hover {
@apply text-primary-600 underline;
}
</style>
21 changes: 21 additions & 0 deletions web-admin/src/features/billing/plans/POCPlan.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
organization,
hasPayment,
plan,
billingPortalUrl,
}: {
organization: string;
hasPayment: boolean;
plan: V1BillingPlan;
billingPortalUrl: string | undefined;
} = $props();

let open = $state(false);
Expand All @@ -22,6 +24,16 @@
<SettingsContainer title={plan?.displayName}>
<div>
<div>You're currently on a custom contract.</div>
{#if billingPortalUrl}
<div>
<a
href={billingPortalUrl}
target="_blank"
rel="noreferrer noopener"
class="invoice-link">View Invoice</a
>
</div>
{/if}
<PlanQuotas {organization} />
</div>
{#snippet contact()}
Expand All @@ -38,3 +50,12 @@
</SettingsContainer>

<StartTeamPlanDialog bind:open {organization} type="base" />

<style lang="postcss">
.invoice-link {
@apply text-sm text-primary-500 no-underline mt-2 inline-block;
}
.invoice-link:hover {
@apply text-primary-600 underline;
}
</style>
Loading
Loading