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 @@ Limit Rate - {#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} Roles Owner, 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} diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index ba61eccca1..96d8bd446b 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -1,19 +1,13 @@

Usage - {#if $organization?.billingPlan === BillingPlan.FREE} + {#if isStarterPlan(currentBillingPlan)} {/if}
- {#if $organization.billingPlan === BillingPlan.SCALE} + {#if planHasGroup(currentBillingPlan, BillingPlanGroup.Scale)}

On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. @@ -87,7 +91,7 @@ {/if}

- {:else if $organization.billingPlan === BillingPlan.PRO} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Pro)}

On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per resource listed below. @@ -102,7 +106,7 @@ {/if}

- {:else if $organization.billingPlan === BillingPlan.FREE} + {:else if isStarterPlan(currentBillingPlan)}

If you exceed the limits of the Free plan, services for your organization's projects may be disrupted. diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte index 90219042af..570e5f7003 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/totalMembers.svelte @@ -1,10 +1,7 @@ Members The number of members in your organization. - {#if $organization.billingPlan !== BillingPlan.FREE} + {#if !organizationMembersSupported}

@@ -37,11 +36,7 @@ - You can add unlimited organization members on the {billingIdToPlan( - $organization.billingPlan - ).name} plan {$organization.billingPlan === BillingPlan.PRO - ? `for ${formatCurrency(plan.addons.seats.price)} each per billing period.` - : '.'} + You can add unlimited organization members on paid plans. @@ -53,8 +48,7 @@
- - {#snippet children(paginatedItems: typeof members.memberships)} + {#snippet children(paginatedItems)} Members diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte index 9b29e6da15..77377918f2 100644 --- a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte +++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/details.svelte @@ -1,9 +1,8 @@
Usage - {#if $organization?.billingPlan === BillingPlan.FREE} + + {#if planHasGroup(currentBillingPlan, BillingPlanGroup.Starter)} {/if}
+
- {#if $organization.billingPlan === BillingPlan.SCALE} + {#if planHasGroup(currentBillingPlan, BillingPlanGroup.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 about plan usage limits.

- {:else if $organization.billingPlan === BillingPlan.PRO} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.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 about plan usage limits.

- {:else if $organization.billingPlan === BillingPlan.FREE} + {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Starter)}

- If you exceed the limits of the {plan} plan, services for your projects may be disrupted. + If you exceed the limits of the {currentBillingPlan.name} plan, services for your projects + may be disrupted. Upgrade for greater capacity. diff --git a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte index 0e8b26ceff..9a6996dfbb 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/create-site/configuration.svelte @@ -186,7 +186,7 @@ items={variables} limit={6} hideFooter={variables.length <= 6}> - {#snippet children(paginatedItems: typeof variables)} + {#snippet children(paginatedItems)} - {#snippet children(paginatedItems: typeof data.templates)} + {#snippet children(paginatedItems)} {#each paginatedItems as template (template.name)} {@const templateFrameworks = template.frameworks.map((t) => t.name)} diff --git a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte index dadbd653b8..d6d4746cfd 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/site-[site]/instantRollbackModal.svelte @@ -128,7 +128,7 @@ (prodDeployment) => prodDeployment.$id !== deployment.$id )} - {#snippet children(paginatedItems: typeof items)} + {#snippet children(paginatedItems)} {#each paginatedItems as prodDeployment} - {#if isCloud && $organization.billingPlan === BillingPlan.FREE} + + + {@const isStarter = isStarterPlan($organization.billingPlan)} + {#if isCloud && isStarter} - Upgrade to Pro or Scale to adjust your CPU and RAM beyond the default. + Upgrade your plan to adjust your CPU and RAM beyond the default. diff --git a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte index d0d4a0b0d6..b4642cf6b6 100644 --- a/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte +++ b/src/routes/(console)/project-[region]-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte @@ -2,15 +2,14 @@ import { Click, Submit, trackEvent } from '$lib/actions/analytics'; import { CardGrid } from '$lib/components'; import { Alert } from '@appwrite.io/pink-svelte'; - import { BillingPlan } from '$lib/constants'; import { Button, Form, InputNumber, InputSelect } from '$lib/elements/forms'; import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion'; import { createByteUnitPair } from '$lib/helpers/unit'; - import { readOnly, upgradeURL } from '$lib/stores/billing'; + import { isStarterPlan, readOnly, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; import { updateBucket } from './+page.svelte'; - import type { Models } from '@appwrite.io/console'; + import { type Models } from '@appwrite.io/console'; export let bucket: Models.Bucket; export let currentPlan: Models.BillingPlan | null; @@ -45,7 +44,9 @@ {#if isCloud} {@const size = humanFileSize(sizeToBytes(service, 'MB', 1000))} - {#if $organization?.billingPlan === BillingPlan.FREE} + + {@const isStarter = isStarterPlan($organization.billingPlan)} + {#if isStarter} The {currentPlan.name} plan has a maximum upload file size limit of {Math.floor( parseInt(size.value) diff --git a/src/routes/(public)/functions/deploy/+page.ts b/src/routes/(public)/functions/deploy/+page.ts index 92476a1256..0310a581c1 100644 --- a/src/routes/(public)/functions/deploy/+page.ts +++ b/src/routes/(public)/functions/deploy/+page.ts @@ -2,12 +2,12 @@ import { sdk } from '$lib/stores/sdk.js'; import { redirect } from '@sveltejs/kit'; import { base } from '$app/paths'; import { isCloud } from '$lib/system'; -import { BillingPlan } from '$lib/constants'; -import { ID } from '@appwrite.io/console'; +import { BillingPlanGroup, ID } from '@appwrite.io/console'; import { getTeamOrOrganizationList } from '$lib/stores/organization'; import { redirectTo } from '$routes/store'; import type { PageLoad } from './$types'; import { getRepositoryInfo } from '$lib/helpers/github'; +import { getBasePlanFromGroup } from '$lib/stores/billing'; export const load: PageLoad = async ({ parent, url }) => { const { account } = await parent(); @@ -72,7 +72,7 @@ export const load: PageLoad = async ({ parent, url }) => { await sdk.forConsole.organizations.create( ID.unique(), 'Personal Projects', - BillingPlan.FREE, + getBasePlanFromGroup(BillingPlanGroup.Starter).$id, null ); } else { diff --git a/src/routes/(public)/sites/deploy/+page.ts b/src/routes/(public)/sites/deploy/+page.ts index 3e68c8fcd3..391a4325d1 100644 --- a/src/routes/(public)/sites/deploy/+page.ts +++ b/src/routes/(public)/sites/deploy/+page.ts @@ -1,13 +1,13 @@ import { sdk } from '$lib/stores/sdk.js'; -import { redirect, error } from '@sveltejs/kit'; +import { error, redirect } from '@sveltejs/kit'; import { base } from '$app/paths'; import { isCloud } from '$lib/system'; -import { BillingPlan } from '$lib/constants'; -import { ID, type Models } from '@appwrite.io/console'; +import { BillingPlanGroup, ID, type Models } from '@appwrite.io/console'; import { getTeamOrOrganizationList } from '$lib/stores/organization'; import { redirectTo } from '$routes/store'; import type { PageLoad } from './$types'; import { getRepositoryInfo } from '$lib/helpers/github'; +import { getBasePlanFromGroup } from '$lib/stores/billing'; export const load: PageLoad = async ({ parent, url }) => { const { account } = await parent(); @@ -88,7 +88,7 @@ export const load: PageLoad = async ({ parent, url }) => { await sdk.forConsole.organizations.create({ organizationId: ID.unique(), name: 'Personal Projects', - billingPlan: BillingPlan.FREE + billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id }); } else { await sdk.forConsole.teams.create({