feat: use plan change estimation in billing flow

This commit is contained in:
harsh mahajan
2026-05-07 13:43:14 +05:30
parent 5f5a20fc62
commit c8f9e41ea4
3 changed files with 387 additions and 155 deletions
@@ -7,6 +7,8 @@
import { AppwriteException, type Models } from '@appwrite.io/console';
import DiscountsApplied from './discountsApplied.svelte';
type PlanChangeEstimate = Models.EstimationUpdatePlan & Partial<Models.EstimationPlanChange>;
export let billingPlan: Models.BillingPlan;
export let collaborators: string[];
export let couponData: Partial<Models.Coupon>;
@@ -14,9 +16,30 @@
export let fixedCoupon = false; // If true, the coupon cannot be removed
export let isDowngrade = false;
export let organizationId: string | undefined = undefined;
export let estimationOverride: PlanChangeEstimate | null = null;
let budgetEnabled = false;
let estimation: Models.Estimation;
let estimation: Models.Estimation | PlanChangeEstimate;
function getEstimateDetails(value: Models.Estimation | PlanChangeEstimate) {
if (!value) return null;
return 'estimation' in value && value.estimation ? value.estimation : value;
}
function normalizeItems(items: unknown): Models.EstimationItem[] {
if (!items) return [];
if (Array.isArray(items)) {
return items as Models.EstimationItem[];
}
if (typeof items === 'object') {
return Object.values(items as Record<string, Models.EstimationItem>);
}
return [];
}
async function getEstimate(
billingPlan: string,
@@ -78,17 +101,22 @@
}
}
$: organizationId
? getUpdatePlanEstimate(organizationId, billingPlan.$id, collaborators, couponData?.code)
: getEstimate(billingPlan.$id, collaborators, couponData?.code);
$: if (estimationOverride) {
estimation = estimationOverride;
} else if (organizationId) {
getUpdatePlanEstimate(organizationId, billingPlan.$id, collaborators, couponData?.code);
} else {
getEstimate(billingPlan.$id, collaborators, couponData?.code);
}
</script>
{#if estimation}
{@const estimationDetails = getEstimateDetails(estimation)}
<Card.Base padding="s">
<Layout.Stack>
<slot />
{#each estimation.items ?? [] as item}
{#each normalizeItems(estimationDetails?.items) as item}
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text variant={isDowngrade ? 'm-500' : 'm-400'}
>{item.label}</Typography.Text>
@@ -96,7 +124,7 @@
>{formatCurrency(item.value)}</Typography.Text>
</Layout.Stack>
{/each}
{#each estimation.discounts ?? [] as item}
{#each normalizeItems(estimationDetails?.discounts) as item}
<DiscountsApplied {couponData} {...item} />
{/each}
@@ -108,15 +136,15 @@
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text>Total due</Typography.Text>
<Typography.Text>
{formatCurrency(estimation.grossAmount)}
{formatCurrency(estimationDetails?.grossAmount ?? 0)}
</Typography.Text>
</Layout.Stack>
<Typography.Text>
You'll pay <b>{formatCurrency(estimation.grossAmount)}</b>
You'll pay <b>{formatCurrency(estimationDetails?.grossAmount ?? 0)}</b>
now.
{#if couponData?.code}Once your credits run out,{:else}Then{/if} you'll be charged
<b>{formatCurrency(estimation.amount)}</b> every 30 days.
<b>{formatCurrency(estimationDetails?.amount ?? 0)}</b> every 30 days.
</Typography.Text>
<InputChoice
+250 -118
View File
@@ -18,12 +18,22 @@
let {
members = [],
storageUsage = 0,
projects = $bindable([])
projects = $bindable([]),
targetPlan = null,
planChangeEstimate = null,
estimateError = null,
loading = false
}: {
storageUsage?: number;
projects?: Models.Project[];
members?: Models.Membership[];
organization: Models.Organization;
targetPlan?: Models.BillingPlan | null;
planChangeEstimate?:
| (Models.EstimationUpdatePlan & Partial<Models.EstimationPlanChange>)
| null;
estimateError?: string | null;
loading?: boolean;
} = $props();
let showSelectProject = $state(false);
@@ -34,6 +44,38 @@
let isDeletingProjects = $state(false);
let selectedProjectsToDelete = $state<Array<string>>([]);
const baseFreePlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
const targetPlanLimits = $derived(planChangeEstimate?.limits ?? null);
const unsupportedAddons = $derived(targetPlanLimits?.unsupportedAddons ?? []);
const nonCompliantProjects = $derived(
targetPlanLimits?.projects?.filter((project) => !project.isCompliant) ?? []
);
const projectComplianceRows = $derived.by(() => {
return nonCompliantProjects.flatMap((project) => {
if (project.error) {
return [
{
id: `${project.$id}-error`,
project: project.name,
resource: 'Project evaluation',
currentUsage: 'Unavailable',
limit: 'Unavailable',
action: project.error
}
];
}
return project.resources
.filter((resource) => resource.status !== 'compliant' || resource.excess > 0)
.map((resource) => ({
id: `${project.$id}-${resource.type}`,
project: project.name,
resource: formatResourceType(resource.type),
currentUsage: formatNumber(resource.currentUsage),
limit: formatNumber(resource.limit),
action: resource.resolutionHint
}));
});
});
// Derived state using runes
const freePlanLimits = $derived({
@@ -42,7 +84,8 @@
storage: getServiceLimit('storage', null, baseFreePlan)
});
// When preparing to downgrade to Free, enforce Free plan limit locally (2)
// When preparing to downgrade to Free, enforce Free plan limit locally.
const isDowngradingToFree = $derived(targetPlan?.group === BillingPlanGroup.Starter);
const allowedProjectsToKeep = $derived(freePlanLimits.projects);
const currentUsage = $derived({
@@ -54,13 +97,13 @@
const storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024));
const isLimitExceeded = $derived({
projects: currentUsage.projects > freePlanLimits.projects,
members: currentUsage.members > freePlanLimits.members,
storage: storageUsageGB > freePlanLimits.storage
projects: isDowngradingToFree && currentUsage.projects > freePlanLimits.projects,
members: isDowngradingToFree && currentUsage.members > freePlanLimits.members,
storage: isDowngradingToFree && storageUsageGB > freePlanLimits.storage
});
const excessUsage = $derived({
projects: Math.max(0, currentUsage.projects),
projects: Math.max(0, currentUsage.projects - freePlanLimits.projects),
members: Math.max(0, currentUsage.members - freePlanLimits.members),
storage: Math.max(0, storageUsageGB - freePlanLimits.storage)
});
@@ -71,6 +114,15 @@
return formatNumberWithCommas(num);
}
function formatResourceType(type: string): string {
return type
.replace(/([A-Z])/g, ' $1')
.replace(/[_-]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.replace(/\b\w/g, (char) => char.toUpperCase());
}
function handleManageProjects() {
showSelectProject = true;
showSelectionReminder = false;
@@ -86,6 +138,7 @@
if (!isUnderLimitPostSelection) {
error = `You can keep a maximum ${allowedProjectsToKeep} projects on the selected plan.`;
isDeletingProjects = false;
return;
}
@@ -147,6 +200,81 @@
</script>
<Layout.Stack gap="l">
{#if targetPlanLimits}
{#if unsupportedAddons.length > 0}
<Alert.Inline status="warning" title="Unsupported add-ons detected">
Remove these add-ons before switching to {targetPlan?.name}: {unsupportedAddons.join(
', '
)}.
</Alert.Inline>
{/if}
{#if targetPlanLimits.canChangePlan}
<Alert.Inline status="success" title="All projects fit this plan">
{targetPlanLimits.totalProjects} project{targetPlanLimits.totalProjects === 1
? ''
: 's'} comply with the {targetPlan?.name} plan limits.
</Alert.Inline>
{:else if targetPlanLimits.nonCompliantProjects > 0}
<Alert.Inline status="warning" title="Some projects exceed the selected plan limits">
{targetPlanLimits.nonCompliantProjects} project{targetPlanLimits.nonCompliantProjects ===
1
? ''
: 's'} need attention before you can switch to the {targetPlan?.name} plan.
</Alert.Inline>
{/if}
{:else if loading}
<Alert.Inline status="info" title="Checking plan limits">
Reviewing your projects against the {targetPlan?.name} plan limits.
</Alert.Inline>
{:else if estimateError}
<Alert.Inline status="warning" title="We couldn't load plan compliance details">
{estimateError}
</Alert.Inline>
{/if}
{#if projectComplianceRows.length > 0}
<div class="responsive-table">
<Table.Root
columns={[
{ id: 'project', width: { min: 180 } },
{ id: 'resource', width: { min: 140 } },
{ id: 'currentUsage', width: { min: 110 } },
{ id: 'limit', width: { min: 110 } },
{ id: 'action', width: { min: 260 } }
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="project" {root}>Project</Table.Header.Cell>
<Table.Header.Cell column="resource" {root}>Resource</Table.Header.Cell>
<Table.Header.Cell column="currentUsage" {root}>Current</Table.Header.Cell>
<Table.Header.Cell column="limit" {root}>Limit</Table.Header.Cell>
<Table.Header.Cell column="action" {root}>Required action</Table.Header.Cell>
</svelte:fragment>
{#each projectComplianceRows as row}
<Table.Row.Base {root}>
<Table.Cell column="project" {root}>
<Typography.Text>{row.project}</Typography.Text>
</Table.Cell>
<Table.Cell column="resource" {root}>
<Typography.Text>{row.resource}</Typography.Text>
</Table.Cell>
<Table.Cell column="currentUsage" {root}>
<Typography.Text>{row.currentUsage}</Typography.Text>
</Table.Cell>
<Table.Cell column="limit" {root}>
<Typography.Text>{row.limit}</Typography.Text>
</Table.Cell>
<Table.Cell column="action" {root}>
<Typography.Text>{row.action}</Typography.Text>
</Table.Cell>
</Table.Row.Base>
{/each}
</Table.Root>
</div>
{/if}
{#if showSelectionReminder}
<Alert.Inline status="warning" title="Choose projects to keep">
The Free plan lets you keep {allowedProjectsToKeep} projects. Select them before continuing.
@@ -160,126 +288,130 @@
</Alert.Inline>
{/if}
<div class="responsive-table">
<Table.Root
columns={[
{ id: 'resource', width: { min: 215 } },
{ id: 'freeLimit', width: { min: 100 } },
{ id: 'excessUsage', width: { min: 120 } },
{ id: 'manage', width: { min: 110 } }
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="resource" {root}>Resource</Table.Header.Cell>
<Table.Header.Cell column="freeLimit" {root}>Free limit</Table.Header.Cell>
<Table.Header.Cell column="excessUsage" {root}>
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Typography.Text>Excess usage</Typography.Text>
<Tooltip placement="bottom" portal>
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">Usage beyond the Free plan limits.</span>
</Tooltip>
</Layout.Stack>
</Table.Header.Cell>
<Table.Header.Cell column="manage" {root}></Table.Header.Cell>
</svelte:fragment>
{#if isDowngradingToFree}
<div class="responsive-table">
<Table.Root
columns={[
{ id: 'resource', width: { min: 215 } },
{ id: 'freeLimit', width: { min: 100 } },
{ id: 'excessUsage', width: { min: 120 } },
{ id: 'manage', width: { min: 110 } }
]}
let:root>
<svelte:fragment slot="header" let:root>
<Table.Header.Cell column="resource" {root}>Resource</Table.Header.Cell>
<Table.Header.Cell column="freeLimit" {root}>Free limit</Table.Header.Cell>
<Table.Header.Cell column="excessUsage" {root}>
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Typography.Text>Excess usage</Typography.Text>
<Tooltip placement="bottom" portal>
<Icon icon={IconInfo} size="s" />
<span slot="tooltip">Usage beyond the Free plan limits.</span>
</Tooltip>
</Layout.Stack>
</Table.Header.Cell>
<Table.Header.Cell column="manage" {root}></Table.Header.Cell>
</svelte:fragment>
<!-- Projects Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Typography.Text>Projects</Typography.Text>
<!-- Projects Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Typography.Text>Projects</Typography.Text>
{#if isLimitExceeded.projects}
<Badge
size="xs"
content="Action required"
variant="secondary"
type="warning" />
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text
>{formatNumber(allowedProjectsToKeep)} projects</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.projects}
<Badge
size="xs"
content="Action required"
variant="secondary"
type="warning" />
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{formatNumber(excessUsage.projects)} projects
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary">
{formatNumber(currentUsage.projects)} / {formatNumber(
allowedProjectsToKeep
)}
</Typography.Text>
{/if}
</Layout.Stack>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text
>{formatNumber(allowedProjectsToKeep)} projects</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.projects}
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{formatNumber(excessUsage.projects)} projects
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary">
{formatNumber(currentUsage.projects)} / {formatNumber(
allowedProjectsToKeep
)}
</Typography.Text>
{/if}
</Table.Cell>
<Table.Cell column="manage" {root}>
{#if isLimitExceeded.projects}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button size="xs" secondary on:click={handleManageProjects}
>Manage projects</Button>
</Layout.Stack>
{/if}
</Table.Cell>
</Table.Row.Base>
</Table.Cell>
<Table.Cell column="manage" {root}>
{#if isLimitExceeded.projects}
<Layout.Stack direction="row" justifyContent="flex-end">
<Button size="xs" secondary on:click={handleManageProjects}
>Manage projects</Button>
</Layout.Stack>
{/if}
</Table.Cell>
</Table.Row.Base>
<!-- Organization Members Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Typography.Text>Organization members</Typography.Text>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text>{formatNumber(freePlanLimits.members)} member</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.members}
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{formatNumber(excessUsage.members)} members
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary">N/A</Typography.Text>
{/if}
</Table.Cell>
<Table.Cell column="manage" {root}></Table.Cell>
</Table.Row.Base>
<!-- Organization Members Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Typography.Text>Organization members</Typography.Text>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text
>{formatNumber(freePlanLimits.members)} member</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.members}
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{formatNumber(excessUsage.members)} members
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary"
>N/A</Typography.Text>
{/if}
</Table.Cell>
<Table.Cell column="manage" {root}></Table.Cell>
</Table.Row.Base>
<!-- Storage Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Typography.Text>Storage</Typography.Text>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text>{freePlanLimits.storage} GB</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.storage}
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{excessUsage.storage.toFixed(2)} GB
<!-- Storage Row -->
<Table.Row.Base {root}>
<Table.Cell column="resource" {root}>
<Typography.Text>Storage</Typography.Text>
</Table.Cell>
<Table.Cell column="freeLimit" {root}>
<Typography.Text>{freePlanLimits.storage} GB</Typography.Text>
</Table.Cell>
<Table.Cell column="excessUsage" {root}>
{#if isLimitExceeded.storage}
<Layout.Stack direction="row" alignItems="center" gap="xs">
<Icon icon={IconArrowUp} size="s" color="--fgcolor-error" />
<Typography.Text color="--fgcolor-error">
{excessUsage.storage.toFixed(2)} GB
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary">
{storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB
</Typography.Text>
</Layout.Stack>
{:else}
<Typography.Text color="--fgcolor-neutral-secondary">
{storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB
</Typography.Text>
{/if}
</Table.Cell>
<Table.Cell column="manage" {root}></Table.Cell>
</Table.Row.Base>
</Table.Root>
</div>
{/if}
</Table.Cell>
<Table.Cell column="manage" {root}></Table.Cell>
</Table.Row.Base>
</Table.Root>
</div>
{/if}
</Layout.Stack>
{#if showSelectProject}
{#if showSelectProject && isDowngradingToFree}
{@const requiredToDelete = currentUsage.projects - allowedProjectsToKeep}
<Modal
bind:show={showSelectProject}
@@ -38,6 +38,8 @@
import { BillingPlanGroup, type Models, Query } from '@appwrite.io/console';
import { toLocaleDate } from '$lib/helpers/date';
type PlanChangeEstimate = Models.EstimationUpdatePlan & Partial<Models.EstimationPlanChange>;
export let data;
let selectedPlan: Models.BillingPlan = data.plan;
@@ -60,6 +62,10 @@
let feedbackMessage: string;
let orgUsage: Models.UsageOrganization;
let allProjects: { projects: Models.Project[] } = { projects: [] };
let planChangeEstimate: PlanChangeEstimate | null = null;
let planEstimateError: string | null = null;
let isLoadingPlanEstimate = false;
let planEstimateRequestId = 0;
$: paymentMethods = null;
@@ -133,11 +139,76 @@
return paymentMethods;
}
function hasExcessProjectsForFreePlan(): boolean {
const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
const freePlanProjectLimit = freeBasePlan?.projects ?? 2;
const currentProjectCount = allProjects.projects.length;
return currentProjectCount > freePlanProjectLimit;
function getAdditionalInvites(): string[] {
return (collaborators ?? []).filter(
(collaborator) =>
!data?.members?.memberships?.find((member) => member.userEmail === collaborator)
);
}
function getPlanChangeLimits(): Models.PlanChangeLimits | null {
return planChangeEstimate?.limits ?? null;
}
function getPlanChangeBlockingMessage(): string | null {
const limits = getPlanChangeLimits();
if (!limits || limits.canChangePlan) {
return null;
}
if (limits.unsupportedAddons?.length) {
return `Remove unsupported add-ons before switching to ${selectedPlan.name}.`;
}
if (limits.nonCompliantProjects > 0) {
return `${limits.nonCompliantProjects} project${limits.nonCompliantProjects !== 1 ? 's' : ''} exceed the ${selectedPlan.name} plan limits. Resolve the issues below before continuing.`;
}
return `This organization doesn't currently meet the ${selectedPlan.name} plan limits.`;
}
async function loadPlanChangeEstimate() {
const requestId = ++planEstimateRequestId;
if (
!data?.organization?.$id ||
!selectedPlan?.$id ||
selectedPlan.$id === $organization?.billingPlanId
) {
planChangeEstimate = null;
planEstimateError = null;
isLoadingPlanEstimate = false;
return;
}
isLoadingPlanEstimate = true;
planEstimateError = null;
try {
const estimation = await sdk.forConsole.organizations.estimationUpdatePlan({
organizationId: data.organization.$id,
billingPlan: selectedPlan.$id,
invites: getAdditionalInvites(),
couponId:
selectedCoupon?.code && selectedCoupon.code.length > 0
? selectedCoupon.code
: null
});
if (requestId !== planEstimateRequestId) return;
planChangeEstimate = estimation as PlanChangeEstimate;
} catch (e) {
if (requestId !== planEstimateRequestId) return;
planChangeEstimate = null;
planEstimateError = e.message;
} finally {
if (requestId === planEstimateRequestId) {
isLoadingPlanEstimate = false;
}
}
}
async function handleSubmit() {
@@ -167,14 +238,11 @@
}
async function downgrade() {
if (selectedPlan.group === BillingPlanGroup.Starter && hasExcessProjectsForFreePlan()) {
const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
const freePlanProjectLimit = freeBasePlan?.projects ?? 2;
const currentProjectCount = allProjects?.projects?.length ?? 0;
const blockingMessage = getPlanChangeBlockingMessage();
if (blockingMessage) {
addNotification({
type: 'error',
message: `Please delete ${currentProjectCount - freePlanProjectLimit} project${currentProjectCount - freePlanProjectLimit !== 1 ? 's' : ''} before downgrading`
message: blockingMessage
});
return;
}
@@ -241,13 +309,7 @@
async function upgrade() {
try {
// Add collaborators
let newCollaborators = [];
if (collaborators?.length) {
newCollaborators = collaborators.filter(
(collaborator) =>
!data?.members?.memberships?.find((m) => m.userEmail === collaborator)
);
}
const newCollaborators = getAdditionalInvites();
const org = await sdk.forConsole.organizations.updatePlan({
organizationId: data.organization.$id,
billingPlan: selectedPlan.$id,
@@ -313,18 +375,22 @@
$: isUpgrade = selectedPlan.order > $currentPlan?.order;
$: isDowngrade = selectedPlan.order < $currentPlan?.order;
$: if (data?.organization?.$id && selectedPlan?.$id) {
loadPlanChangeEstimate();
}
// Check if projects exceed Free plan limit when downgrading
$: isButtonDisabled = (() => {
const freeBasePlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
const freePlanProjectLimit = freeBasePlan?.projects ?? 2;
const hasExcessProjects = allProjects.projects.length > freePlanProjectLimit;
if ($organization?.billingPlanId === selectedPlan.$id) return true;
if (isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && data.hasFreeOrgs)
return true;
if (isDowngrade && selectedPlan.$id !== $organization?.billingPlanId) {
if (isLoadingPlanEstimate) return true;
return isDowngrade && selectedPlan.group === BillingPlanGroup.Starter && hasExcessProjects;
const limits = getPlanChangeLimits();
if (limits && !limits.canChangePlan) return true;
}
return false;
})();
</script>
@@ -400,7 +466,8 @@
features until your billing period ends. After that,
<span class="u-bold"
>all team members except the owner will be removed,</span>
and service disruptions may occur if usage exceeds Free plan limits.
and service disruptions may occur if usage exceeds {selectedPlan.name}
plan limits.
</Alert.Inline>
{/if}
@@ -408,7 +475,11 @@
organization={data.organization}
bind:projects={allProjects.projects}
members={data.members?.memberships || []}
storageUsage={orgUsage?.storageTotal ?? 0} />
storageUsage={orgUsage?.storageTotal ?? 0}
targetPlan={selectedPlan}
{planChangeEstimate}
estimateError={planEstimateError}
loading={isLoadingPlanEstimate} />
{/if}
</Layout.Stack>
</Fieldset>
@@ -484,12 +555,13 @@
{@const isSameGroup = data.organization.billingPlanDetails.group === selectedPlan.group}
{#if !isStarter && !isSameGroup && isSelfService}
<EstimatedTotalBox
{collaborators}
collaborators={getAdditionalInvites()}
{isDowngrade}
billingPlan={selectedPlan}
bind:couponData={selectedCoupon}
bind:billingBudget
organizationId={data.organization.$id} />
organizationId={data.organization.$id}
estimationOverride={planChangeEstimate} />
{:else if isSelfService}
<PlanComparisonBox downgrade={data.hasFreeOrgs ? false : isDowngrade} />
{/if}