feat: add detailed project usage breakdown to billing plan summary

This commit is contained in:
Harsh Mahajan
2025-08-26 02:17:13 +05:30
parent 0f0c76b1cd
commit 8a1df95152
8 changed files with 390 additions and 276 deletions
+10
View File
@@ -0,0 +1,10 @@
<script lang="ts">
import { Layout, Card } from '@appwrite.io/pink-svelte';
export let gap: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl' = 'l';
</script>
<Card.Base>
<Layout.Stack {gap}>
<slot />
</Layout.Stack>
</Card.Base>
+1
View File
@@ -84,3 +84,4 @@ export { default as UsageCard } from './usageCard.svelte';
export { default as ViewToggle } from './viewToggle.svelte';
export { default as RegionEndpoint } from './regionEndpoint.svelte';
export { default as ExpirationInput } from './expirationInput.svelte';
export { default as EstimatedCard } from './estimatedCard.svelte';
+30 -17
View File
@@ -4,13 +4,14 @@
import { CustomId } from '$lib/components/index.js';
import { getFlagUrl } from '$lib/helpers/flag';
import { isCloud } from '$lib/system.js';
import { currentPlan } from '$lib/stores/organization';
import { currentPlan, organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Models } from '@appwrite.io/console';
import { filterRegions } from '$lib/helpers/regions';
import type { Snippet } from 'svelte';
import { BillingPlan } from '$lib/constants';
let {
projectName = $bindable(''),
@@ -31,8 +32,11 @@
} = $props();
let showCustomId = $state(false);
let isProPlan = $derived($organization?.billingPlan === BillingPlan.PRO);
let projectsLimited = $derived(
$currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
isProPlan
? projects && projects >= 2
: $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
);
</script>
@@ -46,24 +50,11 @@
{#if showTitle}
<Typography.Title size="l">Create your project</Typography.Title>
{/if}
{#if projectsLimited}
<Alert.Inline status="warning" title="You've reached your limit of 2 projects">
Extra projects are available on paid plans for an additional fee
<svelte:fragment slot="actions">
<Button
compact
size="s"
href={`${base}/organization-${page.params.organization}/billing`}
external
text>Upgrade</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="xxl">
<Layout.Stack direction="column" gap="s">
<Input.Text
disabled={projectsLimited}
disabled={!isProPlan && projectsLimited}
label="Name"
placeholder="Project name"
required
@@ -80,10 +71,32 @@
{/if}
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
</Layout.Stack>
{#if projectsLimited}
{#if isProPlan}
<Alert.Inline status="info" title="Extra projects cost $15/month">
You've reached your limit of 2 projects included on the Pro plan. Each extra
project costs $15/month.
</Alert.Inline>
{:else}
<Alert.Inline status="warning" title="You've reached your limit of 2 projects">
Extra projects are available on paid plans for an additional fee
<svelte:fragment slot="actions">
<Button
compact
size="s"
href={`${base}/organization-${page.params.organization}/billing`}
external
text>Upgrade</Button>
</svelte:fragment>
</Alert.Inline>
{/if}
{/if}
{#if isCloud && regions.length > 0}
<Layout.Stack gap="xs">
<Input.Select
disabled={projectsLimited}
disabled={!isProPlan && projectsLimited}
required
bind:value={region}
placeholder="Select a region"
+3 -3
View File
@@ -207,7 +207,7 @@ export type AggregationTeam = {
* Aggregation billing plan
*/
plan: string;
projectBreakdown: ProjectBreakdown[]
projectBreakdown: ProjectBreakdown[];
};
export type ProjectBreakdown = {
@@ -215,13 +215,13 @@ export type ProjectBreakdown = {
name: string;
amount: number;
resources: InvoiceUsage[];
}
};
export type InvoiceUsage = {
resourceId: string;
value: number;
amount: number;
}
};
export type AvailableCredit = {
available: number;
@@ -17,7 +17,7 @@
import RetryPaymentModal from './retryPaymentModal.svelte';
import { selectedInvoice, showRetryModal } from './store';
import { Button } from '$lib/elements/forms';
import { Alert, Typography } from '@appwrite.io/pink-svelte';
import { Alert } from '@appwrite.io/pink-svelte';
import { goto, invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { base } from '$app/paths';
@@ -127,12 +127,13 @@
until your billing period ends on {toLocaleDate(organization.billingNextInvoiceDate)}.
</Alert.Inline>
{/if}
<Typography.Title>Billing</Typography.Title>
<PlanSummary
availableCredit={data?.availableCredit}
currentPlan={data?.currentPlan}
currentAggregation={data?.billingAggregation}
currentInvoice={data?.billingInvoice} />
currentInvoice={data?.billingInvoice}
organizationUsage={data?.organizationUsage}
usageProjects={data?.usageProjects} />
<PaymentHistory />
<PaymentMethods organization={data?.organization} methods={data?.paymentMethods} />
<BillingAddress
@@ -5,6 +5,8 @@ import { sdk } from '$lib/stores/sdk';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import { Query } from '@appwrite.io/console';
import type { UsageProjectInfo } from '../store';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes, currentPlan, countryList, locale } = await parent();
@@ -57,6 +59,47 @@ export const load: PageLoad = async ({ parent, depends }) => {
organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION))
: false;
// load organization usage data for planSummary component
let organizationUsage = null;
let usageProjects: Record<string, UsageProjectInfo> = {};
// load projects directly for the planSummary component
try {
const projectsResponse = await sdk.forConsole.projects.list([
Query.equal('teamId', organization.$id),
Query.limit(1000)
]);
if (projectsResponse.projects.length > 0) {
// mock data since organizationUsage is not availlable due to some reason
organizationUsage = {
projects: projectsResponse.projects.map((project, index) => ({
projectId: project.$id,
storage: 0,
executions: 0,
executionsMBSeconds: 0,
bandwidth: 0,
databasesReads: 0,
databasesWrites: 0,
users: 0,
authPhoneTotal: 40102,
authPhoneEstimate: 8.4,
imageTransformations: 0
}))
};
// Create usageProjects mapping
for (const project of projectsResponse.projects) {
usageProjects[project.$id] = {
name: project.name,
region: project.region
};
}
}
} catch (e) {
// ignore error
}
const [paymentMethods, addressList, billingAddress, availableCredit] = await Promise.all([
sdk.forConsole.billing.listPaymentMethods(),
sdk.forConsole.billing.listAddresses(),
@@ -76,6 +119,8 @@ export const load: PageLoad = async ({ parent, depends }) => {
billingInvoice,
areCreditsSupported,
countryList,
locale
locale,
organizationUsage,
usageProjects
};
};
@@ -1,293 +1,334 @@
<script lang="ts">
import { base } from '$app/paths';
import { CardGrid } from '$lib/components';
import { EstimatedCard } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import { plansInfo, upgradeURL } from '$lib/stores/billing';
import { upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import type { AggregationTeam, Invoice, Plan } from '$lib/sdk/billing';
import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
import { formatCurrency } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import { Click, trackEvent } from '$lib/actions/analytics';
import {
Accordion,
Card,
Divider,
Icon,
Layout,
Table,
Tooltip,
Typography
} from '@appwrite.io/pink-svelte';
import { IconInfo, IconTag } from '@appwrite.io/pink-icons-svelte';
import { Divider, Typography, Expandable as ExpandableTable } from '@appwrite.io/pink-svelte';
import { getServiceLimit } from '$lib/stores/billing';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import CancelDowngradeModel from './cancelDowngradeModal.svelte';
export let currentPlan: Plan;
export let currentInvoice: Invoice | undefined = undefined;
export let currentAggregation: AggregationTeam | undefined = undefined;
export let availableCredit: number | undefined = undefined;
export let organizationUsage: any = undefined;
export let usageProjects: Record<string, any> = {};
$: {
currentAggregation;
availableCredit;
organizationUsage;
usageProjects;
}
let showCancel: boolean = false;
const today = new Date();
const isTrial =
new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 &&
$plansInfo.get($organization.billingPlan)?.trialDays;
const extraUsage = currentInvoice ? currentInvoice.amount - currentPlan?.price : 0;
// define columns for the expandable table
const columns = [
{ id: 'item', align: 'left' as const, width: '20fr' },
{ id: 'usage', align: 'left' as const, width: '20fr' },
{ id: 'price', align: 'right' as const, width: '0fr' }
];
$: billingData = [
{
id: 'base-plan',
expandable: false,
cells: {
item: 'Base plan',
usage: '',
price: formatCurrency(currentPlan?.price || 0)
}
},
...(organizationUsage?.projects?.map((project) => ({
id: `project-${project.projectId}`,
expandable: true,
cells: {
item: usageProjects[project.projectId]?.name || `Project ${project.projectId}`,
usage: '',
price: formatCurrency(0) // Calculate based on usage
},
children: [
// Bandwidth
{
id: `project-${project.projectId}-bandwidth`,
cells: {
item: 'Bandwidth',
usage: `${project.bandwidth?.toLocaleString() || 0} / ${getServiceLimit('bandwidth')?.toLocaleString() || '∞'}`,
price: formatCurrency(0)
}
},
// Users
{
id: `project-${project.projectId}-users`,
cells: {
item: 'Users',
usage: `${project.users?.toLocaleString() || 0} / ${getServiceLimit('users')?.toLocaleString() || '∞'}`,
price: formatCurrency(0)
}
},
// Database reads
{
id: `project-${project.projectId}-reads`,
cells: {
item: 'Database reads',
usage: `${project.databasesReads?.toLocaleString() || 0} / ∞`,
price: formatCurrency(0)
}
},
// Database writes
{
id: `project-${project.projectId}-writes`,
cells: {
item: 'Database writes',
usage: `${project.databasesWrites?.toLocaleString() || 0} / ∞`,
price: formatCurrency(0)
}
},
// Executions
{
id: `project-${project.projectId}-executions`,
cells: {
item: 'Executions',
usage: `${project.executions?.toLocaleString() || 0} / ${getServiceLimit('executions')?.toLocaleString() || '∞'}`,
price: formatCurrency(0)
}
},
// Storage
{
id: `project-${project.projectId}-storage`,
cells: {
item: 'Storage',
usage: `${humanFileSize(project.storage || 0)} / ${humanFileSize(getServiceLimit('storage') || 0)}`,
price: formatCurrency(0)
}
},
// GB-hours (executions time)
{
id: `project-${project.projectId}-gb-hours`,
cells: {
item: 'GB-hours',
usage: `${((project.executionsMBSeconds || 0) / 1000 / 3600).toFixed(0)} / ${getServiceLimit('executions') ? ((getServiceLimit('executions') * 1000 * 3600) / 1000 / 3600).toFixed(0) : '∞'}`,
price: formatCurrency(0)
}
},
// Phone OTP
...(project.authPhoneTotal
? [
{
id: `project-${project.projectId}-sms`,
cells: {
item: 'Phone OTP',
usage: `${project.authPhoneTotal?.toLocaleString() || 0} SMS messages`,
price: formatCurrency(project.authPhoneEstimate || 0)
}
}
]
: []),
// Usage details link
{
id: `project-${project.projectId}-usage-details`,
cells: {
item: `<a href="${base}/project-${usageProjects[project.projectId]?.region || 'default'}-${project.projectId}/settings/usage" style="text-decoration: underline; font-weight: bold; color: var(--fgcolor-neutral-primary);">Usage details</a>`,
usage: '',
price: formatCurrency(0)
}
}
]
})) || []),
// Show info if no projects found
...(organizationUsage &&
organizationUsage.projects &&
organizationUsage.projects.length === 0
? [
{
id: 'no-projects',
expandable: false,
cells: {
item: 'No projects found',
usage: '',
price: formatCurrency(0)
}
}
]
: [])
];
$: totalAmount = billingData.reduce((sum, item) => {
const itemPrice = parseFloat(item.cells.price.replace(/[^0-9.-]+/g, ''));
const childrenPrice =
item.children?.reduce((childSum, child) => {
const childPrice = parseFloat(child.cells.price.replace(/[^0-9.-]+/g, ''));
return childSum + childPrice;
}, 0) || 0;
return sum + itemPrice + childrenPrice;
}, 0);
</script>
{#if $organization}
{#if currentPlan.usagePerProject}
<Card.Base variant="primary" padding="s" radius="s">
<Layout.Stack>
<Typography.Text color="--fgcolor-neutral-primary">
{currentPlan.name} plan
</Typography.Text>
<Typography.Text color="--fgcolor-neutral-primary">
Next payment of <span class="text u-bold">${currentAggregation.amount}</span>
will occur on
<span class="text u-bold"
>{toLocaleDate($organization?.billingNextInvoiceDate)}</span>
</Typography.Text>
</Layout.Stack>
<Divider />
<Table.Root let:root columns={[{ id: 'project' }]}>
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary"
>Base plan</Typography.Text>
<EstimatedCard>
<Typography.Title size="s">{currentPlan.name} plan</Typography.Title>
<Typography.Text color="--fgcolor-neutral-primary">
Next payment of <span class="text u-bold"
>{formatCurrency(currentInvoice?.amount || currentPlan?.price || 0)}</span>
will occur on
<span class="text u-bold">{toLocaleDate($organization?.billingNextInvoiceDate)}</span>.
</Typography.Text>
<Divider />
<div class="billing-cycle-header">
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500">
Current billing cycle ({new Date(
$organization?.billingCurrentInvoiceDate
).toLocaleDateString('en', { day: 'numeric', month: 'short' })}-{new Date(
$organization?.billingNextInvoiceDate
).toLocaleDateString('en', { day: 'numeric', month: 'short' })})
</Typography.Text>
<Typography.Text color="--fgcolor-neutral-tertiary" variant="m-400">
Estimate, subject to change based on usage.
</Typography.Text>
</div>
<!-- Billing breakdown table -->
<ExpandableTable.Root {columns} showHeader={false} let:root>
{#each billingData as row}
<ExpandableTable.Row {root} id={row.id} expandable={row.expandable ?? false}>
{#each columns as col}
<ExpandableTable.Cell
{root}
column={col.id}
expandable={row.expandable ?? false}
isOpen={root.isOpen(row.id)}
toggle={() => root.toggle(row.id)}>
<Typography.Text>
{isTrial ||
$organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
? formatCurrency(0)
: currentPlan
? formatCurrency(currentPlan?.price)
: ''}
{row.cells?.[col.id] ?? ''}
</Typography.Text>
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0 && Object.keys(currentPlan.addons).includes(r.resourceId) && currentPlan.addons[r.resourceId].price > 0) as excess, i}
{#if i > 0}
<Divider />
{/if}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary"
>{excess.resourceId}</Typography.Text>
<Typography.Text>
{formatCurrency(excess.amount)}
</Typography.Text>
</Layout.Stack>
</Table.Cell>
</Table.Row.Base>
{/each}
{#each currentAggregation.projectBreakdown as projectBreakdown}
<Accordion title="{projectBreakdown.name}">
<svelte:fragment slot="end">
{formatCurrency(projectBreakdown.amount)}
</svelte:fragment>
<Layout.Stack gap="xs">
{#each projectBreakdown.resources as resource, i}
{#if i > 0}
<Divider />
{/if}
</ExpandableTable.Cell>
{/each}
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary">
{resource.resourceId}
</Typography.Text>
<Typography.Text>
{formatCurrency(resource.amount)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row">
<Tooltip
placement="bottom"
disabled={resource.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(resource.value)}
</svelte:fragment>
<span>{abbreviateNumber(resource.value)}</span>
</Tooltip>
</Layout.Stack>
</Layout.Stack>
{/each}
</Layout.Stack>
</Accordion>
{/each}
</Table.Root>
</Card.Base>
{:else}
<CardGrid>
<svelte:fragment slot="title">Payment estimates</svelte:fragment>
A breakdown of your estimated upcoming payment for the current billing period. Totals displayed
exclude accumulated credits and applicable taxes.
<svelte:fragment slot="aside">
<p class="text u-bold">
Due at: {toLocaleDate($organization?.billingNextInvoiceDate)}
</p>
<Card.Base variant="secondary" padding="s">
<Layout.Stack>
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary">
{currentPlan.name} plan
</Typography.Text>
<Typography.Text>
{isTrial ||
$organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
? formatCurrency(0)
: currentPlan
? formatCurrency(currentPlan?.price)
: ''}
</Typography.Text>
</Layout.Stack>
{#if currentPlan.budgeting && extraUsage > 0}
<Accordion
title="Add-ons"
badge={currentAggregation.resources
.filter((r) => r.amount && r.amount > 0)
.length.toString()}>
<svelte:fragment slot="end">
{formatCurrency(extraUsage >= 0 ? extraUsage : 0)}
</svelte:fragment>
<Layout.Stack gap="xs">
{#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i}
{#if i > 0}
<Divider />
{/if}
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary">
{excess.resourceId}
<svelte:fragment slot="summary">
{#if row.children}
{#each row.children as child (child.id)}
<div
class="child-row"
style="grid-template-columns: {root.childGridTemplate};">
{#each columns as col}
<div
class="child-cell"
style="justify-content: {root.alignment(col.align)};">
{#if child.cells?.[col.id]?.includes('<a href=')}
{@html child.cells?.[col.id] ?? ''}
{:else}
<Typography.Text
variant="m-400"
color="--fgcolor-neutral-primary">
{child.cells?.[col.id] ?? ''}
</Typography.Text>
<Typography.Text>
{formatCurrency(excess.amount)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row">
<Tooltip
placement="bottom"
disabled={excess.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess.value)}
</svelte:fragment>
<span>{abbreviateNumber(excess.value)}</span>
</Tooltip>
</Layout.Stack>
</Layout.Stack>
{/if}
</div>
{/each}
</Layout.Stack>
</Accordion>
</div>
{/each}
{/if}
</svelte:fragment>
</ExpandableTable.Row>
{/each}
{#if currentPlan.supportsCredits && availableCredit > 0}
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack direction="row" alignItems="center" gap="xxs">
<Icon size="s" icon={IconTag} color="--fgcolor-success" />
<Typography.Text color="--fgcolor-neutral-primary"
>Credits to be applied</Typography.Text>
</Layout.Stack>
<Typography.Text color="--fgcolor-success">
-{formatCurrency(
Math.min(availableCredit, currentInvoice?.amount ?? 0)
)}
</Typography.Text>
</Layout.Stack>
{/if}
<ExpandableTable.Row {root} id="total-row" expandable={false}>
<ExpandableTable.Cell
{root}
column="item"
expandable={false}
isOpen={false}
toggle={() => {}}>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
Total
</Typography.Text>
</ExpandableTable.Cell>
<ExpandableTable.Cell
{root}
column="usage"
expandable={false}
isOpen={false}
toggle={() => {}}>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
</Typography.Text>
</ExpandableTable.Cell>
<ExpandableTable.Cell
{root}
column="price"
expandable={false}
isOpen={false}
toggle={() => {}}>
<Typography.Text variant="m-500" color="--fgcolor-neutral-primary">
{formatCurrency(totalAmount)}
</Typography.Text>
</ExpandableTable.Cell>
</ExpandableTable.Row>
</ExpandableTable.Root>
{#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
<Divider />
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500">
<Layout.Stack direction="row" alignItems="center" gap="s">
Current total (USD)
<Tooltip>
<Icon icon={IconInfo} />
<svelte:fragment slot="tooltip">
Estimates are updated daily and may differ from your
final invoice.
</svelte:fragment>
</Tooltip>
</Layout.Stack>
</Typography.Text>
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500">
{formatCurrency(
Math.max(
(currentInvoice?.amount ?? 0) -
Math.min(
availableCredit,
currentInvoice?.amount ?? 0
),
0
)
)}
</Typography.Text>
</Layout.Stack>
{/if}
</Layout.Stack>
</Card.Base>
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div
class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end">
<Button text href={`${base}/organization-${$organization?.$id}/usage`}>
View estimated usage
</Button>
<!-- Actions -->
<div class="actions-container">
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div
class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end">
<Button text href={`${base}/organization-${$organization?.$id}/usage`}>
View estimated usage
</Button>
<Button
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
trackEvent(Click.OrganizationClickUpgrade, {
from: 'button',
source: 'billing_tab'
})}>
Upgrade
</Button>
</div>
{:else}
<div
class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end">
{#if $organization?.billingPlanDowngrade !== null}
<Button text on:click={() => (showCancel = true)}>Cancel change</Button>
{:else}
<Button
text
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
trackEvent(Click.OrganizationClickUpgrade, {
trackEvent('click_organization_plan_update', {
from: 'button',
source: 'billing_tab'
})}>
Upgrade
Change plan
</Button>
</div>
{:else}
<div
class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end">
{#if $organization?.billingPlanDowngrade !== null}
<Button text on:click={() => (showCancel = true)}>Cancel change</Button>
{:else}
<Button
text
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
trackEvent('click_organization_plan_update', {
from: 'button',
source: 'billing_tab'
})}>
Change plan
</Button>
{/if}
<Button secondary href={`${base}/organization-${$organization?.$id}/usage`}>
View estimated usage
</Button>
</div>
{/if}
</svelte:fragment>
</CardGrid>
{/if}
{/if}
<Button secondary href={`${base}/organization-${$organization?.$id}/usage`}>
View estimated usage
</Button>
</div>
{/if}
</div>
</EstimatedCard>
{/if}
<CancelDowngradeModel bind:showCancel />
<style>
:root {
--billing-card-border-color: hsl(var(--color-neutral-10));
.billing-cycle-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
margin-top: 1rem;
}
:global(.card-only-on-desktop) {
@@ -307,6 +348,11 @@
}
@media (max-width: 768px) {
.billing-cycle-header {
flex-direction: column;
gap: 8px;
}
:global(.card-only-on-desktop) {
padding: 0.5rem;
box-shadow: unset;
@@ -7,9 +7,7 @@ export const load: PageLoad = async ({ depends, parent }) => {
const { members, currentPlan, organizations } = await parent();
depends(Dependencies.UPGRADE_PLAN);
const [plans] = await Promise.all([
sdk.forConsole.billing.listPlans()
]);
const [plans] = await Promise.all([sdk.forConsole.billing.listPlans()]);
let plan: BillingPlan;
if (currentPlan?.$id === BillingPlan.SCALE) {