diff --git a/package.json b/package.json
index 3104f604a4..02cb4f1718 100644
--- a/package.json
+++ b/package.json
@@ -22,11 +22,11 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
- "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428",
+ "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134",
"@appwrite.io/pink-icons": "0.25.0",
- "@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1",
+ "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0",
"@appwrite.io/pink-legacy": "^1.0.3",
- "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7",
+ "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0",
"@faker-js/faker": "^9.9.0",
"@popperjs/core": "^2.11.8",
"@sentry/sveltekit": "^8.38.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 28f2888f7f..a6ff6011a7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,20 +12,20 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
'@appwrite.io/console':
- specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428
- version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428
+ specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
+ version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
'@appwrite.io/pink-icons-svelte':
- specifier: ^2.0.0-RC.1
- version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)
+ specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0
+ version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3)
'@appwrite.io/pink-legacy':
specifier: ^1.0.3
version: 1.0.3
'@appwrite.io/pink-svelte':
- specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7
- version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3)
+ specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0
+ version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3)
'@faker-js/faker':
specifier: ^9.9.0
version: 9.9.0
@@ -260,8 +260,8 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
- '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428':
- resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428}
+ '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134':
+ resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -269,8 +269,8 @@ packages:
peerDependencies:
svelte: ^4.0.0
- '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9':
- resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9}
+ '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0':
+ resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0}
version: 2.0.0-RC.1
peerDependencies:
svelte: ^4.0.0
@@ -284,8 +284,8 @@ packages:
'@appwrite.io/pink-legacy@1.0.3':
resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==}
- '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7':
- resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7}
+ '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0':
+ resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0}
version: 2.0.0-RC.2
peerDependencies:
svelte: ^4.0.0
@@ -3700,13 +3700,13 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
- '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428': {}
+ '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
- '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)':
+ '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3)':
dependencies:
svelte: 5.25.3
@@ -3719,7 +3719,7 @@ snapshots:
'@appwrite.io/pink-icons': 1.0.0
the-new-css-reset: 1.11.3
- '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3)':
+ '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3)':
dependencies:
'@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3)
'@floating-ui/dom': 1.6.13
diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte
new file mode 100644
index 0000000000..3ebb938c10
--- /dev/null
+++ b/src/lib/components/archiveProject.svelte
@@ -0,0 +1,292 @@
+
+
+{#if projectsToArchive.length > 0}
+
+
+
+ These projects have been archived and are read-only. You can view and migrate their
+ data.
+
+
+
+
+ {#each projectsToArchive as project}
+ {@const platforms = filterPlatforms(
+ project.platforms.map((platform) => getPlatformInfo(platform.type))
+ )}
+ {@const formatted = formatName(project.name)}
+
+
+ {project?.platforms?.length ? project?.platforms?.length : 'No'} apps
+
+ {formatted}
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ readOnlyInfoOpen = {
+ ...readOnlyInfoOpen,
+ [project.$id]: !readOnlyInfoOpen[project.$id]
+ };
+ }}>
+
+ Read only
+
+
+
+
+ Archived projects are read-only. You can view
+ and migrate their data, but they no longer
+ accept edits or requests.
+
+
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ toggle(e);
+ }}>
+
+
+
+ handleUnarchiveProject(project)}
+ >Unarchive project
+ handleMigrateProject(project)}
+ >Migrate project
+
+
+
+
+
+ {#each platforms.slice(0, 2) as platform}
+ {@const icon = getIconForPlatform(platform.icon)}
+
+
+
+ {/each}
+
+ {#if platforms.length > 3}
+
+ {/if}
+
+
+ {#if isCloud && $regionsStore?.regions}
+ {@const region = findRegion(project)}
+ {region?.name}
+ {/if}
+
+
+ {/each}
+
+
+
+
+{/if}
+
+
+
+ Are you sure you want to unarchive {projectToUnarchive?.name} ?
+ This will move the project back to your active projects list.
+
+
+
+ Cancel
+ Unarchive
+
+
+
+
+
diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte
index 11d832107c..4885db75e1 100644
--- a/src/lib/components/billing/alerts/limitReached.svelte
+++ b/src/lib/components/billing/alerts/limitReached.svelte
@@ -20,9 +20,14 @@
plan. Consider upgrading to increase your resource usage.
-
- View usage
-
+ {#if !page.data.currentPlan?.usagePerProject}
+
+ View usage
+
+ {/if}
{
diff --git a/src/lib/components/billing/alerts/projectsLimit.svelte b/src/lib/components/billing/alerts/projectsLimit.svelte
index 8d7b10b331..c6d8d9f5d2 100644
--- a/src/lib/components/billing/alerts/projectsLimit.svelte
+++ b/src/lib/components/billing/alerts/projectsLimit.svelte
@@ -3,12 +3,8 @@
import { Click } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
- import {
- billingProjectsLimitDate,
- hideBillingHeaderRoutes,
- upgradeURL
- } from '$lib/stores/billing';
- import { currentPlan } from '$lib/stores/organization';
+ import { hideBillingHeaderRoutes, upgradeURL } from '$lib/stores/billing';
+ import { currentPlan, organization } from '$lib/stores/organization';
import SelectProjectCloud from './selectProjectCloud.svelte';
import { toLocaleDate } from '$lib/helpers/date';
@@ -31,8 +27,9 @@
type="warning"
title="Action required: You have more than {$currentPlan.projects} projects.">
- Choose which projects to keep before {toLocaleDate(billingProjectsLimitDate)} or upgrade
- to Pro. Projects over the limit will be blocked after this date.
+ Choose which projects to keep before {toLocaleDate(
+ $organization.billingNextInvoiceDate
+ )} or upgrade to Pro. Projects over the limit will be blocked after this date.
1 ? `${difference} projects` : `${difference} project`}
+ title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}>
{@html formatProjectsToArchive()}
will be archived.
diff --git a/src/lib/components/billing/planComparisonBox.svelte b/src/lib/components/billing/planComparisonBox.svelte
index 9d7443df43..c15e483177 100644
--- a/src/lib/components/billing/planComparisonBox.svelte
+++ b/src/lib/components/billing/planComparisonBox.svelte
@@ -90,6 +90,7 @@
Everything in the Free plan, plus:
Unlimited databases, buckets, functions
+ Unlimited seats
{plan.bandwidth}GB bandwidth
{plan.storage}GB storage
{formatNum(plan.executions)} executions
diff --git a/src/lib/components/billing/planExcess.svelte b/src/lib/components/billing/planExcess.svelte
index 02793dc708..7bdf28e3f8 100644
--- a/src/lib/components/billing/planExcess.svelte
+++ b/src/lib/components/billing/planExcess.svelte
@@ -1,16 +1,22 @@
-
-
- {#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
-
- {/if}
-
-
- {tierFree.description}
-
-
- {formatCurrency(freePlan?.price ?? 0)}
-
-
-
-
- {#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
-
- {/if}
-
-
- {tierPro.description}
-
-
- {formatCurrency(proPlan?.price ?? 0)} per month + usage
-
-
-
-
- {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
-
- {/if}
-
-
- {tierScale.description}
-
-
- {formatCurrency(scalePlan?.price ?? 0)} per month + usage
-
-
- {#if $currentPlan && !isBasePlan}
+ {#each plans as plan}
+
+
+ {#if $organization?.billingPlan === plan.$id && !isNewOrg}
+
+ {/if}
+
+
+ {plan.desc}
+
+
+ {@const isZeroPrice = (plan.price ?? 0) <= 0}
+ {@const price = formatCurrency(plan.price ?? 0)}
+ {isZeroPrice ? price : `${price} per month + usage`}
+
+
+ {/each}
+ {#if $currentPlan && !currentPlanInList}
{
- // php int max is always larger than js
- const exceedsSafeLimit = count >= Number.MAX_SAFE_INTEGER;
- return exceedsSafeLimit ? 'Unlimited' : count || 0;
+ // Check for Infinity or very large numbers
+ const isUnlimited = count === Infinity || !isWithinSafeRange(count);
+ return isUnlimited ? 'Unlimited' : count || 0;
};
function getPlanLimit(key: string): number | false {
diff --git a/src/lib/components/estimatedCard.svelte b/src/lib/components/estimatedCard.svelte
new file mode 100644
index 0000000000..8c935aa37d
--- /dev/null
+++ b/src/lib/components/estimatedCard.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/gridItem1.svelte b/src/lib/components/gridItem1.svelte
index 89b2d3bb22..40c8e66d9e 100644
--- a/src/lib/components/gridItem1.svelte
+++ b/src/lib/components/gridItem1.svelte
@@ -1,6 +1,6 @@
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts
index 63e079b9c7..47afdf41ad 100644
--- a/src/lib/components/index.ts
+++ b/src/lib/components/index.ts
@@ -84,5 +84,6 @@ 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';
export { default as EmailVerificationBanner } from './alerts/emailVerificationBanner.svelte';
export { default as SortButton, type SortDirection } from './sortButton.svelte';
diff --git a/src/lib/components/labelCard.svelte b/src/lib/components/labelCard.svelte
index 9eb761198e..2cd95edc95 100644
--- a/src/lib/components/labelCard.svelte
+++ b/src/lib/components/labelCard.svelte
@@ -45,7 +45,7 @@
-
+
+ import { Button } from '$lib/elements/forms';
+ import { getServiceLimit, plansInfo } from '$lib/stores/billing';
+ import { BillingPlan } from '$lib/constants';
+ import { Click, trackEvent } from '$lib/actions/analytics';
+ import { Badge, Icon, Layout, Table, Typography, Tooltip } from '@appwrite.io/pink-svelte';
+ import { IconArrowUp, IconInfo } from '@appwrite.io/pink-icons-svelte';
+
+ import { formatNumberWithCommas } from '$lib/helpers/numbers';
+ import { Modal } from '$lib/components';
+ import { Alert } from '@appwrite.io/pink-svelte';
+ import { addNotification } from '$lib/stores/notifications';
+ import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
+ import { organization, type Organization } from '$lib/stores/organization';
+ import type { Models } from '@appwrite.io/console';
+
+ // Props
+ type Props = {
+ organization: Organization;
+ projects?: Models.Project[];
+ members?: Models.Membership[];
+ storageUsage?: number;
+ };
+
+ const { projects = [], members = [], storageUsage = 0 }: Props = $props();
+
+ let showSelectProject = $state(false);
+ let selectedProjects = $state([]);
+ let error = $state(null);
+ let showSelectionReminder = $state(false);
+
+ // Derived state using runes
+ let freePlanLimits = $derived({
+ projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
+ members: getServiceLimit('members', BillingPlan.FREE),
+ storage: getServiceLimit('storage', BillingPlan.FREE)
+ });
+
+ // When preparing to downgrade to Free, enforce Free plan limit locally (2)
+ let allowedProjectsToKeep = $derived(freePlanLimits.projects);
+
+ let currentUsage = $derived({
+ projects: projects?.length || 0,
+ members: members?.length || 0,
+ storage: storageUsage || 0
+ });
+
+ let storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024));
+
+ let isLimitExceeded = $derived({
+ projects: currentUsage.projects > freePlanLimits.projects,
+ members: currentUsage.members > freePlanLimits.members,
+ storage: storageUsageGB > freePlanLimits.storage
+ });
+
+ let excessUsage = $derived({
+ projects: Math.max(0, currentUsage.projects),
+ members: Math.max(0, currentUsage.members - freePlanLimits.members),
+ storage: Math.max(0, storageUsageGB - freePlanLimits.storage)
+ });
+
+ // projects that would be archived with the current selection
+ let projectsToArchive = $derived(
+ projects.filter((project) => !selectedProjects.includes(project.$id))
+ );
+
+ function formatProjectsToArchive(): string {
+ let result = '';
+ projectsToArchive.forEach((project, index) => {
+ const isLast = index === projectsToArchive.length - 1;
+ const isSecondLast = index === projectsToArchive.length - 2;
+
+ result += `${index === 0 ? '' : ' '}${project.name}`;
+
+ if (!isLast) {
+ if (isSecondLast) result += ' and';
+ else result += ',';
+ }
+ });
+ return result;
+ }
+
+ function formatNumber(num: number): string {
+ return formatNumberWithCommas(num);
+ }
+
+ function handleManageProjects() {
+ showSelectProject = true;
+ showSelectionReminder = false;
+ trackEvent(Click.OrganizationClickUpgrade, { source: 'usage_limits_manage_projects' });
+ }
+
+ // Expose validation for parent to call before submitting downgrade
+ export function validateOrAlert(): boolean {
+ const filteredSelection = selectedProjects.filter((id) =>
+ projects.some((p) => p.$id === id)
+ );
+ const isValid = filteredSelection.length === allowedProjectsToKeep;
+ showSelectionReminder = !isValid && isLimitExceeded.projects;
+ return isValid;
+ }
+
+ export function getSelectedProjects(): string[] {
+ return selectedProjects.filter((id) => projects.some((p) => p.$id === id));
+ }
+
+ function updateSelected() {
+ error = null;
+ const filteredSelection = selectedProjects.filter((id) =>
+ projects.some((p) => p.$id === id)
+ );
+ if (filteredSelection.length !== allowedProjectsToKeep) {
+ error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`;
+ return;
+ }
+ // Keep selection locally; parent flow will apply after plan change
+ showSelectProject = false;
+ showSelectionReminder = false;
+ addNotification({ type: 'success', message: `Projects selected for archiving` });
+ }
+
+
+
+ {#if showSelectionReminder}
+
+ The Free plan lets you keep {allowedProjectsToKeep} projects. Select them before continuing.
+
+ Manage projects
+
+
+ {/if}
+
+
+
+
+ Resource
+ Free limit
+
+
+ Excess usage
+
+
+ Usage beyond the Free plan limits.
+
+
+
+
+
+
+
+
+
+
+ Projects
+ {#if isLimitExceeded.projects}
+
+ {/if}
+
+
+
+ {formatNumber(allowedProjectsToKeep)} projects
+
+
+ {#if isLimitExceeded.projects}
+
+
+
+ {formatNumber(excessUsage.projects)} projects
+
+
+ {:else}
+
+ {formatNumber(currentUsage.projects)} / {formatNumber(
+ allowedProjectsToKeep
+ )}
+
+ {/if}
+
+
+ {#if isLimitExceeded.projects}
+
+
+ Manage projects
+
+
+ {/if}
+
+
+
+
+
+
+ Organization members
+
+
+ {formatNumber(freePlanLimits.members)} member
+
+
+ {#if isLimitExceeded.members}
+
+
+
+ {formatNumber(excessUsage.members)} members
+
+
+ {:else}
+
+ {formatNumber(currentUsage.members)} / {formatNumber(
+ freePlanLimits.members
+ )}
+
+ {/if}
+
+
+
+
+
+
+
+ Storage
+
+
+ {freePlanLimits.storage} GB
+
+
+ {#if isLimitExceeded.storage}
+
+
+
+ {excessUsage.storage.toFixed(2)} GB
+
+
+ {:else}
+
+ {storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB
+
+ {/if}
+
+
+
+
+
+
+
+
+
+ Choose which two projects to keep. Projects over the limit will be blocked after this date.
+
+
+ {#if error}
+ {error}
+ {/if}
+
+
+
+
+ Project Name
+ Created
+
+ {#each projects as project}
+
+ {project.name}
+
+ {toLocaleDateTime(project.$createdAt)}
+
+
+ {/each}
+
+
+ {#if selectedProjects.length === allowedProjectsToKeep}
+ {@const difference = projects.length - selectedProjects.length}
+ {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`}
+
+ {formatProjectsToArchive()} will be archived
+
+ {/if}
+
+
+ (showSelectProject = false)}>Cancel
+ Save
+
+
+
+
diff --git a/src/lib/components/progressbar/ProgressBar.svelte b/src/lib/components/progressbar/ProgressBar.svelte
index 58b3a3a394..a4cebc8eed 100644
--- a/src/lib/components/progressbar/ProgressBar.svelte
+++ b/src/lib/components/progressbar/ProgressBar.svelte
@@ -66,6 +66,7 @@
flex-direction: row;
gap: 2px;
margin-top: 1rem;
+ overflow: hidden;
}
&__content {
diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts
index c138a7483d..91dd9a37cb 100644
--- a/src/lib/helpers/string.ts
+++ b/src/lib/helpers/string.ts
@@ -49,6 +49,19 @@ export function formatNum(number: number): string {
return formatter.format(number);
}
+/**
+ * Format a string with optional mobile-aware truncation.
+ */
+export function formatName(
+ name: string,
+ limit: number = 19,
+ isSmallViewport: boolean = false
+): string {
+ const mobileLimit = 16;
+ const actualLimit = isSmallViewport ? mobileLimit : limit;
+ return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-';
+}
+
/**
* Returns a regex to check hostname validity. Supports wildcards too!
*/
diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte
index 5d951babd3..52915971d8 100644
--- a/src/lib/layout/createProject.svelte
+++ b/src/lib/layout/createProject.svelte
@@ -4,13 +4,15 @@
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';
+ import { formatCurrency } from '$lib/helpers/numbers';
let {
projectName = $bindable(''),
@@ -18,6 +20,7 @@
regions = [],
region = $bindable(''),
showTitle = true,
+ billingPlan = undefined,
projects = undefined,
submit
}: {
@@ -26,13 +29,17 @@
regions: Array;
region: string;
showTitle: boolean;
+ billingPlan?: BillingPlan;
projects?: number;
submit?: Snippet;
} = $props();
let showCustomId = $state(false);
+ let isProPlan = $derived((billingPlan ?? $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
);
@@ -46,26 +53,12 @@
{#if showTitle}
Create your project
{/if}
- {#if projectsLimited}
-
- Extra projects are available on paid plans for an additional fee
-
- Upgrade
-
-
- {/if}
+
+
{#if isCloud && regions.length > 0}
Region cannot be changed after creation
{/if}
+ {#if projectsLimited}
+ {#if isProPlan}
+
+ Each added project comes with its own dedicated pool of resources.
+
+ {:else}
+
+ Extra projects are available on paid plans for an additional fee
+
+ Upgrade
+
+
+ {/if}
+ {/if}
{@render submit?.()}
diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts
index 3dbd0b9f9d..7ae79e199f 100644
--- a/src/lib/sdk/billing.ts
+++ b/src/lib/sdk/billing.ts
@@ -151,6 +151,79 @@ export type CreditList = {
total: number;
};
+export type AggregationTeam = {
+ $id: string;
+ /**
+ * Aggregation creation time in ISO 8601 format.
+ */
+ $createdAt: string;
+ /**
+ * Aggregation update date in ISO 8601 format.
+ */
+ $updatedAt: string;
+ /**
+ * Beginning date of the invoice.
+ */
+ from: string;
+ /**
+ * End date of the invoice.
+ */
+ to: string;
+ /**
+ * Total amount of the invoice.
+ */
+ amount: number;
+ additionalMembers: number;
+
+ /**
+ * Price for additional members
+ */
+ additionalMemberAmount: number;
+ /**
+ * Total storage usage.
+ */
+ usageStorage: number;
+ /**
+ * Total active users for the billing period.
+ */
+ usageUsers: number;
+ /**
+ * Total number of executions for the billing period.
+ */
+ usageExecutions: number;
+ /**
+ * Total bandwidth usage for the billing period.
+ */
+ usageBandwidth: number;
+ /**
+ * Total realtime usage for the billing period.
+ */
+ usageRealtime: number;
+ /**
+ * Usage logs for the billing period.
+ */
+ resources: InvoiceUsage[];
+ /**
+ * Aggregation billing plan
+ */
+ plan: string;
+ breakdown: AggregationBreakdown[];
+};
+
+export type AggregationBreakdown = {
+ $id: string;
+ name: string;
+ amount: number;
+ region: string;
+ resources: InvoiceUsage[];
+};
+
+export type InvoiceUsage = {
+ resourceId: string;
+ value: number;
+ amount: number;
+};
+
export type AvailableCredit = {
available: number;
};
@@ -316,10 +389,13 @@ export type Plan = {
projects: number;
databases: number;
databasesAllowEncrypt: boolean;
+ databasesReads: number;
+ databasesWrites: number;
buckets: number;
fileSize: number;
functions: number;
executions: number;
+ GBHours: number;
realtime: number;
logs: number;
authPhone: number;
@@ -330,9 +406,14 @@ export type Plan = {
realtime: AdditionalResource;
storage: AdditionalResource;
users: AdditionalResource;
+ databasesReads: AdditionalResource;
+ databasesWrites: AdditionalResource;
+ GBHours: AdditionalResource;
+ imageTransformations: AdditionalResource;
};
addons: {
seats: PlanAddon;
+ projects: PlanAddon;
};
trialDays: number;
budgetCapEnabled: boolean;
@@ -348,6 +429,7 @@ export type Plan = {
supportsOrganizationRoles: boolean;
buildSize: number; // in MB
deploymentSize: number; // in MB
+ usagePerProject: boolean;
};
export type PlanList = {
@@ -355,7 +437,7 @@ export type PlanList = {
total: number;
};
-export type PlansMap = Map;
+export type PlansMap = Map;
export type Roles = {
scopes: string[];
@@ -492,6 +574,22 @@ export class Billing {
});
}
+ async listPlans(queries: string[] = []): Promise {
+ const path = `/console/plans`;
+ const uri = new URL(this.client.config.endpoint + path);
+ const params = {
+ queries
+ };
+ return await this.client.call(
+ 'get',
+ uri,
+ {
+ 'content-type': 'application/json'
+ },
+ params
+ );
+ }
+
async getPlan(planId: string): Promise {
const path = `/console/plans/${planId}`;
const uri = new URL(this.client.config.endpoint + path);
@@ -835,7 +933,7 @@ export class Billing {
);
}
- async getAggregation(organizationId: string, aggregationId: string): Promise {
+ async getAggregation(organizationId: string, aggregationId: string): Promise {
const path = `/organizations/${organizationId}/aggregations/${aggregationId}`;
const params = {
organizationId,
diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts
index 993e01fbe7..2a88fa83af 100644
--- a/src/lib/stores/billing.ts
+++ b/src/lib/stores/billing.ts
@@ -14,7 +14,7 @@ import { cachedStore } from '$lib/helpers/cache';
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
import type {
AddressesList,
- Aggregation,
+ AggregationTeam,
Invoice,
InvoiceList,
PaymentList,
@@ -69,7 +69,6 @@ export const roles = [
export const teamStatusReadonly = 'readonly';
export const billingLimitOutstandingInvoice = 'outstanding_invoice';
-export const billingProjectsLimitDate = '2025-09-01';
export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList);
export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList);
@@ -162,8 +161,13 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan
// the correct info for members/seats, resides in `addons`.
// plan > addons > seats/others
if (serviceId === 'members') {
- // some don't include `limit`, so we fallback!
- return plan?.['addons']['seats']['limit'] ?? 1;
+ // pro and scale plans have unlimited seats (per-project NEW pricing model)
+ const currentTier = tier ?? get(organization)?.billingPlan;
+ if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) {
+ return Infinity; // unlimited seats for Pro and Scale plans
+ }
+ // Free plan still has 1 member limit
+ return (plan?.['addons']['seats'] || [])['limit'] ?? 1;
}
return plan?.[serviceId] ?? 0;
@@ -235,6 +239,7 @@ export const tierEnterprise: TierData = {
};
export const showUsageRatesModal = writable(false);
+export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true);
export function checkForUsageFees(plan: Tier, id: PlanServices) {
if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) {
@@ -253,11 +258,19 @@ export function checkForUsageFees(plan: Tier, id: PlanServices) {
}
export function checkForProjectLimitation(id: PlanServices) {
+ // Members are no longer limited on Pro and Scale plans (unlimited seats)
+ if (id === 'members') {
+ const currentTier = get(organization)?.billingPlan;
+ if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) {
+ return false; // No project limitation for members on Pro/Scale plans
+ }
+ }
+
switch (id) {
case 'databases':
case 'functions':
case 'buckets':
- case 'members':
+ case 'members': // Only applies to Free plan now
case 'platforms':
case 'webhooks':
case 'teams':
@@ -325,7 +338,8 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?:
if (!plan) return;
if (plan.$id !== BillingPlan.FREE) return;
- if (org.projects?.length > 0) return;
+ if (!org.projects) return;
+ if (org.projects.length > 0) return;
const projectCount = orgProjectCount;
if (projectCount === undefined) return;
@@ -383,13 +397,8 @@ export async function checkForUsageLimit(org: Organization) {
];
const members = org.total;
- const plan = get(currentPlan);
- const membersOverflow =
- // `plan` can be null on `onboarding/create-organization` route.
- // nested null checks needed: GitHub Education plan have empty addons.
- members > plan?.addons?.seats?.limit
- ? members - (plan?.addons?.seats?.limit || members)
- : 0;
+ const memberLimit = getServiceLimit('members');
+ const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit);
if (resources.some((r) => r.value >= 100) || membersOverflow > 0) {
readOnly.set(true);
@@ -609,7 +618,7 @@ export const billingURL = derived(
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
-export function calculateExcess(addon: Aggregation, plan: Plan) {
+export function calculateExcess(addon: AggregationTeam, plan: Plan) {
return {
bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth),
storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'),
diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts
index 5aa188f90d..3cf1478a3a 100644
--- a/src/routes/(console)/+layout.ts
+++ b/src/routes/(console)/+layout.ts
@@ -1,7 +1,7 @@
+import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
-import { Dependencies } from '$lib/constants';
import type { Tier } from '$lib/stores/billing';
import type { Plan, PlanList } from '$lib/sdk/billing';
import { Query } from '@appwrite.io/console';
diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts
index 690baa2bbe..57d6280fe4 100644
--- a/src/routes/(console)/create-organization/+page.ts
+++ b/src/routes/(console)/create-organization/+page.ts
@@ -7,10 +7,10 @@ import type { Organization } from '$lib/stores/organization';
export const load: PageLoad = async ({ url, parent, depends }) => {
const { organizations } = await parent();
depends(Dependencies.ORGANIZATIONS);
-
- const [coupon, paymentMethods] = await Promise.all([
+ const [coupon, paymentMethods, plans] = await Promise.all([
getCoupon(url),
- sdk.forConsole.billing.listPaymentMethods()
+ sdk.forConsole.billing.listPaymentMethods(),
+ sdk.forConsole.billing.listPlans()
]);
let plan = getPlanFromUrl(url);
const hasFreeOrganizations = organizations.teams?.some(
@@ -24,6 +24,7 @@ export const load: PageLoad = async ({ url, parent, depends }) => {
return {
plan,
coupon,
+ plans,
hasFreeOrganizations,
paymentMethods,
name: url.searchParams.get('name') ?? ''
diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte
index 10169fe819..2339498ec3 100644
--- a/src/routes/(console)/organization-[organization]/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/+page.svelte
@@ -7,6 +7,7 @@
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
import { page } from '$app/state';
import { registerCommands } from '$lib/commandCenter';
+ import { formatName as formatNameHelper } from '$lib/helpers/string';
import {
CardContainer,
Empty,
@@ -17,7 +18,7 @@
} from '$lib/components';
import { trackEvent, Click } from '$lib/actions/analytics';
import { type Models } from '@appwrite.io/console';
- import { billingProjectsLimitDate, readOnly, upgradeURL } from '$lib/stores/billing';
+ import { readOnly, upgradeURL } from '$lib/stores/billing';
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
@@ -34,8 +35,9 @@
} from '@appwrite.io/pink-icons-svelte';
import { getPlatformInfo } from '$lib/helpers/platform';
import CreateProjectCloud from './createProjectCloud.svelte';
- import { currentPlan, regions as regionsStore } from '$lib/stores/organization';
+ import { currentPlan, organization, regions as regionsStore } from '$lib/stores/organization';
import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte';
+ import ArchiveProject from '$lib/components/archiveProject.svelte';
import { toLocaleDate } from '$lib/helpers/date';
export let data;
@@ -99,24 +101,16 @@
function isSetToArchive(project: Models.Project): boolean {
if (!isCloud) return false;
- if (data.organization.projects?.length === 0) return false;
if (!project || !project.$id) return false;
- return !data.organization.projects?.includes(project.$id);
+ return project.status !== 'active';
}
- function formatName(name: string, limit: number = 19) {
- const mobileLimit = 16;
- const actualLimit = $isSmallViewport ? mobileLimit : limit;
- return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-';
- }
+ $: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active');
+ $: activeProjects = data.projects.projects.filter((project) => project.status === 'active');
function clearSearch() {
searchQuery?.clearInput();
}
-
- $: projectsToArchive = data.projects.projects.filter(
- (project) => !data.organization.projects?.includes(project.$id)
- );
- {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > $currentPlan.projects && $canWriteProjects}
+ {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)}
+ {@const difference = projectsToArchive}
+ {@const messagePrefix =
+ difference.length > 1 ? `${difference} projects` : `${difference} project`}
-
- {#each projectsToArchive as project, index}{@const text = `${project.name} `}
- {@html text}{index === projectsToArchive.length - 2
- ? ', and '
- : index < projectsToArchive.length - 1
- ? ', '
- : ''}
- {/each}
- will be archived
-
+ title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}>
+ Upgrade your plan to restore archived projects
- (showSelectProject = true)}>
- Manage projects
-
{
@@ -171,18 +157,18 @@
{/if}
- {#if data.projects.total}
+ {#if activeProjects.length > 0}
- {#each data.projects.projects as project}
+ {#each activeProjects as project}
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{@const formatted = isSetToArchive(project)
- ? formatName(project.name)
+ ? formatNameHelper(project.name, isSmallViewport ? 19 : 25)
: project.name}
@@ -259,9 +245,14 @@
name="Projects"
limit={data.limit}
offset={data.offset}
- total={data.projects.total} />
-
+ total={activeProjects.length} />
+
+
+
{/if}
- Billing
-
+ {#if $useNewPricingModal}
+
+ {:else}
+
+ {/if}
-
+
Apply Appwrite credits to your organization.
diff --git a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte
index 1bb41f4f28..52bc768599 100644
--- a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte
+++ b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte
@@ -7,7 +7,6 @@
import { wizard } from '$lib/stores/wizard';
import { Query } from '@appwrite.io/console';
import { onMount } from 'svelte';
- import AddCreditWizard from './addCreditWizard.svelte';
import { Button } from '$lib/elements/forms';
import AddCreditModal from './addCreditModal.svelte';
import { formatCurrency } from '$lib/helpers/numbers';
@@ -33,15 +32,6 @@
const limit = 5;
- function handleCredits() {
- if ($organization?.paymentMethodId || $organization?.backupPaymentMethodId) {
- show = true;
- } else {
- wizard.start(AddCreditWizard);
- reloadOnWizardClose = true;
- }
- }
-
async function request() {
if (!$organization?.$id) return;
@@ -116,7 +106,7 @@
content={formatCurrency(creditList.available)} />
{#if creditList?.total}
-
+ (show = true)}>
Add credits
@@ -176,7 +166,7 @@
{/if}
{:else}
- Add credits
+ (show = true)}>Add credits
{/if}
{/if}
diff --git a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte
index 44c9c97102..ac649dd837 100644
--- a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte
+++ b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte
@@ -28,7 +28,12 @@
}
-
+
Your organization is set to change to
{tierToPlan($organization?.billingPlanDowngrade).name}
diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
index 3f27119c9a..51c39a931b 100644
--- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
+++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
@@ -1,180 +1,533 @@
{#if $organization}
-
- Payment estimates
- A breakdown of your estimated upcoming payment for the current billing period. Totals displayed
- exclude accumulated credits and applicable taxes.
-
-
- Due at: {toLocaleDate($organization?.billingNextInvoiceDate)}
-
-
-
-
-
- {currentPlan.name} plan
-
-
- {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
- ? formatCurrency(0)
- : currentPlan
- ? formatCurrency(currentPlan?.price)
- : ''}
-
-
-
- {#if currentPlan.budgeting && extraUsage > 0}
- 0
- ? currentInvoice.usage.length + 1
- : currentInvoice.usage.length
- ).toString()}>
-
- {formatCurrency(extraUsage >= 0 ? extraUsage : 0)}
-
-
- {#if currentAggregation.additionalMembers}
-
-
- Additional members
-
- {formatCurrency(
- currentAggregation.additionalMemberAmount
- )}
-
-
-
- {currentAggregation.additionalMembers}
-
-
- {/if}
- {#if currentInvoice?.usage}
- {#each currentInvoice.usage as excess, i}
- {#if i > 0 || currentAggregation.additionalMembers}
-
- {/if}
-
-
-
-
- {excess.name}
-
-
- {formatCurrency(excess.amount)}
-
-
-
-
-
- {formatNumberWithCommas(excess.value)}
-
- {abbreviateNumber(excess.value)}
-
-
-
- {/each}
+
+ {currentPlan.name} plan
+
+ {#if totalAmount > 0}
+
+ Next payment of {formatCurrency(totalAmount)}
+ will occur on
+ {toLocaleDate($organization?.billingNextInvoiceDate)} .
+
+ {/if}
+
+
+
+
+
+ {#each billingData as row}
+
+ {#each columns as col}
+ root.toggle(row.id)}>
+ {#if col.id === 'item'}
+
+
+ {row.cells?.[col.id] ?? ''}
+
+
+ {:else}
+
+ {row.cells?.[col.id] ?? ''}
+
{/if}
-
-
- {/if}
+
+ {/each}
+
+
+ {#if row.children}
+ {#each row.children as child (child.id)}
+
+ {#each columns as col}
+
+ {:else}
+
+ {child.cells?.[col.id] ?? ''}
+
+ {/if}
+
+ {/each}
+
+ {/each}
+ {/if}
+
+
+ {/each}
+ {#if availableCredit > 0}
+
+ {}}>
+
+
- {#if currentPlan.supportsCredits && availableCredit > 0}
-
-
-
Credits to be applied
+ >Credits
-
- -{formatCurrency(
- Math.min(availableCredit, currentInvoice?.amount ?? 0)
- )}
-
-
- {/if}
-
- {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
-
-
-
-
- Current total (USD)
-
-
-
- Estimates are updated daily and may differ from your
- final invoice.
-
-
-
+
+ {}}>
+
-
- {formatCurrency(
- Math.max(
- (currentInvoice?.amount ?? 0) -
- Math.min(availableCredit, currentInvoice?.amount ?? 0),
- 0
- )
- )}
+
+ {}}>
+
+ -{formatCurrency(creditsApplied)}
-
- {/if}
-
-
-
-
+
+
+ {/if}
+
+
+ {}}>
+
+ Total
+
+
+ {}}>
+
+
+
+ {}}>
+
+ {formatCurrency(totalAmount)}
+
+
+
+
+
+
+
+
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
-
- View estimated usage
-
+ class="u-flex u-cross-center u-gap-8 u-flex-wrap u-width-full-line u-main-end actions-mobile">
+ {#if !currentPlan?.usagePerProject}
+
+ View estimated usage
+
+ {/if}
{:else}
+ class="u-flex u-cross-center u-gap-8 u-flex-wrap u-width-full-line u-main-end actions-mobile">
{#if $organization?.billingPlanDowngrade !== null}
(showCancel = true)}>Cancel change
{:else}
@@ -204,20 +557,24 @@
Change plan
{/if}
-
- View estimated usage
-
+ {#if !currentPlan?.usagePerProject}
+
+ View estimated usage
+
+ {/if}
{/if}
-
-
+
+
{/if}
diff --git a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte
index ca84dbf213..338be255e2 100644
--- a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte
+++ b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte
@@ -17,9 +17,14 @@
Appwrite services, update the budget limit.
-
- View usage
-
+ {#if !page.data.currentPlan?.usagePerProject}
+
+ View usage
+
+ {/if}
Update limit
diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
index b5ef36e437..934ddf3b5e 100644
--- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte
@@ -4,7 +4,6 @@
import { page } from '$app/state';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { PlanComparisonBox, PlanSelection, SelectPaymentMethod } from '$lib/components/billing';
- import PlanExcess from '$lib/components/billing/planExcess.svelte';
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants';
import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms';
@@ -33,7 +32,11 @@
import { onMount } from 'svelte';
import { loadAvailableRegions } from '$routes/(console)/regions';
import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte';
+ import OrganizationUsageLimits from '$lib/components/organizationUsageLimits.svelte';
import { Query } from '@appwrite.io/console';
+ import type { OrganizationUsage } from '$lib/sdk/billing';
+ import type { Models } from '@appwrite.io/console';
+ import { toLocaleDate } from '$lib/helpers/date';
export let data;
@@ -43,6 +46,9 @@
let previousPage: string = base;
let showExitModal = false;
let formComponent: Form;
+ let usageLimitsComponent:
+ | { validateOrAlert: () => boolean; getSelectedProjects: () => string[] }
+ | undefined;
let isSubmitting = writable(false);
let collaborators: string[] =
data?.members?.memberships
@@ -55,6 +61,8 @@
let showCreditModal = false;
let feedbackDowngradeReason: string;
let feedbackMessage: string;
+ let orgUsage: OrganizationUsage;
+ let allProjects: { projects: Models.Project[] } | undefined;
$: paymentMethods = null;
@@ -91,6 +99,21 @@
selectedPlan =
$currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
+
+ try {
+ orgUsage = await sdk.forConsole.billing.listUsage(data.organization.$id);
+ } catch {
+ orgUsage = undefined;
+ }
+
+ try {
+ allProjects = await sdk.forConsole.projects.list([
+ Query.equal('teamId', data.organization.$id),
+ Query.limit(1000)
+ ]);
+ } catch {
+ allProjects = { projects: [] };
+ }
});
async function loadPaymentMethods() {
@@ -100,6 +123,13 @@
async function handleSubmit() {
if (isDowngrade) {
+ // If target plan has a non-zero project limit, ensure selection made
+ const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0;
+ if (targetProjectsLimit > 0 && usageLimitsComponent?.validateOrAlert) {
+ const ok = usageLimitsComponent.validateOrAlert();
+ if (!ok) return;
+ }
+
await downgrade();
} else if (isUpgrade) {
await upgrade();
@@ -136,6 +166,7 @@
async function downgrade() {
try {
+ // 1) update the plan first
await sdk.forConsole.billing.updatePlan(
data.organization.$id,
selectedPlan,
@@ -143,6 +174,22 @@
null
);
+ // 2) If the target plan has a project limit, apply selected projects now
+ const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0;
+ if (targetProjectsLimit > 0 && usageLimitsComponent) {
+ const selected = usageLimitsComponent.getSelectedProjects();
+ if (selected?.length) {
+ try {
+ await sdk.forConsole.billing.updateSelectedProjects(
+ data.organization.$id,
+ selected
+ );
+ } catch (projectError) {
+ console.warn('Project selection failed after plan update:', projectError);
+ }
+ }
+ }
+
await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]);
await goto(previousPage);
@@ -308,14 +355,12 @@
{/if}
{#if isDowngrade}
- {#if selectedPlan === BillingPlan.FREE && !data.hasFreeOrgs}
-
- {:else if selectedPlan === BillingPlan.PRO && data.organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0}
- {@const extraMembers = collaborators?.length ?? 0}
- {@const price = formatCurrency(
- extraMembers *
- ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
- )}
+ {@const extraMembers = collaborators?.length ?? 0}
+ {@const price = formatCurrency(
+ extraMembers *
+ ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
+ )}
+ {#if selectedPlan === BillingPlan.PRO}
Your monthly payments will be adjusted for the Pro plan
@@ -325,7 +370,26 @@
>you will be charged {price} monthly for {extraMembers} team members.
This will be reflected in your next invoice.
+ {:else if selectedPlan === BillingPlan.FREE}
+
+ You will retain access to {tierToPlan($organization.billingPlan)
+ .name} plan features until your billing period ends. After that,
+ all team members except the owner will be removed,
+ and service disruptions may occur if usage exceeds Free plan limits.
+
{/if}
+
+
{/if}
diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts
index ecb712008f..c6a137f96a 100644
--- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts
+++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts
@@ -1,11 +1,20 @@
import type { PageLoad } from './$types';
import type { Organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
+import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async ({ depends, parent }) => {
const { members, currentPlan, organizations } = await parent();
depends(Dependencies.UPGRADE_PLAN);
+ let plans;
+ try {
+ plans = await sdk.forConsole.billing.listPlans();
+ } catch (error) {
+ console.error('Failed to load billing plans:', error);
+ plans = { plans: {} };
+ }
+
let plan: BillingPlan;
if (currentPlan?.$id === BillingPlan.SCALE) {
@@ -22,6 +31,7 @@ export const load: PageLoad = async ({ depends, parent }) => {
return {
members,
plan,
+ plans,
selfService,
hasFreeOrgs
};
diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte
index f114dcc22f..1a81ae48cd 100644
--- a/src/routes/(console)/organization-[organization]/header.svelte
+++ b/src/routes/(console)/organization-[organization]/header.svelte
@@ -66,7 +66,11 @@
event: 'usage',
title: 'Usage',
hasChildren: true,
- disabled: !(isCloud && ($isOwner || $isBilling))
+ disabled: !(
+ isCloud &&
+ ($isOwner || $isBilling) &&
+ !page.data.currentPlan?.usagePerProject
+ )
},
{
href: `${path}/billing`,
diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte
index 4a9f16b99c..05c3500802 100644
--- a/src/routes/(console)/organization-[organization]/members/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/members/+page.svelte
@@ -47,9 +47,7 @@
// Calculate if button should be disabled and tooltip should show
$: memberCount = data.organizationMembers?.total ?? 0;
$: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1;
- $: isButtonDisabled = isCloud
- ? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported
- : false;
+ $: isButtonDisabled = isCloud ? isFreeWithMembers : false;
const resend = async (member: Models.Membership) => {
try {
diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
index e273536246..a9422df60b 100644
--- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
+++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte
@@ -8,7 +8,8 @@
getServiceLimit,
showUsageRatesModal,
type Tier,
- upgradeURL
+ upgradeURL,
+ useNewPricingModal
} from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import ProjectBreakdown from './ProjectBreakdown.svelte';
@@ -76,14 +77,32 @@
{#if $organization.billingPlan === BillingPlan.SCALE}
On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per
- resource listed below. ($showUsageRatesModal = true)}
- >Learn more
+ resource listed below.
+ {#if $useNewPricingModal}
+ ($showUsageRatesModal = true)}>Learn more
+ {:else}
+
+ Learn more
+
+ {/if}
{:else if $organization.billingPlan === BillingPlan.PRO}
On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per
- resource listed below. ($showUsageRatesModal = true)}
- >Learn more
+ resource listed below.
+ {#if $useNewPricingModal}
+ ($showUsageRatesModal = true)}>Learn more
+ {:else}
+
+ Learn more
+
+ {/if}
{:else if $organization.billingPlan === BillingPlan.FREE}
diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
index 24b82ae7b8..31fc890179 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
+++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte
@@ -243,7 +243,7 @@
Executions
- Calculated for all functions that are executed in all projects in your project.
+ Calculated for all functions that are executed in this project.
{#if executions}
{@const current = formatNum(executionsTotal)}
diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
index 4afa9181ad..e5e1ee5a6c 100644
--- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
+++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
@@ -1,4 +1,4 @@
-import type { Aggregation, Invoice } from '$lib/sdk/billing';
+import type { AggregationTeam, Invoice, InvoiceUsage } from '$lib/sdk/billing';
import { accumulateUsage } from '$lib/sdk/usage';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
@@ -11,7 +11,7 @@ export const load: PageLoad = async ({ params, parent }) => {
let startDate: string = organization.billingCurrentInvoiceDate;
let endDate: string = organization.billingNextInvoiceDate;
let currentInvoice: Invoice = undefined;
- let currentAggregation: Aggregation = undefined;
+ let currentAggregation: AggregationTeam = undefined;
if (invoice) {
currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice);
@@ -22,6 +22,15 @@ export const load: PageLoad = async ({ params, parent }) => {
startDate = currentInvoice.from;
endDate = currentInvoice.to;
+ } else {
+ try {
+ currentAggregation = await sdk.forConsole.billing.getAggregation(
+ organization.$id,
+ organization.billingAggregationId
+ );
+ } catch (e) {
+ // ignore error if no aggregation found
+ }
}
const [invoices, usage] = await Promise.all([
@@ -29,10 +38,24 @@ export const load: PageLoad = async ({ params, parent }) => {
sdk.forProject(region, project).project.getUsage({ startDate, endDate })
]);
- if (invoice) {
- usage.usersTotal = currentAggregation.usageUsers;
- usage.executionsTotal = currentAggregation.usageExecutions;
- usage.filesStorageTotal = currentAggregation.usageStorage;
+ if (currentAggregation) {
+ let projectSpecificData = null;
+ if (currentAggregation.breakdown) {
+ projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project);
+ }
+
+ if (projectSpecificData) {
+ const executionsResource = projectSpecificData.resources?.find?.(
+ (r: InvoiceUsage) => r.resourceId === 'executions'
+ );
+ if (executionsResource) {
+ usage.executionsTotal = executionsResource.value || usage.executionsTotal;
+ }
+ } else {
+ usage.usersTotal = currentAggregation.usageUsers;
+ usage.executionsTotal = currentAggregation.usageExecutions;
+ usage.filesStorageTotal = currentAggregation.usageStorage;
+ }
}
usage.users = accumulateUsage(usage.users, usage.usersTotal);