- {:else if selectedTab === BillingPlan.SCALE}
- Everything in the Pro plan, plus:
+ {:else}
-
Unlimited seats
-
Organization roles
-
SOC-2, HIPAA compliance
-
SSO Coming soon
-
Priority support
+
+ Limited to {currentPlan.databases}
+ {pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets}
+ {pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions}
+ {pluralize(currentPlan.functions, 'Function')} per project
+
+
Limited to 1 organization member
+
+ Limited to {currentPlan.bandwidth}GB bandwidth
+
+
+ Limited to {currentPlan.storage}GB storage
+
+
+ Limited to {formatNum(currentPlan.executions)} executions
+
{/if}
-
-
+ {:else if planHasGroup(selectedTab, BillingPlanGroup.Pro)}
+ Everything in the Free plan, plus:
+
+
Unlimited databases, buckets, functions
+
Unlimited seats
+
{currentPlan.bandwidth}GB bandwidth
+
{currentPlan.storage}GB storage
+
{formatNum(currentPlan.executions)} executions
+
Email support
+
+ {:else if planHasGroup(selectedTab, BillingPlanGroup.Scale)}
+ Everything in the Pro plan, plus:
+
+
Unlimited seats
+
Organization roles
+
SOC-2, HIPAA compliance
+
SSO Coming soon
+
Priority support
+
+ {/if}
+{/snippet}
diff --git a/src/lib/components/billing/planExcess.svelte b/src/lib/components/billing/planExcess.svelte
deleted file mode 100644
index 7bdf28e3f8..0000000000
--- a/src/lib/components/billing/planExcess.svelte
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-{#if showExcess}
-
- You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your
- billing period ends. After that,
- {#if excess?.members > 0}
- all team members except the owner will be removed,
- {:else}
- your organization will be limited to Free plan resources,
- {/if} and service disruptions may occur if usage exceeds Free plan limits.
-
-
-
-
- Resource
- Free limit
-
- Excess usage
- Metrics are estimates updated every 24 hours
-
-
-
- {#if excess?.members}
-
- Organization members
- {getServiceLimit('members', tier)} members
-
-
You are limited to {limit}
- {title.toLocaleLowerCase()} per project on the {tier} plan.
- {#if $organization?.billingPlan === BillingPlan.FREE}
You are limited to {limit}
- {title.toLocaleLowerCase()} per organization on the {tier} plan.
+ {title.toLocaleLowerCase()} per organization on the {planName} plan.
($showUsageRatesModal = true)}
>Excess usage fees will apply.
@@ -165,8 +179,8 @@
{:else}
You are limited to {limit}
- {title.toLocaleLowerCase()} per organization on the {tier} plan.
- {#if $organization?.billingPlan === BillingPlan.FREE}
+ {title.toLocaleLowerCase()} per organization on the {planName} plan.
+ {#if canUpgrade($organization.billingPlan)}
Upgrade
for additional {title.toLocaleLowerCase()}.
{/if}
diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte
index c24e9234aa..5117b5738b 100644
--- a/src/lib/layout/createProject.svelte
+++ b/src/lib/layout/createProject.svelte
@@ -4,15 +4,13 @@
import { CustomId } from '$lib/components/index.js';
import { getFlagUrl } from '$lib/helpers/flag';
import { isCloud } from '$lib/system.js';
- 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';
+ import { resolve } from '$app/paths';
let {
projectName = $bindable(),
@@ -20,7 +18,7 @@
regions = [],
region = $bindable(),
showTitle = true,
- billingPlan = undefined,
+ currentPlan = undefined,
projects = undefined,
submit
}: {
@@ -29,21 +27,24 @@
regions: Array;
region: string;
showTitle: boolean;
- billingPlan?: BillingPlan;
+ currentPlan?: Models.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
- );
- let isAddonProject = $derived(
- $currentPlan?.addons?.projects?.supported &&
+
+ const projectsLimited = $derived.by(() => {
+ return currentPlan?.projects > 0 && projects && projects >= currentPlan?.projects;
+ });
+
+ const isAddonProject = $derived.by(() => {
+ return (
+ currentPlan?.addons?.projects?.supported &&
projects &&
- projects >= $currentPlan?.addons?.projects?.planIncluded
- );
+ projects >= currentPlan?.addons?.projects?.planIncluded
+ );
+ });
@@ -61,7 +62,7 @@
0}
Region cannot be changed after creation
{/if}
+
{#if isAddonProject}
Each added project comes with its own dedicated pool of resources.
{/if}
+
{#if projectsLimited}
+ title={`You've reached your limit of ${currentPlan?.projects} projects`}>
Extra projects are available on paid plans for an additional fee
diff --git a/src/lib/layout/shell.svelte b/src/lib/layout/shell.svelte
index 193aa63335..c1034864dd 100644
--- a/src/lib/layout/shell.svelte
+++ b/src/lib/layout/shell.svelte
@@ -9,14 +9,12 @@
import { organization, organizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
- import { billingIdToPlan } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import SideNavigation from '$lib/layout/navigation.svelte';
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
import { isSidebarOpen, noWidthTransition } from '$lib/stores/sidebar';
- import { BillingPlan } from '$lib/constants';
import { page } from '$app/stores';
- import type { Models } from '@appwrite.io/console';
+ import { BillingPlanGroup, type Models } from '@appwrite.io/console';
import { getSidebarState, isInDatabasesRoute, updateSidebarState } from '$lib/helpers/sidebar';
import { isTabletViewport } from '$lib/stores/viewport';
@@ -156,13 +154,13 @@
.toString(),
organizations: $organizationList.teams.map((org) => {
- const billingPlan = org['billingPlan'];
+ const billingPlan = org['billingPlanDetails'] as Models.BillingPlan;
return {
name: org.name,
$id: org.$id,
- showUpgrade: billingPlan === BillingPlan.FREE,
- tierName: isCloud ? billingIdToPlan(billingPlan).name : null,
- isSelected: $organization?.$id === org.$id
+ isSelected: $organization?.$id === org.$id,
+ tierName: isCloud ? billingPlan.name : null,
+ showUpgrade: billingPlan.group === BillingPlanGroup.Starter
};
}),
diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts
index a920e77082..e5fe585c71 100644
--- a/src/lib/stores/billing.ts
+++ b/src/lib/stores/billing.ts
@@ -9,7 +9,7 @@ import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentM
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
-import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
+import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { cachedStore } from '$lib/helpers/cache';
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
import type { BillingPlansMap } from '$lib/sdk/billing';
@@ -25,10 +25,11 @@ import {
import { derived, get, writable } from 'svelte/store';
import { headerAlert } from './headerAlert';
import { addNotification, notifications } from './notifications';
-import { currentPlan, organization, type OrganizationError } from './organization';
+import { currentPlan, type OrganizationError } from './organization';
import { canSeeBilling } from './roles';
import { sdk } from './sdk';
import { user } from './user';
+
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
@@ -68,7 +69,8 @@ export const addressList = derived(
page,
($page) => $page.data.addressList as Models.BillingAddressList
);
-export const plansInfo = derived(page, ($page) => $page.data.plansInfo as BillingPlansMap);
+
+export const plansInfo = writable(new Map());
export const daysLeftInTrial = writable(0);
export const readOnly = writable(false);
@@ -77,24 +79,54 @@ export const showBudgetAlert = derived(
($page) => ($page.data.organization?.billingLimits.budgetLimit ?? 0) >= 100
);
+function makeBillingPlan(billingPlanOrId: string | Models.BillingPlan): Models.BillingPlan {
+ return typeof billingPlanOrId === 'string' ? billingIdToPlan(billingPlanOrId) : billingPlanOrId;
+}
+
export function getRoleLabel(role: string) {
return roles.find((r) => r.value === role)?.label ?? role;
}
-export function planHasGroup(billingPlanId: string, group: BillingPlanGroup) {
- const plansInfoStore = get(plansInfo);
- return plansInfoStore.get(billingPlanId)?.group === group;
+export function isGitHubEducationPlan(billingPlanOrId: string | Models.BillingPlan): boolean {
+ const billingPlan = makeBillingPlan(billingPlanOrId);
+ return !isStarterPlan(billingPlan) && billingPlan.price === 0;
+}
+
+export function isStarterPlan(billingPlanOrId: string | Models.BillingPlan): boolean {
+ const billingPlan = makeBillingPlan(billingPlanOrId);
+ return planHasGroup(billingPlan, BillingPlanGroup.Starter);
+}
+
+export function canUpgrade(billingPlanOrId: string | Models.BillingPlan): boolean {
+ const billingPlan = makeBillingPlan(billingPlanOrId);
+ const nextTier = getNextTierBillingPlan(billingPlan.$id);
+
+ // defaults back to PRO, so adjust the check!
+ return billingPlan.$id !== nextTier.$id;
+}
+
+export function canDowngrade(billingPlanOrId: string | Models.BillingPlan): boolean {
+ const billingPlan = makeBillingPlan(billingPlanOrId);
+ const nextTier = getPreviousTierBillingPlan(billingPlan.$id);
+
+ // defaults back to Starter, so adjust the check!
+ return billingPlan.$id !== nextTier.$id;
+}
+
+export function planHasGroup(
+ billingPlanOrId: string | Models.BillingPlan,
+ group: BillingPlanGroup
+): boolean {
+ const billingPlan = makeBillingPlan(billingPlanOrId);
+
+ return billingPlan?.group === group;
}
export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan {
const plansInfoStore = get(plansInfo);
- // hot fix for now, starter doesn't have a group atm.
- const correctBillingPlanGroup =
- billingPlanGroup === BillingPlanGroup.Starter ? null : billingPlanGroup;
-
const proPlans = Array.from(plansInfoStore.values()).filter(
- (plan) => plan.group === correctBillingPlanGroup
+ (plan) => plan.group === billingPlanGroup
);
return proPlans.sort((a, b) => a.order - b.order)[0];
@@ -111,34 +143,33 @@ export function billingIdToPlan(billingId: string): Models.BillingPlan {
}
}
-// TODO: @itznotabug - just return the BillingPlan object!
-export function getNextTierBillingPlan(tier: string): string {
- const currentPlanData = billingIdToPlan(tier);
+export function getNextTierBillingPlan(billingPlanId: string): Models.BillingPlan {
+ const currentPlanData = billingIdToPlan(billingPlanId);
const currentOrder = currentPlanData.order;
const plans = get(plansInfo);
for (const [, plan] of plans) {
+ // TODO: @itznotabug, check for group maybe?
if (plan.order === currentOrder + 1) {
- return plan.$id;
+ return plan;
}
}
- return getBasePlanFromGroup(BillingPlanGroup.Pro).$id;
+ return getBasePlanFromGroup(BillingPlanGroup.Pro);
}
-// TODO: @itznotabug - just return the BillingPlan object!
-export function getPreviousTierBillingPlan(tier: string): string {
- const currentPlanData = billingIdToPlan(tier);
+export function getPreviousTierBillingPlan(billingPlanId: string): Models.BillingPlan {
+ const currentPlanData = billingIdToPlan(billingPlanId);
const currentOrder = currentPlanData.order;
const plans = get(plansInfo);
for (const [, plan] of plans) {
if (plan.order === currentOrder - 1) {
- return plan.$id;
+ return plan;
}
}
- return getBasePlanFromGroup(BillingPlanGroup.Starter).$id;
+ return getBasePlanFromGroup(BillingPlanGroup.Starter);
}
export type PlanServices =
@@ -230,7 +261,10 @@ export const showUsageRatesModal = writable(false);
export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true);
export function checkForUsageFees(plan: string, id: PlanServices) {
- if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) {
+ const billingPlan = billingIdToPlan(plan);
+ const supportsUsage = Object.keys(billingPlan.usage).length > 0;
+
+ if (supportsUsage) {
switch (id) {
case 'bandwidth':
case 'storage':
@@ -245,11 +279,12 @@ export function checkForUsageFees(plan: string, id: PlanServices) {
} else return false;
}
-export function checkForProjectLimitation(id: PlanServices) {
- // Members are no longer limited on Pro and Scale plans (unlimited seats)
+export function checkForProjectLimitation(plan: string, id: PlanServices) {
if (id === 'members') {
- const currentTier = get(organization)?.billingPlan;
- if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) {
+ const billingPlan = billingIdToPlan(plan);
+ const hasUnlimitedProjects = billingPlan.projects === 0;
+
+ if (hasUnlimitedProjects) {
return false; // No project limitation for members on Pro/Scale plans
}
}
@@ -305,7 +340,8 @@ export function calculateEnterpriseTrial(org: Models.Organization) {
}
export function calculateTrialDay(org: Models.Organization) {
- if (org?.billingPlan === BillingPlan.FREE) return false;
+ if (!org.billingPlanDetails.trial) return false;
+
const endDate = new Date(org?.billingStartDate);
const today = new Date();
@@ -327,13 +363,13 @@ export async function checkForProjectsLimit(org: Models.Organization, orgProject
});
if (!plan) return;
- if (plan.$id !== BillingPlan.FREE) return;
if (!org.projects) return;
if (org.projects.length > 0) return;
const projectCount = orgProjectCount;
if (projectCount === undefined) return;
+ // not unlimited and current exceeds plan limits!
if (plan.projects > 0 && projectCount > plan.projects) {
headerAlert.add({
id: 'projectsLimitReached',
@@ -344,8 +380,11 @@ export async function checkForProjectsLimit(org: Models.Organization, orgProject
}
}
-export async function checkForUsageLimit(org: Models.Organization) {
- if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) {
+export async function checkForUsageLimit(organization: Models.Organization) {
+ if (
+ organization?.status === teamStatusReadonly &&
+ organization?.remarks === billingLimitOutstandingInvoice
+ ) {
headerAlert.add({
id: 'teamReadOnlyFailedInvoices',
component: TeamReadonlyAlert,
@@ -355,12 +394,14 @@ export async function checkForUsageLimit(org: Models.Organization) {
readOnly.set(true);
return;
}
- if (!org?.billingLimits && org?.status !== teamStatusReadonly) {
+
+ if (!organization?.billingLimits && organization?.status !== teamStatusReadonly) {
readOnly.set(false);
return;
}
- if (org?.billingPlan !== BillingPlan.FREE) {
- const { budgetLimit } = org?.billingLimits ?? {};
+
+ if (organization.billingPlanDetails.budgeting) {
+ const { budgetLimit } = organization?.billingLimits ?? {};
if (budgetLimit && budgetLimit >= 100) {
readOnly.set(false);
@@ -377,7 +418,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
}
// TODO: @itznotabug - check with @abnegate, what do we do here? this is billing!
- const { bandwidth, executions, storage, users } = org?.billingLimits ?? {};
+ const { bandwidth, executions, storage, users } = organization?.billingLimits ?? {};
const resources = [
{ value: bandwidth, name: 'bandwidth' },
{ value: executions, name: 'executions' },
@@ -385,7 +426,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
{ value: users, name: 'users' }
];
- const members = org.total;
+ const members = organization.total;
const memberLimit = getServiceLimit('members');
const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit);
@@ -404,10 +445,16 @@ export async function checkForUsageLimit(org: Models.Organization) {
if (now - lastNotification < 1000 * 60 * 60 * 24) return;
localStorage.setItem('limitReachedNotification', now.toString());
- let message = `${org.name} has reached 75% of the ${billingIdToPlan(BillingPlan.FREE).name} plan's ${resources.find((r) => r.value >= 75).name} limit. Upgrade to ensure there are no service disruptions.`;
- if (resources.filter((r) => r.value >= 75)?.length > 1) {
- message = `Usage for ${org.name} has reached 75% of the ${billingIdToPlan(BillingPlan.FREE).name} plan limit. Upgrade to ensure there are no service disruptions.`;
+
+ const threshold = 75;
+ const exceededResources = resources.filter((r) => r.value >= threshold);
+
+ let message = `${organization.name} has reached ${threshold}% of its ${exceededResources[0].name} limit. Upgrade to ensure there are no service disruptions.`;
+
+ if (exceededResources.length > 1) {
+ message = `Usage for ${organization.name} has reached ${threshold}% of its plan limits. Upgrade to ensure there are no service disruptions.`;
}
+
addNotification({
type: 'warning',
isHtml: true,
@@ -419,7 +466,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
method: () => {
goto(
resolve('/(console)/organization-[organization]/usage', {
- organization: org.$id
+ organization: organization.$id
})
);
}
@@ -429,7 +476,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
method: () => {
goto(
resolve('/(console)/organization-[organization]/change-plan', {
- organization: org.$id
+ organization: organization.$id
})
);
trackEvent(Click.OrganizationClickUpgrade, {
@@ -446,7 +493,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
}
export async function checkPaymentAuthorizationRequired(org: Models.Organization) {
- if (org.billingPlan === BillingPlan.FREE) return;
+ if (!org.billingPlanDetails.requiresPaymentMethod) return;
const invoices = await sdk.forConsole.organizations.listInvoices({
organizationId: org.$id,
@@ -576,7 +623,7 @@ export async function checkForMissingPaymentMethod() {
// Display upgrade banner for new users after 1 week for 30 days
export async function checkForNewDevUpgradePro(org: Models.Organization) {
// browser or plan check.
- if (!browser || org?.billingPlan !== BillingPlan.FREE) return;
+ if (!browser || !org.billingPlanDetails.supportsCredits) return;
// already dismissed by user!
if (localStorage.getItem('newDevUpgradePro')) return;
diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte
index 196d56b8a0..b09f0e59a8 100644
--- a/src/routes/(console)/+layout.svelte
+++ b/src/routes/(console)/+layout.svelte
@@ -1,6 +1,6 @@
-
+
{!areCreditsSupported ? 'Credits' : 'Available credit'}
@@ -172,7 +171,7 @@
{/if}
- {#if $organization?.billingPlan === BillingPlan.FREE}
+ {#if !areCreditsSupported}
{#if !currentPlan.budgeting}
Get notified by email when your organization meets a percentage of your budget cap. {billingIdToPlan(organization.billingPlan).name} organizations will receive one notification
- at 75% resource usage.
+ >{organization.billingPlanDetails.name} organizations will receive one notification at 75%
+ resource usage.
{:else}
Get notified by email when your organization meets or exceeds a percentage of your specified
billing alert(s).
@@ -148,7 +148,7 @@