diff --git a/src/lib/components/backupDatabaseAlert.svelte b/src/lib/components/backupDatabaseAlert.svelte
index 1c2ceed16f..46b44a41e8 100644
--- a/src/lib/components/backupDatabaseAlert.svelte
+++ b/src/lib/components/backupDatabaseAlert.svelte
@@ -1,6 +1,5 @@
{#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)}
- {@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
+ {@const areBackupsAvailable = $organization?.billingPlanDetails.backupsEnabled}
- {@const subtitle = isFreePlan
+ {@const subtitle = !areBackupsAvailable
? 'Upgrade your plan to ensure your data stays safe and backed up'
: 'Protect your data by quickly adding a backup policy'}
- {@const ctaText = isFreePlan ? 'Upgrade plan' : 'Create policy'}
- {@const ctaURL = isFreePlan ? $upgradeURL : `${page.url.pathname}/backups`}
+ {@const ctaText = !areBackupsAvailable ? 'Upgrade plan' : 'Create policy'}
+ {@const ctaURL = !areBackupsAvailable ? $upgradeURL : `${page.url.pathname}/backups`}
{subtitle}
@@ -35,7 +34,7 @@
href={ctaURL}
secondary
fullWidthMobile
- event={isFreePlan ? 'backup_banner_upgrade' : 'backup_banner_add'}>
+ event={!areBackupsAvailable ? 'backup_banner_upgrade' : 'backup_banner_add'}>
{ctaText}
diff --git a/src/lib/components/backupRestoreBox.svelte b/src/lib/components/backupRestoreBox.svelte
index f1de9d3bab..5210d20852 100644
--- a/src/lib/components/backupRestoreBox.svelte
+++ b/src/lib/components/backupRestoreBox.svelte
@@ -4,7 +4,7 @@
import { onMount } from 'svelte';
import { isCloud, isSelfHosted } from '$lib/system';
import { organization } from '$lib/stores/organization';
- import { BillingPlan, Dependencies } from '$lib/constants';
+ import { Dependencies } from '$lib/constants';
import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups';
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/state';
@@ -125,7 +125,7 @@
onMount(() => {
// fast path: don't subscribe if org is on a free plan or is self-hosted.
- if (isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE)) return;
+ if (isSelfHosted || (isCloud && !$organization?.billingPlanDetails.backupsEnabled)) return;
return realtime.forProject(page.params.region, 'console', (response) => {
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte
index 12605cded4..cb6e934b8d 100644
--- a/src/lib/components/billing/alerts/limitReached.svelte
+++ b/src/lib/components/billing/alerts/limitReached.svelte
@@ -2,26 +2,19 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
- import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { HeaderAlert } from '$lib/layout';
- import {
- hideBillingHeaderRoutes,
- readOnly,
- billingIdToPlan,
- upgradeURL
- } from '$lib/stores/billing';
+ import { hideBillingHeaderRoutes, readOnly, upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
-{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
+{#if $organization?.$id && !$organization?.billingPlanDetails.usage && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
+ title={`${$organization.name} usage has reached the ${$organization.billingPlanDetails.name} plan limit`}>
- Usage for the {$organization.name} organization has reached the limits of the {billingIdToPlan(
- $organization.billingPlan
- ).name}
+ Usage for the {$organization.name} organization has reached the limits of the {$organization
+ .billingPlanDetails.name}
plan. Consider upgrading to increase your resource usage.
diff --git a/src/lib/components/billing/alerts/missingPaymentMethod.svelte b/src/lib/components/billing/alerts/missingPaymentMethod.svelte
index 96c29c5d78..fd7f4b80ac 100644
--- a/src/lib/components/billing/alerts/missingPaymentMethod.svelte
+++ b/src/lib/components/billing/alerts/missingPaymentMethod.svelte
@@ -1,14 +1,13 @@
-{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes(page.url.pathname)}
+{#if $orgMissingPaymentMethod.billingPlanDetails.requiresPaymentMethod && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes(page.url.pathname)}
diff --git a/src/lib/components/billing/alerts/newDevUpgradePro.svelte b/src/lib/components/billing/alerts/newDevUpgradePro.svelte
index 733736f02a..7d38ec049c 100644
--- a/src/lib/components/billing/alerts/newDevUpgradePro.svelte
+++ b/src/lib/components/billing/alerts/newDevUpgradePro.svelte
@@ -2,7 +2,7 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
- import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
+ import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { activeHeaderAlert } from '$routes/(console)/store';
@@ -23,7 +23,7 @@
}
-{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes(base + '/account')}
+{#if show && $organization?.$id && !$organization?.billingPlanDetails.supportsCredits && !page.url.pathname.includes(base + '/account')}
;
export let billingBudget: number;
@@ -79,8 +79,8 @@
}
$: organizationId
- ? getUpdatePlanEstimate(organizationId, billingPlan, collaborators, couponData?.code)
- : getEstimate(billingPlan, collaborators, couponData?.code);
+ ? getUpdatePlanEstimate(organizationId, billingPlan.$id, collaborators, couponData?.code)
+ : getEstimate(billingPlan.$id, collaborators, couponData?.code);
{#if estimation}
@@ -103,6 +103,7 @@
{#if couponData?.status === 'active'}
{/if}
+
Total due
diff --git a/src/lib/components/billing/planComparisonBox.svelte b/src/lib/components/billing/planComparisonBox.svelte
index 80ae87f25e..7a71d2f01e 100644
--- a/src/lib/components/billing/planComparisonBox.svelte
+++ b/src/lib/components/billing/planComparisonBox.svelte
@@ -1,6 +1,5 @@
@@ -29,9 +48,9 @@
@@ -57,7 +76,7 @@
{#if $currentPlan && !currentPlanInList}
diff --git a/src/lib/components/billing/selectPlan.svelte b/src/lib/components/billing/selectPlan.svelte
index 2b9d9498fd..017d1730ea 100644
--- a/src/lib/components/billing/selectPlan.svelte
+++ b/src/lib/components/billing/selectPlan.svelte
@@ -1,15 +1,24 @@
{#if billingPlan}
@@ -19,10 +28,10 @@
diff --git a/src/lib/components/billing/usageRates.svelte b/src/lib/components/billing/usageRates.svelte
index 5e264dd294..0db4ac04e7 100644
--- a/src/lib/components/billing/usageRates.svelte
+++ b/src/lib/components/billing/usageRates.svelte
@@ -2,23 +2,18 @@
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
- import { plansInfo } from '$lib/stores/billing';
- import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers';
- import { BillingPlan } from '$lib/constants';
import { Table, Typography } from '@appwrite.io/pink-svelte';
- import type { Models } from '@appwrite.io/console';
+ import { BillingPlanGroup, type Models } from '@appwrite.io/console';
+ import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers';
export let show = false;
export let org: Models.Organization;
- let plan: Models.BillingPlan;
- $: plan = $plansInfo?.get(org.billingPlan);
-
$: nextDate = org?.name
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
: org?.billingNextInvoiceDate;
- $: isFree = org.billingPlan === BillingPlan.FREE;
+ $: isFree = org.billingPlanDetails.group === BillingPlanGroup.Starter;
// equal or above means unlimited!
const getCorrectSeatsCountValue = (count: number): string | number => {
@@ -28,22 +23,22 @@
};
function getPlanLimit(key: string): number | false {
- return plan[key] || false;
+ return org.billingPlanDetails[key] || false;
}
{#if isFree}
- Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
- Next billing period: {toLocaleDate(nextDate)}.
+ Usage on the {org.billingPlanDetails.name} plan is limited for the following resources. Next
+ billing period: {toLocaleDate(nextDate)}.
- {:else if org.billingPlan === BillingPlan.PRO}
+ {:else if org.billingPlanDetails.group === BillingPlanGroup.Pro}
Usage on the Pro plan will be charged at the end of each billing period at the following
rates. Next billing period: {toLocaleDate(nextDate)}.
- {:else if org.billingPlan === BillingPlan.SCALE}
+ {:else if org.billingPlanDetails.group === BillingPlanGroup.Scale}
Usage on the Scale plan will be charged at the end of each billing period at the
following rates. Next billing period: {toLocaleDate(nextDate)}.
@@ -57,7 +52,7 @@
LimitRate
- {#each Object.values(plan.addons) as addon}
+ {#each Object.values(org.billingPlanDetails.addons) as addon}
{addon.invoiceDesc}
@@ -70,7 +65,7 @@
{/if}
{/each}
- {#each Object.entries(plan.usage) as [key, usage]}
+ {#each Object.entries(org.billingPlanDetails.usage) as [key, usage]}
{@const limit = getPlanLimit(key)}
{@const show = limit !== false}
{#if show}
diff --git a/src/lib/components/bottomModalAlert.svelte b/src/lib/components/bottomModalAlert.svelte
index 623a510c19..37712efdac 100644
--- a/src/lib/components/bottomModalAlert.svelte
+++ b/src/lib/components/bottomModalAlert.svelte
@@ -10,8 +10,7 @@
} from '$lib/stores/bottom-alerts';
import { onMount } from 'svelte';
import { organization } from '$lib/stores/organization';
- import { BillingPlan } from '$lib/constants';
- import { upgradeURL } from '$lib/stores/billing';
+ import { canUpgrade, upgradeURL } from '$lib/stores/billing';
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
import { project } from '$routes/(console)/project-[region]-[project]/store';
import { page } from '$app/state';
@@ -142,7 +141,7 @@
// the button component cannot have both href and on:click!
function triggerWindowLink(alert: BottomModalAlertItem, event?: string) {
const alertAction = alert.cta;
- const shouldShowUpgrade = showUpgrade();
+ const shouldShowUpgrade = canUpgrade($organization?.billingPlanDetails);
// for correct event tracking after removal
const currentModalId = currentModalAlert.id;
@@ -170,26 +169,11 @@
});
}
- function showUpgrade() {
- const plan = currentModalAlert.plan;
- const organizationPlan = $organization?.billingPlan;
- switch (plan) {
- case 'free':
- return false;
- case 'pro':
- return organizationPlan === BillingPlan.FREE;
- case 'scale':
- return (
- organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
- );
- }
- }
-
onMount(addBottomModalAlerts);
{#if !isOnOnboarding && filteredModalAlerts.length > 0 && currentModalAlert}
- {@const shouldShowUpgrade = showUpgrade()}
+ {@const shouldShowUpgrade = canUpgrade($organization?.billingPlanDetails)}
diff --git a/src/lib/components/breadcrumbs.svelte b/src/lib/components/breadcrumbs.svelte
index 7f1a59c47c..57da33005f 100644
--- a/src/lib/components/breadcrumbs.svelte
+++ b/src/lib/components/breadcrumbs.svelte
@@ -25,7 +25,6 @@
import { ID, type Models, Query } from '@appwrite.io/console';
import { sdk } from '$lib/stores/sdk';
import { page } from '$app/state';
- import { BillingPlan } from '$lib/constants';
import { onDestroy } from 'svelte';
type Organization = {
@@ -254,7 +253,7 @@
let badgeType: 'success' | undefined;
$: badgeType =
- $organization && $organization.billingPlan !== BillingPlan.FREE ? 'success' : undefined;
+ $organization && $organization.billingPlanDetails.price > 0 ? 'success' : undefined;
diff --git a/src/lib/components/emptyCardImageCloud.svelte b/src/lib/components/emptyCardImageCloud.svelte
index 870edc4889..977d82e4f5 100644
--- a/src/lib/components/emptyCardImageCloud.svelte
+++ b/src/lib/components/emptyCardImageCloud.svelte
@@ -2,7 +2,7 @@
import { isSmallViewport } from '$lib/stores/viewport';
import { organization } from '$lib/stores/organization';
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
- import { getNextTierBillingPlan, billingIdToPlan } from '$lib/stores/billing';
+ import { getNextTierBillingPlan } from '$lib/stores/billing';
export let source = 'empty_state_card';
export let responsive = false;
@@ -26,7 +26,7 @@
-
+
diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte
index 9705442c47..fc97c9b257 100644
--- a/src/lib/components/organizationUsageLimits.svelte
+++ b/src/lib/components/organizationUsageLimits.svelte
@@ -1,7 +1,6 @@
@@ -11,7 +10,7 @@
{#if isCloud}
- {#if $organization?.billingPlan !== BillingPlan.FREE}
+ {#if $organization?.billingPlanDetails.supportsOrganizationRoles}
RolesOwner, Developer, Editor, Analyst and Billing.
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 33c900742a..2cb52605e3 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -609,17 +609,6 @@ export const eventServices: Array = [
}
];
-export enum BillingPlan {
- FREE = 'tier-0',
- PRO = 'tier-1',
- SCALE = 'tier-2',
- GITHUB_EDUCATION = 'auto-1',
- CUSTOM = 'cont-1',
- ENTERPRISE = 'ent-1'
-}
-
-export const BASE_BILLING_PLANS: string[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
-
export const feedbackDowngradeOptions = [
{
value: 'availableFeatures',
diff --git a/src/lib/helpers/program.ts b/src/lib/helpers/program.ts
new file mode 100644
index 0000000000..ead1fa9ecf
--- /dev/null
+++ b/src/lib/helpers/program.ts
@@ -0,0 +1,12 @@
+import { IconGithub } from '@appwrite.io/pink-icons-svelte';
+
+/**
+ * Models.BillingPlan > program
+ * |_ Models.Program > icon
+ * |_ icon > string
+ *
+ * So we need to map them as needed from Pink icons library
+ */
+export const IconsMap = {
+ github: IconGithub
+};
diff --git a/src/lib/layout/containerButton.svelte b/src/lib/layout/containerButton.svelte
index de3f0024cf..516eead977 100644
--- a/src/lib/layout/containerButton.svelte
+++ b/src/lib/layout/containerButton.svelte
@@ -1,17 +1,17 @@
@@ -95,11 +95,19 @@
.join(', ')}
{#if services.length}
-
- {#if $organization?.billingPlan !== BillingPlan.FREE && hasUsageFees}
+ {@const supportsUsage = Object.keys($currentPlan.usage).length > 0}
+
+ {#if !supportsUsage && hasUsageFees}
- You've reached the {services} limit for the {tier} plan.
+ You've reached the {services} limit for the {planName} plan.
($showUsageRatesModal = true)}
>Excess usage fees will apply.
@@ -108,7 +116,7 @@
{:else}
- You've reached the {services} limit for the {tier} plan. Upgrade your
@@ -132,7 +140,7 @@
on:click={() => (showDropdown = !showDropdown)}>
- {:else if $organization?.billingPlan !== BillingPlan.SCALE}
+ {:else}
{/if}
-
+
{#if hasProjectLimitation}
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 6db2db6975..95eb8fbfb3 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 { BillingPlansMap } from '$lib/sdk/billing';
import { isCloud } from '$lib/system';
@@ -24,10 +24,11 @@ import {
import { derived, get, writable } from 'svelte/store';
import { headerAlert } from './headerAlert';
import { addNotification, notifications } from './notifications';
-import { currentPlan, organization } from './organization';
+import { currentPlan } 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';
@@ -67,7 +68,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);
@@ -76,24 +78,49 @@ 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 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];
@@ -110,34 +137,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 =
@@ -229,7 +255,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':
@@ -244,11 +273,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
}
}
@@ -304,7 +334,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();
@@ -326,13 +357,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',
@@ -343,8 +374,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,
@@ -354,12 +388,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);
@@ -376,7 +412,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' },
@@ -384,7 +420,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);
@@ -403,10 +439,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,
@@ -418,7 +460,7 @@ export async function checkForUsageLimit(org: Models.Organization) {
method: () => {
goto(
resolve('/(console)/organization-[organization]/usage', {
- organization: org.$id
+ organization: organization.$id
})
);
}
@@ -428,7 +470,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, {
@@ -445,7 +487,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,
@@ -575,7 +617,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 @@