From 47fe4f63d258800ee148f3b53f8489460101596f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 15 Jun 2025 08:29:28 +0000 Subject: [PATCH 01/69] Project changes: fetch plan values --- .../components/billing/planSelection.svelte | 87 ++++++------------- src/lib/sdk/billing.ts | 14 ++- src/lib/stores/billing.ts | 4 +- src/routes/(console)/+layout.ts | 11 ++- .../(console)/create-organization/+page.ts | 6 +- 5 files changed, 51 insertions(+), 71 deletions(-) diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index db11d298f..b69e3aa7e 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -1,79 +1,46 @@ - - - {#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg} - - {/if} - - - {tierFree.description} - - - {formatCurrency(freePlan?.price ?? 0)} - - - - - {#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg} - - {/if} - - - {tierPro.description} - - - {formatCurrency(proPlan?.price ?? 0)} per month + usage - - - - - {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg} - - {/if} - - - {tierScale.description} - - - {formatCurrency(scalePlan?.price ?? 0)} per month + usage - - + {#each plans as plan} + + + {#if $organization?.billingPlan === plan.$id && !isNewOrg} + + {/if} + + + {plan.desc} + + + {@const isZeroPrice = (plan.price ?? 0) <= 0} + {@const price = formatCurrency(plan.price ?? 0)} + {isZeroPrice ? price : `${price} per month + usage`} + + + {/each} {#if $currentPlan && !isBasePlan} ; +export type PlansMap = Map; export type Roles = { scopes: string[]; @@ -487,6 +488,17 @@ export class Billing { }); } + async listPlans(queries: string[] = []): Promise { + const path = `/console/plans`; + const uri = new URL(this.client.config.endpoint + path); + const params = { + queries, + } + return await this.client.call('get', uri, { + 'content-type': 'application/json' + }, params); + } + async getPlan(planId: string): Promise { const path = `/console/plans/${planId}`; const uri = new URL(this.client.config.endpoint + path); diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index c68f336c1..46000eb6c 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -161,7 +161,7 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan // plan > addons > seats/others if (serviceId === 'members') { // some don't include `limit`, so we fallback! - return plan?.['addons']['seats']['limit'] ?? 1; + return (plan?.['addons']['seats'] || [])['limit'] ?? 1; } return plan?.[serviceId] ?? 0; @@ -357,7 +357,7 @@ export async function checkForUsageLimit(org: Organization) { const members = org.total; const plan = get(currentPlan); const membersOverflow = - members > plan.addons.seats.limit ? members - (plan.addons.seats.limit || members) : 0; + members > plan.addons.seats?.limit ? members - (plan.addons.seats.limit || members) : 0; if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index 249914fc8..24d9855d0 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -1,6 +1,5 @@ import { Dependencies } from '$lib/constants'; import type { Plan } from '$lib/sdk/billing'; -import type { Tier } from '$lib/stores/billing'; import { sdk } from '$lib/stores/sdk'; import { isCloud } from '$lib/system'; import type { LayoutLoad } from './$types'; @@ -25,13 +24,13 @@ export const load: LayoutLoad = async ({ params, fetch, depends, parent }) => { const [data, variables] = await Promise.all([versionPromise, variablesPromise]); - let plansInfo = new Map(); + let plansInfo = new Map(); if (isCloud) { - const plansArray = await sdk.forConsole.billing.getPlansInfo(); - plansInfo = plansArray.plans.reduce((map, plan) => { - map.set(plan.$id as Tier, plan); + const plansArray = await sdk.forConsole.billing.listPlans(); + plansInfo = Object.values(plansArray.plans).reduce((map, plan) => { + map.set(plan.$id, plan); return map; - }, new Map()); + }, new Map()); } const organizations = !isCloud diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts index 150cd76f3..6767d0e4f 100644 --- a/src/routes/(console)/create-organization/+page.ts +++ b/src/routes/(console)/create-organization/+page.ts @@ -7,9 +7,10 @@ import type { Organization } from '$lib/stores/organization'; export const load: PageLoad = async ({ url, parent, depends }) => { depends(Dependencies.CREATE_ORGANIZATION); const { organizations } = await parent(); - const [coupon, paymentMethods] = await Promise.all([ + const [coupon, paymentMethods, plans] = await Promise.all([ getCoupon(url), - sdk.forConsole.billing.listPaymentMethods() + sdk.forConsole.billing.listPaymentMethods(), + sdk.forConsole.billing.listPlans() ]); let plan = getPlanFromUrl(url); const hasFreeOrganizations = organizations.teams?.some( @@ -22,6 +23,7 @@ export const load: PageLoad = async ({ url, parent, depends }) => { return { plan, coupon, + plans, hasFreeOrganizations, paymentMethods, name: url.searchParams.get('name') ?? '' From 6de72265b6aac95ece835668bf4158095042da49 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 16 Jun 2025 03:33:12 +0000 Subject: [PATCH 02/69] plan summary improvement --- src/lib/sdk/billing.ts | 81 +++++++++++++++++-- .../billing/planSummary.svelte | 74 +++++++---------- 2 files changed, 104 insertions(+), 51 deletions(-) diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 3147b6aaa..e4a6b2786 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -151,6 +151,70 @@ export type CreditList = { total: number; }; +export type AggregationTeam = { + $id: string; + /** + * Aggregation creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Aggregation update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Beginning date of the invoice. + */ + from: string; + /** + * End date of the invoice. + */ + to: string; + /** + * Total amount of the invoice. + */ + amount: number; + additionalMembers: number; + + /** + * Price for additional members + */ + additionalMemberAmount: number; + /** + * Total storage usage. + */ + usageStorage: number; + /** + * Total active users for the billing period. + */ + usageUsers: number; + /** + * Total number of executions for the billing period. + */ + usageExecutions: number; + /** + * Total bandwidth usage for the billing period. + */ + usageBandwidth: number; + /** + * Total realtime usage for the billing period. + */ + usageRealtime: number; + /** + * Usage logs for the billing period. + */ + resources: InvoiceUsage[]; + /** + * Aggregation billing plan + */ + plan: string; +}; + +export type InvoiceUsage = { + resourceId: string; + value: number; + amount: number; +}; + export type Aggregation = { $id: string; /** @@ -492,11 +556,16 @@ export class Billing { const path = `/console/plans`; const uri = new URL(this.client.config.endpoint + path); const params = { - queries, - } - return await this.client.call('get', uri, { - 'content-type': 'application/json' - }, params); + queries + }; + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); } async getPlan(planId: string): Promise { @@ -823,7 +892,7 @@ export class Billing { ); } - async getAggregation(organizationId: string, aggregationId: string): Promise { + async getAggregation(organizationId: string, aggregationId: string): Promise { const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; const params = { organizationId, diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index f35163025..bc60e538e 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -5,7 +5,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { plansInfo, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing'; + import type { Aggregation, AggregationTeam, CreditList, Invoice, Plan } from '$lib/sdk/billing'; import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; @@ -20,14 +20,20 @@ } from '@appwrite.io/pink-svelte'; import { IconInfo, IconTag } from '@appwrite.io/pink-icons-svelte'; import CancelDowngradeModel from './cancelDowngradeModal.svelte'; + import { onMount } from 'svelte'; export let currentPlan: Plan; export let creditList: CreditList; export let currentInvoice: Invoice | undefined = undefined; - export let currentAggregation: Aggregation | undefined = undefined; + export let currentAggregation: AggregationTeam | undefined = undefined; let showCancel: boolean = false; + onMount(() => { + console.log(currentAggregation); + console.log(currentInvoice); + }); + const availableCredit = creditList.available; const today = new Date(); const isTrial = @@ -64,63 +70,41 @@ 0 - ? currentInvoice.usage.length + 1 - : currentInvoice.usage.length - ).toString()}> + badge={currentAggregation.resources + .filter((r) => r.amount && r.amount > 0) + .length.toString()}> {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - {#if currentAggregation.additionalMembers} + {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i} + {#if i > 0} + + {/if} + - Additional members + + {excess.resourceId} + - {formatCurrency( - currentAggregation.additionalMemberAmount - )} + {formatCurrency(excess.amount)} - {currentAggregation.additionalMembers} + + + {formatNumberWithCommas(excess.value)} + + {abbreviateNumber(excess.value)} + - {/if} - {#if currentInvoice?.usage} - {#each currentInvoice.usage as excess, i} - {#if i > 0 || currentAggregation.additionalMembers} - - {/if} - - - - - {excess.name} - - - {formatCurrency(excess.amount)} - - - - - - {formatNumberWithCommas(excess.value)} - - {abbreviateNumber(excess.value)} - - - - {/each} - {/if} + {/each} {/if} From 60b61fecd3211c1b2a223ec067c42f4956c3278f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 16 Jun 2025 03:51:24 +0000 Subject: [PATCH 03/69] remove logs --- .../billing/planSummary.svelte | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index bc60e538e..8c936a736 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -5,7 +5,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { plansInfo, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { Aggregation, AggregationTeam, CreditList, Invoice, Plan } from '$lib/sdk/billing'; + import type {AggregationTeam, CreditList, Invoice, Plan } from '$lib/sdk/billing'; import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; @@ -20,7 +20,6 @@ } from '@appwrite.io/pink-svelte'; import { IconInfo, IconTag } from '@appwrite.io/pink-icons-svelte'; import CancelDowngradeModel from './cancelDowngradeModal.svelte'; - import { onMount } from 'svelte'; export let currentPlan: Plan; export let creditList: CreditList; @@ -29,11 +28,6 @@ let showCancel: boolean = false; - onMount(() => { - console.log(currentAggregation); - console.log(currentInvoice); - }); - const availableCredit = creditList.available; const today = new Date(); const isTrial = From 449fe5a7f8dee1f758fcd3dddeaf9a47704caee5 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 16 Jun 2025 05:22:47 +0000 Subject: [PATCH 04/69] fix plan selection --- src/lib/components/billing/planSelection.svelte | 4 ++-- .../organization-[organization]/change-plan/+page.svelte | 7 ++++--- .../organization-[organization]/change-plan/+page.ts | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index b69e3aa7e..d4842102f 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -13,8 +13,8 @@ export let isNewOrg = false; export let selfService = true; - $: isBasePlan = BASE_BILLING_PLANS.includes($currentPlan?.$id); $: plans = Object.values(page.data.plans.plans) as Plan[]; + $: currentPlanInList = plans.filter((plan) => plan.$id === $currentPlan?.$id).length > 0; @@ -41,7 +41,7 @@ {/each} - {#if $currentPlan && !isBasePlan} + {#if $currentPlan && !currentPlanInList} $currentPlan?.order; - $: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan?.order; + $: isUpgrade = $plansInfo.get(selectedPlan)?.order > $currentPlan?.order; + $: isDowngrade = $plansInfo.get(selectedPlan)?.order < $currentPlan?.order; $: isButtonDisabled = $organization?.billingPlan === selectedPlan; diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index b698d60dc..ce18725fc 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -8,9 +8,10 @@ export const load: PageLoad = async ({ depends, parent, url }) => { const { members, organization, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); - const [coupon, paymentMethods] = await Promise.all([ + const [coupon, paymentMethods, plans] = await Promise.all([ getCoupon(url), - sdk.forConsole.billing.listPaymentMethods() + sdk.forConsole.billing.listPaymentMethods(), + sdk.forConsole.billing.listPlans() ]); let plan = getPlanFromUrl(url); @@ -29,6 +30,7 @@ export const load: PageLoad = async ({ depends, parent, url }) => { return { members, plan, + plans, coupon, selfService, hasFreeOrgs, From 42bc0c8f523a432c68152ef8329283a225911c27 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 16 Jun 2025 06:53:54 +0000 Subject: [PATCH 05/69] format --- src/lib/components/billing/planSelection.svelte | 2 +- .../organization-[organization]/billing/planSummary.svelte | 2 +- .../organization-[organization]/change-plan/+page.svelte | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index d4842102f..846a887f2 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index eee7a0469..62fdbf3d7 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -322,6 +322,7 @@ export function checkForProjectsLimit(org: Organization, projects: number) { const plan = get(plansInfo)?.get(org.billingPlan); if (!plan) return; if (plan.$id !== BillingPlan.FREE) return; + if (!org.projects) return; if (org.projects.length > 0) return; if (projects > plan.projects) { diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 8484d6106..f18d9e978 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -122,7 +122,8 @@ function isSetToArchive(project: Models.Project): boolean { if (!isCloud) return false; - if (data.organization.projects.length === 0) return false; + if(!data.organization.projects) return false; + if (data.organization.projects?.length === 0) return false; if (!project || !project.$id) return false; return !data.organization.projects.includes(project.$id); } @@ -131,8 +132,8 @@ return name ? (name.length > limit ? `${name.slice(0, limit)}...` : name) : '-'; } - $: projectsToArchive = data.projects.projects.filter( - (project) => !data.organization.projects.includes(project.$id) + $: projectsToArchive = data.projects.projects?.filter( + (project) => !data.organization.projects?.includes(project.$id) ); @@ -163,7 +164,7 @@ - {#if isCloud && data.organization.projects.length > 0 && $canWriteProjects} + {#if isCloud && data.organization.projects?.length > 0 && $canWriteProjects} From ee293bc66b050dfa71aea82059a92553287c338b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 25 Jun 2025 06:58:52 +0000 Subject: [PATCH 07/69] handle addons and project usage --- src/lib/sdk/billing.ts | 8 + .../billing/planSummary.svelte | 366 +++++++++++------- 2 files changed, 237 insertions(+), 137 deletions(-) diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 581d10add..baaa7f440 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -207,8 +207,15 @@ export type AggregationTeam = { * Aggregation billing plan */ plan: string; + projectBreakdown: ProjectBreakdown[] }; +export type ProjectBreakdown = { + $id: string; + name: string; + amount: number; +} + export type InvoiceUsage = { resourceId: string; value: number; @@ -413,6 +420,7 @@ export type Plan = { supportsOrganizationRoles: boolean; buildSize: number; // in MB deploymentSize: number; // in MB + usagePerProject: boolean; }; export type PlanList = { diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 09ea95f4a..d2ce8405c 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -15,6 +15,7 @@ Divider, Icon, Layout, + Table, Tooltip, Typography } from '@appwrite.io/pink-svelte'; @@ -36,159 +37,250 @@ {#if $organization} - - Payment estimates - A breakdown of your estimated upcoming payment for the current billing period. Totals displayed - exclude accumulated credits and applicable taxes. - -

- Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} -

- - - - - {currentPlan.name} plan - - - {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION - ? formatCurrency(0) - : currentPlan - ? formatCurrency(currentPlan?.price) - : ''} - - - - {#if currentPlan.budgeting && extraUsage > 0} - r.amount && r.amount > 0) - .length.toString()}> - - {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - - - {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i} - {#if i > 0} - - {/if} - - - - - {excess.resourceId} - - - {formatCurrency(excess.amount)} - - - - - - {formatNumberWithCommas(excess.value)} - - {abbreviateNumber(excess.value)} - - - - {/each} - - - {/if} - - {#if currentPlan.supportsCredits && availableCredit > 0} + {#if currentPlan.usagePerProject} + + + + {currentPlan.name} plan + + + Next payment of ${currentAggregation.amount} + will occur on + {toLocaleDate($organization?.billingNextInvoiceDate)} + + + + + + - - - Credits to be applied - - - -{formatCurrency( - Math.min(availableCredit, currentInvoice?.amount ?? 0) - )} + Base plan + + {isTrial || + $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} - {/if} - - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + + + {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0 && Object.keys(currentPlan.addons).includes(r.resourceId) && currentPlan.addons[r.resourceId].price > 0) as excess, i} + {#if i > 0} - - - - Current total (USD) - - - - Estimates are updated daily and may differ from your - final invoice. - - + {/if} + + + + {excess.resourceId} + + {formatCurrency(excess.amount)} + + + + + {/each} + {#each currentAggregation.projectBreakdown as projectBreakdown} + + + {formatCurrency(projectBreakdown.amount)} + + + + + + {/each} + + + {:else} + + Payment estimates + A breakdown of your estimated upcoming payment for the current billing period. Totals displayed + exclude accumulated credits and applicable taxes. + +

+ Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} +

+ + + + + {currentPlan.name} plan - - {formatCurrency( - Math.max( - (currentInvoice?.amount ?? 0) - - Math.min(availableCredit, currentInvoice?.amount ?? 0), - 0 - ) - )} + + {isTrial || + $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} - {/if} - - -
- - {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} -
- - -
- {:else} -
- {#if $organization?.billingPlanDowngrade !== null} - - {:else} + + {#if currentPlan.budgeting && extraUsage > 0} + r.amount && r.amount > 0) + .length.toString()}> + + {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} + + + {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i} + {#if i > 0} + + {/if} + + + + + {excess.resourceId} + + + {formatCurrency(excess.amount)} + + + + + + {formatNumberWithCommas(excess.value)} + + {abbreviateNumber(excess.value)} + + + + {/each} + + + {/if} + + {#if currentPlan.supportsCredits && availableCredit > 0} + + + + Credits to be applied + + + -{formatCurrency( + Math.min(availableCredit, currentInvoice?.amount ?? 0) + )} + + + {/if} + + {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + + + + + Current total (USD) + + + + Estimates are updated daily and may differ from your + final invoice. + + + + + + {formatCurrency( + Math.max( + (currentInvoice?.amount ?? 0) - + Math.min( + availableCredit, + currentInvoice?.amount ?? 0 + ), + 0 + ) + )} + + + {/if} + + + + + {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} +
+ - {/if} - -
- {/if} -
- +
+ {:else} +
+ {#if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} + +
+ {/if} +
+
+ {/if} {/if} From 4498077f26f153693ea36ab89f5d0fbe0405731d Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:33:43 +0530 Subject: [PATCH 08/69] add organization usage limits component for plan downgrades --- src/lib/components/labelCard.svelte | 4 +- .../components/organizationUsageLimits.svelte | 322 ++++++++++++++++++ src/lib/sdk/billing.ts | 6 +- .../billing/planSummary.svelte | 6 +- .../change-plan/+page.svelte | 12 + .../change-plan/+page.ts | 4 +- 6 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 src/lib/components/organizationUsageLimits.svelte diff --git a/src/lib/components/labelCard.svelte b/src/lib/components/labelCard.svelte index 9eb761198..3b6a00778 100644 --- a/src/lib/components/labelCard.svelte +++ b/src/lib/components/labelCard.svelte @@ -45,7 +45,9 @@ -
+
+ import { Button } from '$lib/elements/forms'; + import { getServiceLimit } from '$lib/stores/billing'; + import { BillingPlan } from '$lib/constants'; + import { Click, trackEvent } from '$lib/actions/analytics'; + import { Badge, Icon, Layout, Table, Typography } from '@appwrite.io/pink-svelte'; + import { IconArrowUp } from '@appwrite.io/pink-icons-svelte'; + + import { formatNumberWithCommas } from '$lib/helpers/numbers'; + import { Modal } from '$lib/components'; + import { organization, currentPlan } from '$lib/stores/organization'; + import { Alert } from '@appwrite.io/pink-svelte'; + import { sdk } from '$lib/stores/sdk'; + import { addNotification } from '$lib/stores/notifications'; + import { invalidate } from '$app/navigation'; + import { Dependencies } from '$lib/constants'; + import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date'; + import { billingProjectsLimitDate } from '$lib/stores/billing'; + import { page } from '$app/state'; + + let showSelectProject = false; + let selectedProjects: string[] = []; + let error: string | null = null; + let showSelectionReminder = false; + + // Get data from layout + $: projects = page.data.allProjects?.projects || []; + $: members = page.data.members?.memberships || []; + $: storageUsage = page.data.billingAggregation?.usageStorage || 0; + + $: freePlanLimits = { + projects: 2, // fallback + members: getServiceLimit('members', BillingPlan.FREE), + storage: getServiceLimit('storage', BillingPlan.FREE) + }; + + //fallback to free limit when undefined + $: allowedProjectsToKeep = + $currentPlan?.projects && $currentPlan.projects > 0 + ? $currentPlan.projects + : freePlanLimits.projects; + + $: currentUsage = { + projects: projects?.length || 0, + members: members?.length || 0, + storage: storageUsage || 0 + }; + + $: storageUsageGB = storageUsage / (1024 * 1024 * 1024); + + $: isLimitExceeded = { + projects: currentUsage.projects > freePlanLimits.projects, + members: currentUsage.members > freePlanLimits.members, + storage: storageUsageGB > freePlanLimits.storage + }; + + $: excessUsage = { + projects: Math.max(0, currentUsage.projects), + members: Math.max(0, currentUsage.members - freePlanLimits.members), + storage: Math.max(0, storageUsageGB - freePlanLimits.storage) + }; + + // projects that would be archived with the current selection + $: projectsToArchive = projects.filter((project) => !selectedProjects.includes(project.$id)); + + function formatProjectsToArchive(): string { + let result = ''; + projectsToArchive.forEach((project, index) => { + const isLast = index === projectsToArchive.length - 1; + const isSecondLast = index === projectsToArchive.length - 2; + + result += `${index === 0 ? '' : ' '}${project.name}`; + + if (!isLast) { + if (isSecondLast) result += ' and'; + else result += ','; + } + }); + return result; + } + + function formatNumber(num: number): string { + return formatNumberWithCommas(num); + } + + function handleManageProjects() { + showSelectProject = true; + trackEvent(Click.OrganizationClickUpgrade, { source: 'usage_limits_manage_projects' }); + } + + // Expose validation for parent to call before submitting downgrade + export function validateOrAlert(): boolean { + const normalizedSelection = Array.from(new Set((selectedProjects || []).map(String))); + const validIds = new Set(projects.map((p) => p.$id)); + const filteredSelection = normalizedSelection.filter((id) => validIds.has(id)); + const isValid = filteredSelection.length === allowedProjectsToKeep; + showSelectionReminder = !isValid && isLimitExceeded.projects; + return isValid; + } + + async function updateSelected() { + error = null; + + if (!$organization?.$id) { + error = 'Missing organization ID.'; + return; + } + + const normalizedSelection = Array.from(new Set((selectedProjects || []).map(String))); + const validIds = new Set(projects.map((p) => p.$id)); + const filteredSelection = normalizedSelection.filter((id) => validIds.has(id)); + + if (filteredSelection.length !== allowedProjectsToKeep) { + error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`; + return; + } + + try { + await sdk.forConsole.billing.updateSelectedProjects( + $organization.$id, + filteredSelection + ); + showSelectProject = false; + invalidate(Dependencies.ORGANIZATION); + addNotification({ + type: 'success', + message: `Projects updated for archiving` + }); + } catch (e) { + console.error('Error updating projects:', e); + error = e.message || 'Failed to update projects.'; + } + } + + + + {#if showSelectionReminder} + + The Free plan lets you keep {allowedProjectsToKeep} projects. Select them before continuing. +
+ +
+
+ {/if} +
+ + + Resource + Free limit + +
+ + Excess usage + + + +
+
+ +
+ + + + +
+ Projects + {#if isLimitExceeded.projects} + + {/if} +
+
+ + {formatNumber(allowedProjectsToKeep)} projects + + + {#if isLimitExceeded.projects} + + + + {formatNumber(excessUsage.projects)} projects + + + {:else} + + {formatNumber(currentUsage.projects)} / {formatNumber( + allowedProjectsToKeep + )} + + {/if} + + + {#if isLimitExceeded.projects} +
+
+ +
+
+ {/if} +
+
+ + + + + Organization members + + + {formatNumber(freePlanLimits.members)} member + + + {#if isLimitExceeded.members} + + + + {formatNumber(excessUsage.members)} members + + + {:else} + + {formatNumber(currentUsage.members)} / {formatNumber( + freePlanLimits.members + )} + + {/if} + + + + + + + + Storage + + + {freePlanLimits.storage} GB + + + {#if isLimitExceeded.storage} + + + + {excessUsage.storage.toFixed(2)} GB + + + {:else} + + {storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB + + {/if} + + + +
+
+
+ + + + Choose which two projects to keep. Projects over the limit will be blocked after this date. + + + {#if error} + {error} + {/if} + +
+ + + Project Name + Created + + {#each projects as project} + + {project.name} + + {toLocaleDateTime(project.$createdAt)} + + + {/each} + +
+ {#if selectedProjects.length === allowedProjectsToKeep} + + {formatProjectsToArchive()} will be archived + + {/if} + + +
+ +
+
+ +
+
+
diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 4692bbcd7..9a0163599 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -207,7 +207,7 @@ export type AggregationTeam = { * Aggregation billing plan */ plan: string; - projectBreakdown: ProjectBreakdown[] + projectBreakdown: ProjectBreakdown[]; }; export type ProjectBreakdown = { @@ -215,13 +215,13 @@ export type ProjectBreakdown = { name: string; amount: number; resources: InvoiceUsage[]; -} +}; export type InvoiceUsage = { resourceId: string; value: number; amount: number; -} +}; export type AvailableCredit = { available: number; diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 11b2e824c..f545aabcf 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -85,7 +85,7 @@ {/each} {#each currentAggregation.projectBreakdown as projectBreakdown} - + {formatCurrency(projectBreakdown.amount)} @@ -96,9 +96,7 @@ {/if} - + {resource.resourceId} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index f899405b0..2de853c51 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -33,6 +33,7 @@ import { onMount } from 'svelte'; import { loadAvailableRegions } from '$routes/(console)/regions'; import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte'; + import OrganizationUsageLimits from '$lib/components/organizationUsageLimits.svelte'; import { Query } from '@appwrite.io/console'; export let data; @@ -43,6 +44,7 @@ let previousPage: string = base; let showExitModal = false; let formComponent: Form; + let usageLimitsComponent: any; let isSubmitting = writable(false); let collaborators: string[] = data?.members?.memberships @@ -100,6 +102,12 @@ async function handleSubmit() { if (isDowngrade) { + if (selectedPlan === BillingPlan.FREE && usageLimitsComponent?.validateOrAlert) { + const ok = usageLimitsComponent.validateOrAlert(); + if (!ok) { + return; + } + } await downgrade(); } else if (isUpgrade) { await upgrade(); @@ -317,6 +325,10 @@ {/if} {/if} + + {#if isDowngrade && selectedPlan === BillingPlan.FREE} + + {/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index 5f1af0a4e..2272bd636 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -7,9 +7,7 @@ export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); - const [plans] = await Promise.all([ - sdk.forConsole.billing.listPlans() - ]); + const [plans] = await Promise.all([sdk.forConsole.billing.listPlans()]); let plan: BillingPlan; if (currentPlan?.$id === BillingPlan.SCALE) { From 5cd65a3752920a3426f77be85328ef4fbdf0f5ed Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:21:13 +0530 Subject: [PATCH 09/69] new archive projects ui --- src/lib/components/archiveProject.svelte | 154 ++++++++++++++++++ src/lib/components/gridItem1.svelte | 2 +- src/lib/sdk/billing.ts | 6 +- .../organization-[organization]/+page.svelte | 22 ++- .../billing/planSummary.svelte | 6 +- .../change-plan/+page.ts | 4 +- 6 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/lib/components/archiveProject.svelte diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte new file mode 100644 index 000000000..d521b2b24 --- /dev/null +++ b/src/lib/components/archiveProject.svelte @@ -0,0 +1,154 @@ + + +{#if projectsToArchive.length > 0} +
+ + + These projects have been archived and are read-only. You can view and migrate their + data. + + +
+ + {#each projectsToArchive as project} + {@const platforms = filterPlatforms( + project.platforms.map((platform) => getPlatformInfo(platform.type)) + )} + + + {project?.platforms?.length ? project?.platforms?.length : 'No'} apps + + {project.name} + + + + { + e.preventDefault(); + e.stopPropagation(); + readOnlyInfoOpen = { + ...readOnlyInfoOpen, + [project.$id]: !readOnlyInfoOpen[project.$id] + }; + }}> + + Read only + + +
  • + + Archived projects are read-only. You can view + and migrate their data, but they no longer + accept edits or requests. + +
  • +
    +
    + + + + Unarchive project + Migrate project + + +
    +
    + {#each platforms.slice(0, 2) as platform} + {@const icon = getIconForPlatform(platform.icon)} + + + + {/each} +
    + {/each} +
    +
    +
    +
    +{/if} diff --git a/src/lib/components/gridItem1.svelte b/src/lib/components/gridItem1.svelte index 89b2d3bb2..40c8e66d9 100644 --- a/src/lib/components/gridItem1.svelte +++ b/src/lib/components/gridItem1.svelte @@ -1,6 +1,6 @@ diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 4692bbcd7..9a0163599 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -207,7 +207,7 @@ export type AggregationTeam = { * Aggregation billing plan */ plan: string; - projectBreakdown: ProjectBreakdown[] + projectBreakdown: ProjectBreakdown[]; }; export type ProjectBreakdown = { @@ -215,13 +215,13 @@ export type ProjectBreakdown = { name: string; amount: number; resources: InvoiceUsage[]; -} +}; export type InvoiceUsage = { resourceId: string; value: number; amount: number; -} +}; export type AvailableCredit = { available: number; diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index a622d9523..3787a3857 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -40,6 +40,7 @@ import CreateProjectCloud from './createProjectCloud.svelte'; import { currentPlan, regions as regionsStore } from '$lib/stores/organization'; import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte'; + import ArchiveProject from '$lib/components/archiveProject.svelte'; import { toLocaleDate } from '$lib/helpers/date'; export let data; @@ -138,6 +139,10 @@ $: projectsToArchive = data.projects.projects.filter( (project) => !data.organization.projects?.includes(project.$id) ); + + $: activeProjects = data.projects.projects.filter((project) => + data.organization.projects?.includes(project.$id) + ); @@ -167,9 +172,9 @@
    - {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > 2 && $canWriteProjects} + {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && projectsToArchive.length > 0 && $canWriteProjects} + title={`${projectsToArchive.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}> {#each projectsToArchive as project, index}{@const text = `${project.name}`} {@html text}{index == projectsToArchive.length - 2 @@ -199,13 +204,13 @@ {/if} - {#if data.projects.total} + {#if activeProjects.length > 0} - {#each data.projects.projects as project} + {#each activeProjects as project} {@const platforms = filterPlatforms( project.platforms.map((platform) => getPlatformInfo(platform.type)) )} @@ -270,7 +275,7 @@

    Create a new project

    - {:else} + {:else if data.projects.total === 0} + total={activeProjects.length} /> + + + diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 11b2e824c..f545aabcf 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -85,7 +85,7 @@ {/each} {#each currentAggregation.projectBreakdown as projectBreakdown} - + {formatCurrency(projectBreakdown.amount)} @@ -96,9 +96,7 @@ {/if} - + {resource.resourceId} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index 5f1af0a4e..2272bd636 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -7,9 +7,7 @@ export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); - const [plans] = await Promise.all([ - sdk.forConsole.billing.listPlans() - ]); + const [plans] = await Promise.all([sdk.forConsole.billing.listPlans()]); let plan: BillingPlan; if (currentPlan?.$id === BillingPlan.SCALE) { From 8a1df95152facb0651d855355658c89fbc48c808 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Tue, 26 Aug 2025 02:17:13 +0530 Subject: [PATCH 10/69] feat: add detailed project usage breakdown to billing plan summary --- src/lib/components/estimatedCard.svelte | 10 + src/lib/components/index.ts | 1 + src/lib/layout/createProject.svelte | 47 +- src/lib/sdk/billing.ts | 6 +- .../billing/+page.svelte | 7 +- .../billing/+page.ts | 47 +- .../billing/planSummary.svelte | 544 ++++++++++-------- .../change-plan/+page.ts | 4 +- 8 files changed, 390 insertions(+), 276 deletions(-) create mode 100644 src/lib/components/estimatedCard.svelte diff --git a/src/lib/components/estimatedCard.svelte b/src/lib/components/estimatedCard.svelte new file mode 100644 index 000000000..8c935aa37 --- /dev/null +++ b/src/lib/components/estimatedCard.svelte @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 3f451a4bf..678861167 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -84,3 +84,4 @@ export { default as UsageCard } from './usageCard.svelte'; export { default as ViewToggle } from './viewToggle.svelte'; export { default as RegionEndpoint } from './regionEndpoint.svelte'; export { default as ExpirationInput } from './expirationInput.svelte'; +export { default as EstimatedCard } from './estimatedCard.svelte'; diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 4e419c0b1..2199f0a4b 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -4,13 +4,14 @@ import { CustomId } from '$lib/components/index.js'; import { getFlagUrl } from '$lib/helpers/flag'; import { isCloud } from '$lib/system.js'; - import { currentPlan } from '$lib/stores/organization'; + import { currentPlan, organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { base } from '$app/paths'; import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { filterRegions } from '$lib/helpers/regions'; import type { Snippet } from 'svelte'; + import { BillingPlan } from '$lib/constants'; let { projectName = $bindable(''), @@ -31,8 +32,11 @@ } = $props(); let showCustomId = $state(false); + let isProPlan = $derived($organization?.billingPlan === BillingPlan.PRO); let projectsLimited = $derived( - $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects + isProPlan + ? projects && projects >= 2 + : $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects ); @@ -46,24 +50,11 @@ {#if showTitle} Create your project {/if} - {#if projectsLimited} - - Extra projects are available on paid plans for an additional fee - - - - - {/if} + + {#if projectsLimited} + {#if isProPlan} + + You've reached your limit of 2 projects included on the Pro plan. Each extra + project costs $15/month. + + {:else} + + Extra projects are available on paid plans for an additional fee + + + + + {/if} + {/if} + {#if isCloud && regions.length > 0} {/if} - Billing + currentInvoice={data?.billingInvoice} + organizationUsage={data?.organizationUsage} + usageProjects={data?.usageProjects} /> { const { organization, scopes, currentPlan, countryList, locale } = await parent(); @@ -57,6 +59,47 @@ export const load: PageLoad = async ({ parent, depends }) => { organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION)) : false; + // load organization usage data for planSummary component + let organizationUsage = null; + let usageProjects: Record = {}; + + // load projects directly for the planSummary component + try { + const projectsResponse = await sdk.forConsole.projects.list([ + Query.equal('teamId', organization.$id), + Query.limit(1000) + ]); + + if (projectsResponse.projects.length > 0) { + // mock data since organizationUsage is not availlable due to some reason + organizationUsage = { + projects: projectsResponse.projects.map((project, index) => ({ + projectId: project.$id, + storage: 0, + executions: 0, + executionsMBSeconds: 0, + bandwidth: 0, + databasesReads: 0, + databasesWrites: 0, + users: 0, + authPhoneTotal: 40102, + authPhoneEstimate: 8.4, + imageTransformations: 0 + })) + }; + + // Create usageProjects mapping + for (const project of projectsResponse.projects) { + usageProjects[project.$id] = { + name: project.name, + region: project.region + }; + } + } + } catch (e) { + // ignore error + } + const [paymentMethods, addressList, billingAddress, availableCredit] = await Promise.all([ sdk.forConsole.billing.listPaymentMethods(), sdk.forConsole.billing.listAddresses(), @@ -76,6 +119,8 @@ export const load: PageLoad = async ({ parent, depends }) => { billingInvoice, areCreditsSupported, countryList, - locale + locale, + organizationUsage, + usageProjects }; }; diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 11b2e824c..ad2c37097 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -1,293 +1,334 @@ {#if $organization} - {#if currentPlan.usagePerProject} - - - - {currentPlan.name} plan - - - Next payment of ${currentAggregation.amount} - will occur on - {toLocaleDate($organization?.billingNextInvoiceDate)} - - - - - - - - Base plan + + {currentPlan.name} plan + + + Next payment of {formatCurrency(currentInvoice?.amount || currentPlan?.price || 0)} + will occur on + {toLocaleDate($organization?.billingNextInvoiceDate)}. + + + +
    + + Current billing cycle ({new Date( + $organization?.billingCurrentInvoiceDate + ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}-{new Date( + $organization?.billingNextInvoiceDate + ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}) + + + Estimate, subject to change based on usage. + +
    + + + {#each billingData as row} + + {#each columns as col} + root.toggle(row.id)}> - {isTrial || - $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION - ? formatCurrency(0) - : currentPlan - ? formatCurrency(currentPlan?.price) - : ''} + {row.cells?.[col.id] ?? ''} -
    -
    -
    - {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0 && Object.keys(currentPlan.addons).includes(r.resourceId) && currentPlan.addons[r.resourceId].price > 0) as excess, i} - {#if i > 0} - - {/if} - - - - {excess.resourceId} - - {formatCurrency(excess.amount)} - - - - - {/each} - {#each currentAggregation.projectBreakdown as projectBreakdown} - - - {formatCurrency(projectBreakdown.amount)} - - - {#each projectBreakdown.resources as resource, i} - {#if i > 0} - - {/if} + + {/each} - - - - {resource.resourceId} - - - {formatCurrency(resource.amount)} - - - - - - {formatNumberWithCommas(resource.value)} - - {abbreviateNumber(resource.value)} - - - - {/each} - - - {/each} -
    -
    - {:else} - - Payment estimates - A breakdown of your estimated upcoming payment for the current billing period. Totals displayed - exclude accumulated credits and applicable taxes. - -

    - Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} -

    - - - - - {currentPlan.name} plan - - - {isTrial || - $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION - ? formatCurrency(0) - : currentPlan - ? formatCurrency(currentPlan?.price) - : ''} - - - - {#if currentPlan.budgeting && extraUsage > 0} - r.amount && r.amount > 0) - .length.toString()}> - - {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - - - {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i} - {#if i > 0} - - {/if} - - - - - {excess.resourceId} + + {#if row.children} + {#each row.children as child (child.id)} + + {/each} {/if} + + + {/each} - {#if currentPlan.supportsCredits && availableCredit > 0} - - - - Credits to be applied - - - -{formatCurrency( - Math.min(availableCredit, currentInvoice?.amount ?? 0) - )} - - - {/if} + + {}}> + + Total + + + {}}> + + + + {}}> + + {formatCurrency(totalAmount)} + + + + - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} - - - - - Current total (USD) - - - - Estimates are updated daily and may differ from your - final invoice. - - - - - - {formatCurrency( - Math.max( - (currentInvoice?.amount ?? 0) - - Math.min( - availableCredit, - currentInvoice?.amount ?? 0 - ), - 0 - ) - )} - - - {/if} - - -
    - - {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} -
    - + +
    + {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} +
    + + +
    + {:else} +
    + {#if $organization?.billingPlanDowngrade !== null} + + {:else} -
    - {:else} -
    - {#if $organization?.billingPlanDowngrade !== null} - - {:else} - - {/if} - -
    - {/if} - - - {/if} + {/if} + +
    + {/if} +
    + {/if} From ed1bffd8c023be891bc82ef988bfa99a21830205 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:56:25 +0530 Subject: [PATCH 19/69] Update src/lib/components/archiveProject.svelte Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/components/archiveProject.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte index 812a060d0..6f2896662 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -155,7 +155,7 @@ {#if projectsToArchive.length > 0} -
    +
    These projects have been archived and are read-only. You can view and migrate their From fb1a19a9b8c7863f4f10efd6a312d65c686d4c00 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:57:58 +0530 Subject: [PATCH 20/69] Update src/routes/(console)/organization-[organization]/+page.svelte Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../(console)/organization-[organization]/+page.svelte | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 3e4eb62b3..2eac0f3c8 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -136,13 +136,12 @@ } $: selectedProjects = data.organization.projects ?? []; - $: hasSelection = Array.isArray(selectedProjects) && selectedProjects.length > 0; - $: projectsToArchive = hasSelection + $: projectsToArchive = Array.isArray(selectedProjects) && selectedProjects.length > 0 ? data.projects.projects.filter((project) => !selectedProjects.includes(project.$id)) : []; - $: activeProjects = hasSelection + $: activeProjects = Array.isArray(selectedProjects) && selectedProjects.length > 0 ? data.projects.projects.filter((project) => selectedProjects.includes(project.$id)) : data.projects.projects; From 3c9ae868bc931d864102f180da03161a1fdc6bf6 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:27:47 +0530 Subject: [PATCH 21/69] feat: some chnages like using classses instead of style and adding format string as a helper --- src/lib/components/archiveProject.svelte | 29 +++++++++---------- .../components/billing/planSelection.svelte | 3 +- src/lib/helpers/string.ts | 13 +++++++++ .../organization-[organization]/+page.svelte | 19 ++++++------ 4 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte index 6f2896662..ce228172e 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -147,10 +147,9 @@ return $regionsStore.regions.find((region) => region.$id === project.region); } + import { formatName as formatNameHelper } from '$lib/helpers/string'; function formatName(name: string, limit: number = 19) { - const mobileLimit = 16; - const actualLimit = $isSmallViewport ? mobileLimit : limit; - return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; + return formatNameHelper(name, limit, $isSmallViewport); } @@ -162,7 +161,7 @@ data. -
    +
    {#each projectsToArchive as project} {@const platforms = filterPlatforms( @@ -267,10 +266,8 @@ - +

    Are you sure you want to unarchive {projectToUnarchive?.name}?

    +

    This will move the project back to your active projects list.

    @@ -279,17 +276,17 @@
    diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index 00952e8ab..2018e3944 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -11,9 +11,10 @@ export let billingPlan: Tier; export let isNewOrg = false; export let selfService = true; + export let anyOrgFree = false; $: plans = Object.values(page.data.plans.plans) as Plan[]; - $: currentPlanInList = plans.filter((plan) => plan.$id === $currentPlan?.$id).length > 0; + $: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id); diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts index c138a7483..91dd9a37c 100644 --- a/src/lib/helpers/string.ts +++ b/src/lib/helpers/string.ts @@ -49,6 +49,19 @@ export function formatNum(number: number): string { return formatter.format(number); } +/** + * Format a string with optional mobile-aware truncation. + */ +export function formatName( + name: string, + limit: number = 19, + isSmallViewport: boolean = false +): string { + const mobileLimit = 16; + const actualLimit = isSmallViewport ? mobileLimit : limit; + return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; +} + /** * Returns a regex to check hostname validity. Supports wildcards too! */ diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 2eac0f3c8..a81b00c71 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -129,21 +129,22 @@ return !data.organization.projects?.includes(project.$id); } + import { formatName as formatNameHelper } from '$lib/helpers/string'; function formatName(name: string, limit: number = 19) { - const mobileLimit = 16; - const actualLimit = $isSmallViewport ? mobileLimit : limit; - return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; + return formatNameHelper(name, limit, $isSmallViewport); } $: selectedProjects = data.organization.projects ?? []; - $: projectsToArchive = Array.isArray(selectedProjects) && selectedProjects.length > 0 - ? data.projects.projects.filter((project) => !selectedProjects.includes(project.$id)) - : []; + $: projectsToArchive = + Array.isArray(selectedProjects) && selectedProjects.length > 0 + ? data.projects.projects.filter((project) => !selectedProjects.includes(project.$id)) + : []; - $: activeProjects = Array.isArray(selectedProjects) && selectedProjects.length > 0 - ? data.projects.projects.filter((project) => selectedProjects.includes(project.$id)) - : data.projects.projects; + $: activeProjects = + Array.isArray(selectedProjects) && selectedProjects.length > 0 + ? data.projects.projects.filter((project) => selectedProjects.includes(project.$id)) + : data.projects.projects; Date: Thu, 28 Aug 2025 17:21:07 +0530 Subject: [PATCH 22/69] gate new pricing UI by plan flag; optimize billing loads Add billingPlan prop to createProject.svelte; derive isProPlan from prop Introduce useNewPricingModal derived from currentPlan.usagePerProject Wire pricing modal triggers to use new modal only when usagePerProject is true Add planSummaryOld.svelte and conditionally render based on useNewPricingModal Optimize billing/+page.ts to fetch only needed projects by from usage/aggregation Remove no-op reactive block in planSummary.svelte; resolve unused prop warning --- src/lib/layout/containerHeader.svelte | 7 +- src/lib/layout/createProject.svelte | 4 +- src/lib/stores/billing.ts | 1 + .../billing/+page.svelte | 25 +- .../billing/+page.ts | 30 ++- .../billing/budgetCap.svelte | 4 +- .../billing/planSummary.svelte | 8 - .../billing/planSummaryOld.svelte | 250 ++++++++++++++++++ .../usage/[[invoice]]/+page.svelte | 9 +- .../settings/usage/[[invoice]]/+page.svelte | 11 +- 10 files changed, 313 insertions(+), 36 deletions(-) create mode 100644 src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte diff --git a/src/lib/layout/containerHeader.svelte b/src/lib/layout/containerHeader.svelte index 1e5a9fb95..a7af909f0 100644 --- a/src/lib/layout/containerHeader.svelte +++ b/src/lib/layout/containerHeader.svelte @@ -11,6 +11,7 @@ getServiceLimit, readOnly, showUsageRatesModal, + useNewPricingModal, tierToPlan, upgradeURL, type PlanServices @@ -101,7 +102,7 @@ You've reached the {services} limit for the {tier} plan. - ($showUsageRatesModal = true)} + ($showUsageRatesModal = $useNewPricingModal)} >Excess usage fees will apply. @@ -159,7 +160,9 @@

    You are limited to {limit} {title.toLocaleLowerCase()} per organization on the {tier} plan. - ($showUsageRatesModal = true)} + + ($showUsageRatesModal = $useNewPricingModal)} >Excess usage fees will apply.

    diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 90701ef4d..13538ca1a 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -19,6 +19,7 @@ regions = [], region = $bindable(''), showTitle = true, + billingPlan = undefined, projects = undefined, submit }: { @@ -27,12 +28,13 @@ regions: Array; region: string; showTitle: boolean; + billingPlan?: BillingPlan; projects?: number; submit?: Snippet; } = $props(); let showCustomId = $state(false); - let isProPlan = $derived($organization?.billingPlan === BillingPlan.PRO); + let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO); let projectsLimited = $derived( isProPlan ? projects && projects >= 2 diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index e8b78bad8..c08cd31b1 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -235,6 +235,7 @@ export const tierEnterprise: TierData = { }; export const showUsageRatesModal = writable(false); +export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true); export function checkForUsageFees(plan: Tier, id: PlanServices) { if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) { diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index 8b5413f62..05299ad92 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -2,12 +2,13 @@ import { Container } from '$lib/layout'; import BudgetCap from './budgetCap.svelte'; import PlanSummary from './planSummary.svelte'; + import PlanSummaryOld from './planSummaryOld.svelte'; import BillingAddress from './billingAddress.svelte'; import PaymentMethods from './paymentMethods.svelte'; import AvailableCredit from './availableCredit.svelte'; import PaymentHistory from './paymentHistory.svelte'; import TaxId from './taxId.svelte'; - import { failedInvoice, tierToPlan, upgradeURL } from '$lib/stores/billing'; + import { failedInvoice, tierToPlan, upgradeURL, useNewPricingModal } from '$lib/stores/billing'; import type { PaymentMethodData } from '$lib/sdk/billing'; import { onMount } from 'svelte'; import { page } from '$app/state'; @@ -127,13 +128,21 @@ until your billing period ends on {toLocaleDate(organization.billingNextInvoiceDate)}.
    {/if} - + {#if $useNewPricingModal} + + {:else} + + {/if} { } try { - const projectsResponse = await sdk.forConsole.projects.list([ - Query.equal('teamId', organization.$id), - Query.limit(1000) - ]); - for (const project of projectsResponse.projects) { - usageProjects[project.$id] = { - name: project.name, - region: project.region - }; + const neededIds = new Set(); + const addId = (id?: string) => id && neededIds.add(id); + + if (organizationUsage?.projects?.length) { + for (const p of organizationUsage.projects) addId(p.projectId); + } else if (billingAggregation?.projectBreakdown?.length) { + for (const p of billingAggregation.projectBreakdown) addId(p.$id); + } + + if (neededIds.size > 0) { + const ids = Array.from(neededIds); + const projectsResponse = await sdk.forConsole.projects.list([ + Query.equal('$id', ids), + Query.limit(ids.length) + ]); + for (const project of projectsResponse.projects) { + usageProjects[project.$id] = { + name: project.name, + region: project.region + }; + } } } catch (e) { // ignore error diff --git a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte index bd08a88ca..dc8043eaa 100644 --- a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte +++ b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte @@ -4,7 +4,7 @@ import { CardGrid } from '$lib/components'; import { BillingPlan, Dependencies } from '$lib/constants'; import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms'; - import { showUsageRatesModal, upgradeURL } from '$lib/stores/billing'; + import { showUsageRatesModal, upgradeURL, useNewPricingModal } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; import { type Organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; @@ -72,7 +72,7 @@ Budget cap limits do not include the base amount of your plan. diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 5c8167f4c..1a8f610ea 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -19,17 +19,9 @@ export let currentPlan: Plan; export let currentInvoice: Invoice | undefined = undefined; export let currentAggregation: AggregationTeam | undefined = undefined; - export let availableCredit: number | undefined = undefined; export let organizationUsage: any = undefined; export let usageProjects: Record = {}; - $: { - currentAggregation; - availableCredit; - organizationUsage; - usageProjects; - } - let showCancel: boolean = false; // define columns for the expandable table diff --git a/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte new file mode 100644 index 000000000..3f27119c9 --- /dev/null +++ b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte @@ -0,0 +1,250 @@ + + +{#if $organization} + + Payment estimates + A breakdown of your estimated upcoming payment for the current billing period. Totals displayed + exclude accumulated credits and applicable taxes. + +

    + Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} +

    + + + + + {currentPlan.name} plan + + + {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} + + + + {#if currentPlan.budgeting && extraUsage > 0} + 0 + ? currentInvoice.usage.length + 1 + : currentInvoice.usage.length + ).toString()}> + + {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} + + + {#if currentAggregation.additionalMembers} + + + Additional members + + {formatCurrency( + currentAggregation.additionalMemberAmount + )} + + + + {currentAggregation.additionalMembers} + + + {/if} + {#if currentInvoice?.usage} + {#each currentInvoice.usage as excess, i} + {#if i > 0 || currentAggregation.additionalMembers} + + {/if} + + + + + {excess.name} + + + {formatCurrency(excess.amount)} + + + + + + {formatNumberWithCommas(excess.value)} + + {abbreviateNumber(excess.value)} + + + + {/each} + {/if} + + + {/if} + + {#if currentPlan.supportsCredits && availableCredit > 0} + + + + Credits to be applied + + + -{formatCurrency( + Math.min(availableCredit, currentInvoice?.amount ?? 0) + )} + + + {/if} + + {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + + + + + Current total (USD) + + + + Estimates are updated daily and may differ from your + final invoice. + + + + + + {formatCurrency( + Math.max( + (currentInvoice?.amount ?? 0) - + Math.min(availableCredit, currentInvoice?.amount ?? 0), + 0 + ) + )} + + + {/if} + + +
    + + {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} +
    + + +
    + {:else} +
    + {#if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} + +
    + {/if} +
    +
    +{/if} + + + + diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index e27353624..99d1077f6 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -8,7 +8,8 @@ getServiceLimit, showUsageRatesModal, type Tier, - upgradeURL + upgradeURL, + useNewPricingModal } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import ProjectBreakdown from './ProjectBreakdown.svelte'; @@ -76,13 +77,15 @@ {#if $organization.billingPlan === BillingPlan.SCALE}

    On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. ($showUsageRatesModal = true)} + resource listed below. ($showUsageRatesModal = $useNewPricingModal)} >Learn more

    {:else if $organization.billingPlan === BillingPlan.PRO}

    On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. ($showUsageRatesModal = true)} + resource listed below. ($showUsageRatesModal = $useNewPricingModal)} >Learn more

    {:else if $organization.billingPlan === BillingPlan.FREE} diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 24b82ae7b..a6ad580a0 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -1,7 +1,12 @@ {#if $organization} From 1228ee2a0f0fa55334ad4198775765d6f8fa218e Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Sat, 30 Aug 2025 01:29:57 +0530 Subject: [PATCH 30/69] fix: project selection for archive fix --- .../components/organizationUsageLimits.svelte | 40 +++---------------- .../change-plan/+page.svelte | 37 +++++++++-------- 2 files changed, 27 insertions(+), 50 deletions(-) diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index 39133ba92..6d2aa65cf 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -8,12 +8,8 @@ import { formatNumberWithCommas } from '$lib/helpers/numbers'; import { Modal } from '$lib/components'; - import { currentPlan } from '$lib/stores/organization'; import { Alert } from '@appwrite.io/pink-svelte'; - import { sdk } from '$lib/stores/sdk'; import { addNotification } from '$lib/stores/notifications'; - import { invalidate } from '$app/navigation'; - import { Dependencies } from '$lib/constants'; import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date'; import { billingProjectsLimitDate } from '$lib/stores/billing'; import type { Organization } from '$lib/stores/organization'; @@ -21,7 +17,6 @@ // Props let { - organization, projects = [], members = [], storageUsage = 0 @@ -44,12 +39,8 @@ storage: getServiceLimit('storage', BillingPlan.FREE) }); - //fallback to free limit when undefined - let allowedProjectsToKeep = $derived( - $currentPlan?.projects && $currentPlan.projects > 0 - ? $currentPlan.projects - : freePlanLimits.projects - ); + // When preparing to downgrade to Free, enforce Free plan limit locally (2) + let allowedProjectsToKeep = $derived(freePlanLimits.projects); let currentUsage = $derived({ projects: projects?.length || 0, @@ -115,37 +106,18 @@ return selectedProjects.filter((id) => projects.some((p) => p.$id === id)); } - async function updateSelected() { + function updateSelected() { error = null; - - if (!organization?.$id) { - error = 'Missing organization ID.'; - return; - } - const filteredSelection = selectedProjects.filter((id) => projects.some((p) => p.$id === id) ); - if (filteredSelection.length !== allowedProjectsToKeep) { error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`; return; } - - try { - await sdk.forConsole.billing.updateSelectedProjects( - organization.$id, - filteredSelection - ); - showSelectProject = false; - invalidate(Dependencies.ORGANIZATION); - addNotification({ - type: 'success', - message: `Projects updated for archiving` - }); - } catch (e) { - error = e.message; - } + // Keep selection locally; parent flow will apply after plan change + showSelectProject = false; + addNotification({ type: 'success', message: `Projects selected for archiving` }); } diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index f0399fbba..4ed648e7c 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -123,11 +123,11 @@ async function handleSubmit() { if (isDowngrade) { - if (selectedPlan === BillingPlan.FREE && usageLimitsComponent?.validateOrAlert) { + // If target plan has a non-zero project limit, ensure selection made + const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; + if (targetProjectsLimit > 0 && usageLimitsComponent?.validateOrAlert) { const ok = usageLimitsComponent.validateOrAlert(); - if (!ok) { - return; - } + if (!ok) return; } await downgrade(); } else if (isUpgrade) { @@ -165,18 +165,7 @@ async function downgrade() { try { - // If downgrading to FREE and we have project limits, select projects first - if (selectedPlan === BillingPlan.FREE && usageLimitsComponent) { - const selected = usageLimitsComponent.getSelectedProjects(); - if (selected.length > 0) { - // Send selected projects BEFORE downgrading - await sdk.forConsole.billing.updateSelectedProjects( - data.organization.$id, - selected - ); - } - } - + // 1) ppdate the plan first await sdk.forConsole.billing.updatePlan( data.organization.$id, selectedPlan, @@ -184,6 +173,22 @@ null ); + // 2) If the target plan has a project limit, apply selected projects now + const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; + if (targetProjectsLimit > 0 && usageLimitsComponent) { + const selected = usageLimitsComponent.getSelectedProjects(); + if (selected?.length) { + try { + await sdk.forConsole.billing.updateSelectedProjects( + data.organization.$id, + selected + ); + } catch (projectError) { + console.warn('Project selection failed after plan update:', projectError); + } + } + } + trackDowngradeFeedback(); await invalidate(Dependencies.ORGANIZATION); From 25300097ac7f79ed3d72c9c8f32849aad9f2a2c0 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:18:42 +0530 Subject: [PATCH 31/69] fix: fixed colors --- .../billing/planSummary.svelte | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index c363a89e4..f08950446 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -401,17 +401,38 @@ {/if}
    - - {child.cells?.[col.id] ?? ''} - + {#if child.cells?.[col.id]?.includes(' / ')} + {@const usageParts = ( + child.cells?.[col.id] ?? '' + ).split(' / ')} + + {usageParts[0]} + + + {' / '} + + + {usageParts[1]} + + {:else} + + {child.cells?.[col.id] ?? ''} + + {/if}
    {:else} + color="--fgcolor-neutral-secondary"> {child.cells?.[col.id] ?? ''} {/if} From 637de1c83728f29d4d27ae58551a22f5a8c54de5 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:48:24 +0530 Subject: [PATCH 32/69] fix: removed usage tab and estimated usage button --- .../billing/alerts/limitReached.svelte | 11 ++++++++--- .../billing/planSummary.svelte | 16 ++++++++++------ .../billing/planSummaryOld.svelte | 16 ++++++++++------ .../budgetLimitAlert.svelte | 11 ++++++++--- .../organization-[organization]/header.svelte | 6 +++++- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte index 11d832107..4885db75e 100644 --- a/src/lib/components/billing/alerts/limitReached.svelte +++ b/src/lib/components/billing/alerts/limitReached.svelte @@ -20,9 +20,14 @@ plan. Consider upgrading to increase your resource usage. - + {#if !page.data.currentPlan?.usagePerProject} + + {/if} + {#if !currentPlan?.usagePerProject} + + {/if} + {#if !currentPlan?.usagePerProject} + + {/if}
    {/if}
    diff --git a/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte index 3f27119c9..18fdd81cc 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte @@ -172,9 +172,11 @@ {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
    - + {#if !currentPlan?.usagePerProject} + + {/if} + {#if !currentPlan?.usagePerProject} + + {/if}
    {/if}
    diff --git a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte index ca84dbf21..338be255e 100644 --- a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte +++ b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte @@ -17,9 +17,14 @@ Appwrite services, update the budget limit. - + {#if !page.data.currentPlan?.usagePerProject} + + {/if} diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index f114dcc22..1a81ae48c 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -66,7 +66,11 @@ event: 'usage', title: 'Usage', hasChildren: true, - disabled: !(isCloud && ($isOwner || $isBilling)) + disabled: !( + isCloud && + ($isOwner || $isBilling) && + !page.data.currentPlan?.usagePerProject + ) }, { href: `${path}/billing`, From 92a2be06dc22ebfcfc3500891bb5ba68ffa7145d Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 31 Aug 2025 13:59:22 +0530 Subject: [PATCH 33/69] cleanup. --- src/routes/(authenticated)/git/+layout.ts | 22 ++++++++++++++++--- .../git/authorize-contributor/+page.svelte | 14 ++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/routes/(authenticated)/git/+layout.ts b/src/routes/(authenticated)/git/+layout.ts index efd98b286..cd35d7478 100644 --- a/src/routes/(authenticated)/git/+layout.ts +++ b/src/routes/(authenticated)/git/+layout.ts @@ -2,10 +2,26 @@ import { redirect } from '@sveltejs/kit'; import { base } from '$app/paths'; import type { LayoutLoad } from './$types'; -export const load: LayoutLoad = async ({ parent }) => { +export const load: LayoutLoad = async ({ parent, url }) => { const { account } = await parent(); - if (!account) { - redirect(303, base); + const projectId = url.searchParams.get('projectId'); + const repositoryId = url.searchParams.get('repositoryId'); + const installationId = url.searchParams.get('installationId'); + const providerPullRequestId = url.searchParams.get('providerPullRequestId'); + + if (!installationId || !repositoryId || !providerPullRequestId) { + redirect(303, `${base}`); } + + if (!account) { + redirect(303, `${base}`); + } + + return { + projectId, + repositoryId, + installationId, + providerPullRequestId + }; }; diff --git a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte index 9a5d8e99e..23c1b34d5 100644 --- a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte +++ b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte @@ -1,5 +1,4 @@ {#if $organization} @@ -331,8 +340,7 @@ {currentPlan.name} plan - Next payment of {formatCurrency(currentInvoice?.amount || currentPlan?.price || 0)} + Next payment of {formatCurrency(totalAmount)} will occur on {toLocaleDate($organization?.billingNextInvoiceDate)}. @@ -450,6 +458,47 @@ {/each} + {#if availableCredit > 0} + + {}}> + + + + Credits + + + {}}> + + + + {}}> + + -{formatCurrency(availableCredit)} + + + + {/if} { if (currentAggregation) { let projectSpecificData = null; if (currentAggregation.breakdown) { - projectSpecificData = currentAggregation.breakdown.find( - (p) => p.$id === project - ); + projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); } if (projectSpecificData) { From 333ad68ef9f8bd01b295969061ac3ca5ab0cc2c0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 31 Aug 2025 13:18:35 +0000 Subject: [PATCH 40/69] remove unused import --- .../organization-[organization]/billing/planSummary.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 9189a68d4..33ce6d70e 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -5,7 +5,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { AggregationTeam, Invoice, Plan } from '$lib/sdk/billing'; + import type { AggregationTeam, Plan } from '$lib/sdk/billing'; import { formatCurrency } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; From b0625be54a9c745bc8dca177ced95d2890c92253 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 31 Aug 2025 13:34:49 +0000 Subject: [PATCH 41/69] remove unused code --- .../(console)/organization-[organization]/billing/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index 4f1b4bad5..ef4c7a11f 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -132,8 +132,7 @@ + currentAggregation={data?.billingAggregation} /> {:else} Date: Sun, 31 Aug 2025 20:00:32 +0530 Subject: [PATCH 42/69] remove: feature flags for sites. --- src/lib/flags.ts | 3 +- .../(images)/empty-sites-dark-mobile.svg | 119 ------- .../sites/(images)/empty-sites-dark.svg | 239 ------------- .../(images)/empty-sites-light-mobile.svg | 157 --------- .../sites/(images)/empty-sites-light.svg | 321 ------------------ .../sites/+page.svelte | 142 ++------ .../project-[region]-[project]/sites/+page.ts | 24 +- 7 files changed, 39 insertions(+), 966 deletions(-) delete mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg delete mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg delete mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg delete mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg diff --git a/src/lib/flags.ts b/src/lib/flags.ts index 70bba582a..724986186 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -5,6 +5,7 @@ import type { Organization } from './stores/organization'; // Parse feature flags from env as a string array (exact match only) const flagsRaw = (env.PUBLIC_CONSOLE_FEATURE_FLAGS ?? '').split(','); +// @ts-expect-error: unused method! function isFlagEnabled(name: string) { // loose generic to allow safe access while retaining type safety return (data: T) => { @@ -19,5 +20,5 @@ function isFlagEnabled(name: string) { } export const flags = { - showSites: isFlagEnabled('sites') + }; diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg deleted file mode 100644 index 39c7917cf..000000000 --- a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg +++ /dev/null @@ -1,119 +0,0 @@ - - -
    - - - -
    - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg deleted file mode 100644 index b64811a48..000000000 --- a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg deleted file mode 100644 index 8bd4b239e..000000000 --- a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg +++ /dev/null @@ -1,157 +0,0 @@ - - -
    - - - - -
    -
    - - - - -
    -
    - - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg deleted file mode 100644 index a0314acfc..000000000 --- a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg +++ /dev/null @@ -1,321 +0,0 @@ - - - - - - - -
    - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte index 7cbb99660..caa180049 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte @@ -11,14 +11,12 @@ import { isServiceLimited } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import { canWriteSites } from '$lib/stores/roles.js'; - import { Card, Icon, Layout, Typography } from '@appwrite.io/pink-svelte'; + import { Icon, Layout } from '@appwrite.io/pink-svelte'; import { Button } from '$lib/elements/forms'; import { app } from '$lib/stores/app'; import CreateSiteModal from './createSiteModal.svelte'; import EmptyLight from './(images)/empty-sites-light.svg'; import EmptyDark from './(images)/empty-sites-dark.svg'; - import EmptyLightMobile from './(images)/empty-sites-light-mobile.svg'; - import EmptyDarkMobile from './(images)/empty-sites-dark-mobile.svg'; import Grid from './grid.svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import { columns } from './store'; @@ -28,15 +26,10 @@ import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; - import { isSmallViewport } from '$lib/stores/viewport'; - import { addNotification } from '$lib/stores/notifications'; - import { isOnWaitlistSites, joinWaitlistSites } from '$lib/helpers/waitlist'; - import { user } from '$lib/stores/user'; export let data; let show = false; - let isOnWaitlist = isOnWaitlistSites($user); $: $registerCommands([ { @@ -57,118 +50,51 @@ onMount(() => { return sdk.forConsole.client.subscribe('console', (response) => { - if (response.events.includes(`sites.*`)) { + if (response.events.includes('sites.*')) { invalidate(Dependencies.SITES); } }); }); - - $: isDark = $app.themeInUse === 'dark'; - $: imgSrc = isDark - ? $isSmallViewport - ? EmptyDarkMobile - : EmptyDark - : $isSmallViewport - ? EmptyLightMobile - : EmptyLight; - $: imgClass = $isSmallViewport ? 'mobile' : 'desktop'; - - function addToWaitlist() { - joinWaitlistSites($user); - addNotification({ - type: 'success', - title: 'Waitlist joined', - message: "We'll let you know as soon as Appwrite Sites is ready for you." - }); - - isOnWaitlist = true; - } - {#if data.sitesLive} - - - - - - - {#if $canWriteSites} - - {/if} - + + + - {#if data.siteList.total} - {#if data.view === View.Grid} - - {:else} - + + + {#if $canWriteSites} + {/if} - - {:else if data.search} - + + + {#if data.siteList.total} + {#if data.view === View.Grid} + {:else} - (show = true)}> - +
    {/if} + + {:else if data.search} + {:else} - - - - - - {#if isOnWaitlist} - - You've successfully joined the Sites waitlist - - - - We can't wait for you to try out Sites on Cloud. You will get access - soon. - - {:else} - - - Appwrite Sites is in high demand - - -
    - - To ensure a smooth experience for everyone, we’re rolling out - access gradually. Join the waitlist and be one of the first to - deploy with Sites. - -
    - -
    - -
    -
    - {/if} -
    -
    -
    + (show = true)}> + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.ts b/src/routes/(console)/project-[region]-[project]/sites/+page.ts index e1cc8f1b6..6e711b2d5 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.ts @@ -1,12 +1,9 @@ -import { Query, type Models } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; -import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; +import { Query } from '@appwrite.io/console'; import { CARD_LIMIT, Dependencies } from '$lib/constants'; -import { flags } from '$lib/flags'; - -export const load = async ({ url, depends, route, params, parent }) => { - const data = await parent(); +import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; +export const load = async ({ url, depends, route, params }) => { depends(Dependencies.SITES); const page = getPage(url); const search = getSearch(url); @@ -14,22 +11,7 @@ export const load = async ({ url, depends, route, params, parent }) => { const offset = pageToOffset(page, limit); const view = getView(url, route, View.Grid, View.Grid); - if (!flags.showSites(data)) { - return { - sitesLive: false, - offset, - limit, - search, - view, - siteList: { - total: 0, - sites: [] - } as Models.SiteList - }; - } - return { - sitesLive: true, offset, limit, search, From 93a75e468bca01fd2c09000cb5d2f1509a37ebe0 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 31 Aug 2025 20:02:41 +0530 Subject: [PATCH 43/69] remove: feature flags from envs. --- .env.example | 2 +- .github/workflows/publish.yml | 8 ++++---- src/lib/flags.ts | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 5a16f982f..9ba4011c0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ PUBLIC_CONSOLE_MODE=self-hosted -PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths +PUBLIC_CONSOLE_FEATURE_FLAGS= PUBLIC_APPWRITE_MULTI_REGION=false PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1 PUBLIC_STRIPE_KEY= diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a700a4add..8cfa0c03f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | "PUBLIC_CONSOLE_MODE=cloud" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}" @@ -79,7 +79,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | "PUBLIC_CONSOLE_MODE=cloud" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" @@ -118,7 +118,7 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=self-hosted" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" publish-cloud-no-regions: @@ -156,6 +156,6 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" diff --git a/src/lib/flags.ts b/src/lib/flags.ts index 724986186..dfbedecd9 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -19,6 +19,4 @@ function isFlagEnabled(name: string) { }; } -export const flags = { - -}; +export const flags = {}; From 0747cdda5ca0776e78997d7089bdc0a4b5b2a727 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 31 Aug 2025 20:03:19 +0530 Subject: [PATCH 44/69] fix: number coercing! --- src/lib/elements/forms/inputNumber.svelte | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/lib/elements/forms/inputNumber.svelte b/src/lib/elements/forms/inputNumber.svelte index f18aff0fa..43fb41538 100644 --- a/src/lib/elements/forms/inputNumber.svelte +++ b/src/lib/elements/forms/inputNumber.svelte @@ -18,20 +18,6 @@ let error: string; - function coerceToNumber(event: CustomEvent) { - const raw = event.detail ?? ''; - - if (raw === '') { - value = nullable ? null : (undefined as unknown as number); - return; - } - - const parsed = Number(raw); - if (Number.isFinite(parsed)) { - value = parsed; - } - } - const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => { event.preventDefault(); @@ -74,8 +60,7 @@ autofocus={autofocus || undefined} helper={error || helper} state={error ? 'error' : 'default'} - on:invalid={handleInvalid} - on:change={coerceToNumber}> + on:invalid={handleInvalid}> From ea991293d376100f285096c8739ff225e485019b Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 31 Aug 2025 20:05:40 +0530 Subject: [PATCH 45/69] fix: re-add the deleted images. --- .../(images)/empty-sites-dark-mobile.svg | 119 +++++++ .../sites/(images)/empty-sites-dark.svg | 239 +++++++++++++ .../(images)/empty-sites-light-mobile.svg | 157 +++++++++ .../sites/(images)/empty-sites-light.svg | 321 ++++++++++++++++++ 4 files changed, 836 insertions(+) create mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg create mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg create mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg create mode 100644 src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg new file mode 100644 index 000000000..39c7917cf --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark-mobile.svg @@ -0,0 +1,119 @@ + + +
    + + + +
    + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg new file mode 100644 index 000000000..b64811a48 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-dark.svg @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg new file mode 100644 index 000000000..8bd4b239e --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light-mobile.svg @@ -0,0 +1,157 @@ + + +
    + + + + +
    +
    + + + + +
    +
    + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg new file mode 100644 index 000000000..a0314acfc --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/sites/(images)/empty-sites-light.svg @@ -0,0 +1,321 @@ + + + + + + + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    From e5dfdc1fb79c51220481f712a231dc8169ee3681 Mon Sep 17 00:00:00 2001 From: Darshan Date: Sun, 31 Aug 2025 20:11:30 +0530 Subject: [PATCH 46/69] remove: waitlist helper. --- src/lib/helpers/waitlist.ts | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/lib/helpers/waitlist.ts diff --git a/src/lib/helpers/waitlist.ts b/src/lib/helpers/waitlist.ts deleted file mode 100644 index d0ff50e85..000000000 --- a/src/lib/helpers/waitlist.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sdk } from '$lib/stores/sdk'; -import { type Account } from '$lib/stores/user'; - -export const joinWaitlistSites = (user: Account) => { - const prefs = user.prefs; - const newPrefs = { - ...prefs, - joinWaitlistSites: true - }; - - sdk.forConsole.account.updatePrefs({ prefs: newPrefs }); - - if (sessionStorage) { - sessionStorage.setItem('joinWaitlistSites', 'true'); - } -}; - -export const isOnWaitlistSites = (user: Account): boolean => { - const prefs = user.prefs; - const joinedInPrefs = 'joinWaitlistSites' in prefs; - - let joinedInSession = false; - if (sessionStorage) { - joinedInSession = sessionStorage.getItem('joinWaitlistSites') === 'true'; - } - - return joinedInSession || joinedInPrefs; -}; From e3272cc5e7e140e16550302707d85fabd0c62589 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:58:38 +0530 Subject: [PATCH 47/69] fix: small fixes --- src/lib/components/archiveProject.svelte | 37 ++- src/lib/components/billing/usageRates.svelte | 4 +- .../components/organizationUsageLimits.svelte | 26 +- src/lib/layout/createProject.svelte | 7 +- .../organization-[organization]/+page.svelte | 14 +- .../billing/planSummary.svelte | 306 +++++++++--------- .../change-plan/+page.svelte | 33 +- 7 files changed, 202 insertions(+), 225 deletions(-) diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte index 6fbd32589..0fbec6ad5 100644 --- a/src/lib/components/archiveProject.svelte +++ b/src/lib/components/archiveProject.svelte @@ -8,7 +8,8 @@ Tag, Accordion, ActionMenu, - Popover + Popover, + Layout } from '@appwrite.io/pink-svelte'; import { IconAndroid, @@ -25,7 +26,6 @@ import { getPlatformInfo } from '$lib/helpers/platform'; import type { Models } from '@appwrite.io/console'; import type { ComponentType } from 'svelte'; - import { organization, currentPlan } from '$lib/stores/organization'; import { BillingPlan } from '$lib/constants'; import { goto } from '$app/navigation'; import { base } from '$app/paths'; @@ -41,9 +41,14 @@ // props interface Props { projectsToArchive: Models.Project[]; + organization: Models.Organization; + currentPlan: { + projects: number; + [key: string]: any; + }; } - let { projectsToArchive }: Props = $props(); + let { projectsToArchive, organization, currentPlan }: Props = $props(); // Track Read-only info droplist per archived project let readOnlyInfoOpen = $state>({}); @@ -77,14 +82,11 @@ // Check if unarchive should be disabled function isUnarchiveDisabled(): boolean { - const org = $organization; - const plan = $currentPlan; + if (!organization || !currentPlan) return true; - if (!org || !plan) return true; - - if (org.billingPlan === BillingPlan.FREE) { - const currentProjectCount = org.projects?.length || 0; - const projectLimit = plan.projects || 0; + if (organization.billingPlan === BillingPlan.FREE) { + const currentProjectCount = organization.projects?.length || 0; + const projectLimit = currentPlan.projects || 0; return currentProjectCount >= projectLimit; } @@ -107,8 +109,7 @@ if (!projectToUnarchive) return; try { - const org = $organization; - if (!org) { + if (!organization) { addNotification({ type: 'error', message: 'Organization not found' @@ -116,12 +117,12 @@ return; } - const currentSelectedProjects = org.projects ?? []; + const currentSelectedProjects = organization.projects ?? []; const updatedProjects = Array.from( new Set([...currentSelectedProjects, projectToUnarchive.$id]) ); - await sdk.forConsole.billing.updateSelectedProjects(org.$id, updatedProjects); + await sdk.forConsole.billing.updateSelectedProjects(organization.$id, updatedProjects); await invalidate(Dependencies.ORGANIZATION); @@ -272,9 +273,11 @@

    Are you sure you want to unarchive {projectToUnarchive?.name}?

    This will move the project back to your active projects list.

    - - - + + + + + diff --git a/src/lib/components/billing/usageRates.svelte b/src/lib/components/billing/usageRates.svelte index 3a3ea9291..49ab414e9 100644 --- a/src/lib/components/billing/usageRates.svelte +++ b/src/lib/components/billing/usageRates.svelte @@ -4,7 +4,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { type Organization } from '$lib/stores/organization'; import { plansInfo } from '$lib/stores/billing'; - import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers'; + import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; import { Table, Typography } from '@appwrite.io/pink-svelte'; @@ -22,7 +22,7 @@ // equal or above means unlimited! const getCorrectSeatsCountValue = (count: number): string | number => { // Check for Infinity or very large numbers - const isUnlimited = count === Infinity || count >= Number.MAX_SAFE_INTEGER; + const isUnlimited = count === Infinity || !isWithinSafeRange(count); return isUnlimited ? 'Unlimited' : count || 0; }; diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index 6d2aa65cf..aa4348263 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -1,6 +1,6 @@ - + Apply Appwrite credits to your organization. diff --git a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte index 1bb41f4f2..52bc76859 100644 --- a/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte +++ b/src/routes/(console)/organization-[organization]/billing/availableCredit.svelte @@ -7,7 +7,6 @@ import { wizard } from '$lib/stores/wizard'; import { Query } from '@appwrite.io/console'; import { onMount } from 'svelte'; - import AddCreditWizard from './addCreditWizard.svelte'; import { Button } from '$lib/elements/forms'; import AddCreditModal from './addCreditModal.svelte'; import { formatCurrency } from '$lib/helpers/numbers'; @@ -33,15 +32,6 @@ const limit = 5; - function handleCredits() { - if ($organization?.paymentMethodId || $organization?.backupPaymentMethodId) { - show = true; - } else { - wizard.start(AddCreditWizard); - reloadOnWizardClose = true; - } - } - async function request() { if (!$organization?.$id) return; @@ -116,7 +106,7 @@ content={formatCurrency(creditList.available)} /> {#if creditList?.total} - @@ -176,7 +166,7 @@ {/if} {:else} - Add credits + (show = true)}>Add credits {/if} {/if} From 53a2289c60ba7000cd47e83073a1c24181b5e653 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:17:54 +0530 Subject: [PATCH 49/69] fix: small fix in alert --- src/lib/layout/createProject.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index c426d613f..52915971d 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -107,8 +107,7 @@ compact size="s" href={`${base}/organization-${page.params.organization}/billing`} - external - text>Upgrade + external>Upgrade {/if} From 6982ae6d0ad0a284a5ca20373c2a6accc5214c4e Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:05:01 +0530 Subject: [PATCH 50/69] fix: small ui fixes --- .../components/progressbar/ProgressBar.svelte | 1 + .../organization-[organization]/+page.svelte | 21 ++------- .../billing/planSummary.svelte | 46 +++++++------------ 3 files changed, 21 insertions(+), 47 deletions(-) diff --git a/src/lib/components/progressbar/ProgressBar.svelte b/src/lib/components/progressbar/ProgressBar.svelte index 58b3a3a39..a4cebc8ee 100644 --- a/src/lib/components/progressbar/ProgressBar.svelte +++ b/src/lib/components/progressbar/ProgressBar.svelte @@ -66,6 +66,7 @@ flex-direction: row; gap: 2px; margin-top: 1rem; + overflow: hidden; } &__content { diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index b1fd6ac5e..6c90cf669 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -144,26 +144,11 @@ {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)} 0 - ? `${projectsToArchive.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}` - : `Your organization has exceeded the project limit. Projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}> - - {#if projectsToArchive.length > 0} - {#each projectsToArchive as project, index}{@const text = `${project.name}`} - {@html text}{index == projectsToArchive.length - 2 - ? ', and ' - : index < projectsToArchive.length - 1 - ? ', ' - : ''} - {/each} - will be archived - {/if} - + title={`${projectsToArchive.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}> + Upgrade your plan to restore archived projects - - - handleUnarchiveProject(project)} - >Unarchive project - handleMigrateProject(project)} - >Migrate project - - - - - - {#each platforms.slice(0, 2) as platform} - {@const icon = getIconForPlatform(platform.icon)} - - - - {/each} - - {#if platforms.length > 3} - - {/if} - - - {#if isCloud && $regionsStore?.regions} - {@const region = findRegion(project)} - {region?.name} - {/if} - - - {/each} - - - - -{/if} - - - -

    Are you sure you want to unarchive {projectToUnarchive?.name}?

    -

    This will move the project back to your active projects list.

    - - - - - - - -
    - - diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte index 4885db75e..11d832107 100644 --- a/src/lib/components/billing/alerts/limitReached.svelte +++ b/src/lib/components/billing/alerts/limitReached.svelte @@ -20,14 +20,9 @@ plan. Consider upgrading to increase your resource usage. - {#if !page.data.currentPlan?.usagePerProject} - - {/if} + - -
    - {/if} - -
    - - - Resource - Free limit - - - Excess usage - - - Usage beyond the Free plan limits. - - - - - - - - - - - Projects - {#if isLimitExceeded.projects} - - {/if} - - - - {formatNumber(allowedProjectsToKeep)} projects - - - {#if isLimitExceeded.projects} - - - - {formatNumber(excessUsage.projects)} projects - - - {:else} - - {formatNumber(currentUsage.projects)} / {formatNumber( - allowedProjectsToKeep - )} - - {/if} - - - {#if isLimitExceeded.projects} - - - - {/if} - - - - - - - Organization members - - - {formatNumber(freePlanLimits.members)} member - - - {#if isLimitExceeded.members} - - - - {formatNumber(excessUsage.members)} members - - - {:else} - - {formatNumber(currentUsage.members)} / {formatNumber( - freePlanLimits.members - )} - - {/if} - - - - - - - - Storage - - - {freePlanLimits.storage} GB - - - {#if isLimitExceeded.storage} - - - - {excessUsage.storage.toFixed(2)} GB - - - {:else} - - {storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB - - {/if} - - - - -
    - - - - - Choose which two projects to keep. Projects over the limit will be blocked after this date. - - - {#if error} - {error} - {/if} - -
    - - - Project Name - Created - - {#each projects as project} - - {project.name} - - {toLocaleDateTime(project.$createdAt)} - - - {/each} - -
    - {#if selectedProjects.length === allowedProjectsToKeep} - {@const difference = projects.length - selectedProjects.length} - {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`} - - {formatProjectsToArchive()} will be archived - - {/if} - - - - - -
    - - diff --git a/src/lib/components/progressbar/ProgressBar.svelte b/src/lib/components/progressbar/ProgressBar.svelte index a4cebc8ee..58b3a3a39 100644 --- a/src/lib/components/progressbar/ProgressBar.svelte +++ b/src/lib/components/progressbar/ProgressBar.svelte @@ -66,7 +66,6 @@ flex-direction: row; gap: 2px; margin-top: 1rem; - overflow: hidden; } &__content { diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts index 91dd9a37c..c138a7483 100644 --- a/src/lib/helpers/string.ts +++ b/src/lib/helpers/string.ts @@ -49,19 +49,6 @@ export function formatNum(number: number): string { return formatter.format(number); } -/** - * Format a string with optional mobile-aware truncation. - */ -export function formatName( - name: string, - limit: number = 19, - isSmallViewport: boolean = false -): string { - const mobileLimit = 16; - const actualLimit = isSmallViewport ? mobileLimit : limit; - return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; -} - /** * Returns a regex to check hostname validity. Supports wildcards too! */ diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 52915971d..5d951babd 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 { currentPlan } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { base } from '$app/paths'; import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { filterRegions } from '$lib/helpers/regions'; import type { Snippet } from 'svelte'; - import { BillingPlan } from '$lib/constants'; - import { formatCurrency } from '$lib/helpers/numbers'; let { projectName = $bindable(''), @@ -20,7 +18,6 @@ regions = [], region = $bindable(''), showTitle = true, - billingPlan = undefined, projects = undefined, submit }: { @@ -29,17 +26,13 @@ regions: Array; region: string; showTitle: boolean; - billingPlan?: BillingPlan; projects?: number; submit?: Snippet; } = $props(); let showCustomId = $state(false); - let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO); let projectsLimited = $derived( - isProPlan - ? projects && projects >= 2 - : $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects + $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects ); @@ -53,12 +46,26 @@ {#if showTitle} Create your project {/if} - + {#if projectsLimited} + + Extra projects are available on paid plans for an additional fee + + + + + {/if} - {#if isCloud && regions.length > 0} Region cannot be changed after creation {/if} - {#if projectsLimited} - {#if isProPlan} - - Each added project comes with its own dedicated pool of resources. - - {:else} - - Extra projects are available on paid plans for an additional fee - - - - - {/if} - {/if} {@render submit?.()} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 7ae79e199..3dbd0b9f9 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -151,79 +151,6 @@ export type CreditList = { total: number; }; -export type AggregationTeam = { - $id: string; - /** - * Aggregation creation time in ISO 8601 format. - */ - $createdAt: string; - /** - * Aggregation update date in ISO 8601 format. - */ - $updatedAt: string; - /** - * Beginning date of the invoice. - */ - from: string; - /** - * End date of the invoice. - */ - to: string; - /** - * Total amount of the invoice. - */ - amount: number; - additionalMembers: number; - - /** - * Price for additional members - */ - additionalMemberAmount: number; - /** - * Total storage usage. - */ - usageStorage: number; - /** - * Total active users for the billing period. - */ - usageUsers: number; - /** - * Total number of executions for the billing period. - */ - usageExecutions: number; - /** - * Total bandwidth usage for the billing period. - */ - usageBandwidth: number; - /** - * Total realtime usage for the billing period. - */ - usageRealtime: number; - /** - * Usage logs for the billing period. - */ - resources: InvoiceUsage[]; - /** - * Aggregation billing plan - */ - plan: string; - breakdown: AggregationBreakdown[]; -}; - -export type AggregationBreakdown = { - $id: string; - name: string; - amount: number; - region: string; - resources: InvoiceUsage[]; -}; - -export type InvoiceUsage = { - resourceId: string; - value: number; - amount: number; -}; - export type AvailableCredit = { available: number; }; @@ -389,13 +316,10 @@ export type Plan = { projects: number; databases: number; databasesAllowEncrypt: boolean; - databasesReads: number; - databasesWrites: number; buckets: number; fileSize: number; functions: number; executions: number; - GBHours: number; realtime: number; logs: number; authPhone: number; @@ -406,14 +330,9 @@ export type Plan = { realtime: AdditionalResource; storage: AdditionalResource; users: AdditionalResource; - databasesReads: AdditionalResource; - databasesWrites: AdditionalResource; - GBHours: AdditionalResource; - imageTransformations: AdditionalResource; }; addons: { seats: PlanAddon; - projects: PlanAddon; }; trialDays: number; budgetCapEnabled: boolean; @@ -429,7 +348,6 @@ export type Plan = { supportsOrganizationRoles: boolean; buildSize: number; // in MB deploymentSize: number; // in MB - usagePerProject: boolean; }; export type PlanList = { @@ -437,7 +355,7 @@ export type PlanList = { total: number; }; -export type PlansMap = Map; +export type PlansMap = Map; export type Roles = { scopes: string[]; @@ -574,22 +492,6 @@ export class Billing { }); } - async listPlans(queries: string[] = []): Promise { - const path = `/console/plans`; - const uri = new URL(this.client.config.endpoint + path); - const params = { - queries - }; - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); - } - async getPlan(planId: string): Promise { const path = `/console/plans/${planId}`; const uri = new URL(this.client.config.endpoint + path); @@ -933,7 +835,7 @@ export class Billing { ); } - async getAggregation(organizationId: string, aggregationId: string): Promise { + async getAggregation(organizationId: string, aggregationId: string): Promise { const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; const params = { organizationId, diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 2a88fa83a..993e01fbe 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -14,7 +14,7 @@ import { cachedStore } from '$lib/helpers/cache'; import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion'; import type { AddressesList, - AggregationTeam, + Aggregation, Invoice, InvoiceList, PaymentList, @@ -69,6 +69,7 @@ export const roles = [ export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; +export const billingProjectsLimitDate = '2025-09-01'; export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList); export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList); @@ -161,13 +162,8 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan // the correct info for members/seats, resides in `addons`. // plan > addons > seats/others if (serviceId === 'members') { - // pro and scale plans have unlimited seats (per-project NEW pricing model) - const currentTier = tier ?? get(organization)?.billingPlan; - if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { - return Infinity; // unlimited seats for Pro and Scale plans - } - // Free plan still has 1 member limit - return (plan?.['addons']['seats'] || [])['limit'] ?? 1; + // some don't include `limit`, so we fallback! + return plan?.['addons']['seats']['limit'] ?? 1; } return plan?.[serviceId] ?? 0; @@ -239,7 +235,6 @@ export const tierEnterprise: TierData = { }; export const showUsageRatesModal = writable(false); -export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true); export function checkForUsageFees(plan: Tier, id: PlanServices) { if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) { @@ -258,19 +253,11 @@ export function checkForUsageFees(plan: Tier, id: PlanServices) { } export function checkForProjectLimitation(id: PlanServices) { - // Members are no longer limited on Pro and Scale plans (unlimited seats) - if (id === 'members') { - const currentTier = get(organization)?.billingPlan; - if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { - return false; // No project limitation for members on Pro/Scale plans - } - } - switch (id) { case 'databases': case 'functions': case 'buckets': - case 'members': // Only applies to Free plan now + case 'members': case 'platforms': case 'webhooks': case 'teams': @@ -338,8 +325,7 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?: if (!plan) return; if (plan.$id !== BillingPlan.FREE) return; - if (!org.projects) return; - if (org.projects.length > 0) return; + if (org.projects?.length > 0) return; const projectCount = orgProjectCount; if (projectCount === undefined) return; @@ -397,8 +383,13 @@ export async function checkForUsageLimit(org: Organization) { ]; const members = org.total; - const memberLimit = getServiceLimit('members'); - const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit); + const plan = get(currentPlan); + const membersOverflow = + // `plan` can be null on `onboarding/create-organization` route. + // nested null checks needed: GitHub Education plan have empty addons. + members > plan?.addons?.seats?.limit + ? members - (plan?.addons?.seats?.limit || members) + : 0; if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); @@ -618,7 +609,7 @@ export const billingURL = derived( export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account']; -export function calculateExcess(addon: AggregationTeam, plan: Plan) { +export function calculateExcess(addon: Aggregation, plan: Plan) { return { bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth), storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index 3cf1478a3..5aa188f90 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -1,7 +1,7 @@ -import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; import { isCloud } from '$lib/system'; import type { LayoutLoad } from './$types'; +import { Dependencies } from '$lib/constants'; import type { Tier } from '$lib/stores/billing'; import type { Plan, PlanList } from '$lib/sdk/billing'; import { Query } from '@appwrite.io/console'; diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts index 57d6280fe..690baa2bb 100644 --- a/src/routes/(console)/create-organization/+page.ts +++ b/src/routes/(console)/create-organization/+page.ts @@ -7,10 +7,10 @@ import type { Organization } from '$lib/stores/organization'; export const load: PageLoad = async ({ url, parent, depends }) => { const { organizations } = await parent(); depends(Dependencies.ORGANIZATIONS); - const [coupon, paymentMethods, plans] = await Promise.all([ + + const [coupon, paymentMethods] = await Promise.all([ getCoupon(url), - sdk.forConsole.billing.listPaymentMethods(), - sdk.forConsole.billing.listPlans() + sdk.forConsole.billing.listPaymentMethods() ]); let plan = getPlanFromUrl(url); const hasFreeOrganizations = organizations.teams?.some( @@ -24,7 +24,6 @@ export const load: PageLoad = async ({ url, parent, depends }) => { return { plan, coupon, - plans, hasFreeOrganizations, paymentMethods, name: url.searchParams.get('name') ?? '' diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 2339498ec..10169fe81 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -7,7 +7,6 @@ import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; import { page } from '$app/state'; import { registerCommands } from '$lib/commandCenter'; - import { formatName as formatNameHelper } from '$lib/helpers/string'; import { CardContainer, Empty, @@ -18,7 +17,7 @@ } from '$lib/components'; import { trackEvent, Click } from '$lib/actions/analytics'; import { type Models } from '@appwrite.io/console'; - import { readOnly, upgradeURL } from '$lib/stores/billing'; + import { billingProjectsLimitDate, readOnly, upgradeURL } from '$lib/stores/billing'; import { onMount, type ComponentType } from 'svelte'; import { canWriteProjects } from '$lib/stores/roles'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; @@ -35,9 +34,8 @@ } from '@appwrite.io/pink-icons-svelte'; import { getPlatformInfo } from '$lib/helpers/platform'; import CreateProjectCloud from './createProjectCloud.svelte'; - import { currentPlan, organization, regions as regionsStore } from '$lib/stores/organization'; + import { currentPlan, regions as regionsStore } from '$lib/stores/organization'; import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte'; - import ArchiveProject from '$lib/components/archiveProject.svelte'; import { toLocaleDate } from '$lib/helpers/date'; export let data; @@ -101,16 +99,24 @@ function isSetToArchive(project: Models.Project): boolean { if (!isCloud) return false; + if (data.organization.projects?.length === 0) return false; if (!project || !project.$id) return false; - return project.status !== 'active'; + return !data.organization.projects?.includes(project.$id); } - $: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active'); + function formatName(name: string, limit: number = 19) { + const mobileLimit = 16; + const actualLimit = $isSmallViewport ? mobileLimit : limit; + return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; + } - $: activeProjects = data.projects.projects.filter((project) => project.status === 'active'); function clearSearch() { searchQuery?.clearInput(); } + + $: projectsToArchive = data.projects.projects.filter( + (project) => !data.organization.projects?.includes(project.$id) + ); - {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)} - {@const difference = projectsToArchive} - {@const messagePrefix = - difference.length > 1 ? `${difference} projects` : `${difference} project`} + {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > $currentPlan.projects && $canWriteProjects} - Upgrade your plan to restore archived projects + title={`${data.projects.total - data.organization.projects.length} projects will be archived on ${toLocaleDate(billingProjectsLimitDate)}`}> + + {#each projectsToArchive as project, index}{@const text = `${project.name}`} + {@html text}{index === projectsToArchive.length - 2 + ? ', and ' + : index < projectsToArchive.length - 1 + ? ', ' + : ''} + {/each} + will be archived + + @@ -166,7 +176,7 @@ {/if} {:else} - (show = true)}>Add credits + Add credits {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte index ac649dd83..44c9c9710 100644 --- a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte +++ b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte @@ -28,12 +28,7 @@ } - +

    Your organization is set to change to {tierToPlan($organization?.billingPlanDowngrade).name} diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 51c39a931..3f27119c9 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -1,533 +1,180 @@ {#if $organization} - - {currentPlan.name} plan + + Payment estimates + A breakdown of your estimated upcoming payment for the current billing period. Totals displayed + exclude accumulated credits and applicable taxes. + +

    + Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} +

    + + + + + {currentPlan.name} plan + + + {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} + + - {#if totalAmount > 0} - - Next payment of {formatCurrency(totalAmount)} - will occur on - {toLocaleDate($organization?.billingNextInvoiceDate)}. - - {/if} - -
    - - Current billing cycle ({new Date( - $organization?.billingCurrentInvoiceDate - ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}-{new Date( - $organization?.billingNextInvoiceDate - ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}) - - - Estimate, subject to change based on usage. - -
    - -
    - - {#each billingData as row} - - {#each columns as col} - root.toggle(row.id)}> - {#if col.id === 'item'} -
    - - {row.cells?.[col.id] ?? ''} - -
    - {:else} - - {row.cells?.[col.id] ?? ''} - + {#if currentPlan.budgeting && extraUsage > 0} + 0 + ? currentInvoice.usage.length + 1 + : currentInvoice.usage.length + ).toString()}> + + {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} + + + {#if currentAggregation.additionalMembers} + + + Additional members + + {formatCurrency( + currentAggregation.additionalMemberAmount + )} + + + + {currentAggregation.additionalMembers} + + {/if} -
    - {/each} + {#if currentInvoice?.usage} + {#each currentInvoice.usage as excess, i} + {#if i > 0 || currentAggregation.additionalMembers} + + {/if} - - {#if row.children} - {#each row.children as child (child.id)} - - {/each} -
    - {/each} - {/if} - - - {/each} - {#if availableCredit > 0} - - {}}> - - - - Credits + + + + {excess.name} + + + {formatCurrency(excess.amount)} + + + + + + {formatNumberWithCommas(excess.value)} + + {abbreviateNumber(excess.value)} + + + + {/each} + {/if} - - {}}> - - - - {}}> - - -{formatCurrency(creditsApplied)} - - - - {/if} + + {/if} - - {}}> - - Total - - - {}}> - - - - {}}> - - {formatCurrency(totalAmount)} - - - - - + {#if currentPlan.supportsCredits && availableCredit > 0} + + + + Credits to be applied + + + -{formatCurrency( + Math.min(availableCredit, currentInvoice?.amount ?? 0) + )} + + + {/if} - -
    + {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + + + + + Current total (USD) + + + + Estimates are updated daily and may differ from your + final invoice. + + + + + + {formatCurrency( + Math.max( + (currentInvoice?.amount ?? 0) - + Math.min(availableCredit, currentInvoice?.amount ?? 0), + 0 + ) + )} + + + {/if} + + + + {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
    - {#if !currentPlan?.usagePerProject} - - {/if} + class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end"> + {:else} @@ -557,24 +204,20 @@ Change plan {/if} - {#if !currentPlan?.usagePerProject} - - {/if} +
    {/if} -
    - + + {/if} diff --git a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte index 338be255e..ca84dbf21 100644 --- a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte +++ b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte @@ -17,14 +17,9 @@ Appwrite services, update the budget limit. - {#if !page.data.currentPlan?.usagePerProject} - - {/if} + diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index 934ddf3b5..b5ef36e43 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -4,6 +4,7 @@ import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { PlanComparisonBox, PlanSelection, SelectPaymentMethod } from '$lib/components/billing'; + import PlanExcess from '$lib/components/billing/planExcess.svelte'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants'; import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms'; @@ -32,11 +33,7 @@ import { onMount } from 'svelte'; import { loadAvailableRegions } from '$routes/(console)/regions'; import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte'; - import OrganizationUsageLimits from '$lib/components/organizationUsageLimits.svelte'; import { Query } from '@appwrite.io/console'; - import type { OrganizationUsage } from '$lib/sdk/billing'; - import type { Models } from '@appwrite.io/console'; - import { toLocaleDate } from '$lib/helpers/date'; export let data; @@ -46,9 +43,6 @@ let previousPage: string = base; let showExitModal = false; let formComponent: Form; - let usageLimitsComponent: - | { validateOrAlert: () => boolean; getSelectedProjects: () => string[] } - | undefined; let isSubmitting = writable(false); let collaborators: string[] = data?.members?.memberships @@ -61,8 +55,6 @@ let showCreditModal = false; let feedbackDowngradeReason: string; let feedbackMessage: string; - let orgUsage: OrganizationUsage; - let allProjects: { projects: Models.Project[] } | undefined; $: paymentMethods = null; @@ -99,21 +91,6 @@ selectedPlan = $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO; - - try { - orgUsage = await sdk.forConsole.billing.listUsage(data.organization.$id); - } catch { - orgUsage = undefined; - } - - try { - allProjects = await sdk.forConsole.projects.list([ - Query.equal('teamId', data.organization.$id), - Query.limit(1000) - ]); - } catch { - allProjects = { projects: [] }; - } }); async function loadPaymentMethods() { @@ -123,13 +100,6 @@ async function handleSubmit() { if (isDowngrade) { - // If target plan has a non-zero project limit, ensure selection made - const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; - if (targetProjectsLimit > 0 && usageLimitsComponent?.validateOrAlert) { - const ok = usageLimitsComponent.validateOrAlert(); - if (!ok) return; - } - await downgrade(); } else if (isUpgrade) { await upgrade(); @@ -166,7 +136,6 @@ async function downgrade() { try { - // 1) update the plan first await sdk.forConsole.billing.updatePlan( data.organization.$id, selectedPlan, @@ -174,22 +143,6 @@ null ); - // 2) If the target plan has a project limit, apply selected projects now - const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; - if (targetProjectsLimit > 0 && usageLimitsComponent) { - const selected = usageLimitsComponent.getSelectedProjects(); - if (selected?.length) { - try { - await sdk.forConsole.billing.updateSelectedProjects( - data.organization.$id, - selected - ); - } catch (projectError) { - console.warn('Project selection failed after plan update:', projectError); - } - } - } - await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]); await goto(previousPage); @@ -355,12 +308,14 @@ {/if} {#if isDowngrade} - {@const extraMembers = collaborators?.length ?? 0} - {@const price = formatCurrency( - extraMembers * - ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0) - )} - {#if selectedPlan === BillingPlan.PRO} + {#if selectedPlan === BillingPlan.FREE && !data.hasFreeOrgs} + + {:else if selectedPlan === BillingPlan.PRO && data.organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0} + {@const extraMembers = collaborators?.length ?? 0} + {@const price = formatCurrency( + extraMembers * + ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0) + )} Your monthly payments will be adjusted for the Pro plan @@ -370,26 +325,7 @@ >you will be charged {price} monthly for {extraMembers} team members. This will be reflected in your next invoice. - {:else if selectedPlan === BillingPlan.FREE} - - You will retain access to {tierToPlan($organization.billingPlan) - .name} plan features until your billing period ends. After that, - all team members except the owner will be removed, - and service disruptions may occur if usage exceeds Free plan limits. - {/if} - - {/if}
    diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index c6a137f96..ecb712008 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -1,20 +1,11 @@ import type { PageLoad } from './$types'; import type { Organization } from '$lib/stores/organization'; import { BillingPlan, Dependencies } from '$lib/constants'; -import { sdk } from '$lib/stores/sdk'; export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); - let plans; - try { - plans = await sdk.forConsole.billing.listPlans(); - } catch (error) { - console.error('Failed to load billing plans:', error); - plans = { plans: {} }; - } - let plan: BillingPlan; if (currentPlan?.$id === BillingPlan.SCALE) { @@ -31,7 +22,6 @@ export const load: PageLoad = async ({ depends, parent }) => { return { members, plan, - plans, selfService, hasFreeOrgs }; diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index 1a81ae48c..f114dcc22 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -66,11 +66,7 @@ event: 'usage', title: 'Usage', hasChildren: true, - disabled: !( - isCloud && - ($isOwner || $isBilling) && - !page.data.currentPlan?.usagePerProject - ) + disabled: !(isCloud && ($isOwner || $isBilling)) }, { href: `${path}/billing`, diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte index 05c350080..4a9f16b99 100644 --- a/src/routes/(console)/organization-[organization]/members/+page.svelte +++ b/src/routes/(console)/organization-[organization]/members/+page.svelte @@ -47,7 +47,9 @@ // Calculate if button should be disabled and tooltip should show $: memberCount = data.organizationMembers?.total ?? 0; $: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1; - $: isButtonDisabled = isCloud ? isFreeWithMembers : false; + $: isButtonDisabled = isCloud + ? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported + : false; const resend = async (member: Models.Membership) => { try { diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index a9422df60..e27353624 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -8,8 +8,7 @@ getServiceLimit, showUsageRatesModal, type Tier, - upgradeURL, - useNewPricingModal + upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import ProjectBreakdown from './ProjectBreakdown.svelte'; @@ -77,32 +76,14 @@ {#if $organization.billingPlan === BillingPlan.SCALE}

    On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. - {#if $useNewPricingModal} - ($showUsageRatesModal = true)}>Learn more - {:else} - - Learn more - - {/if} + resource listed below. ($showUsageRatesModal = true)} + >Learn more

    {:else if $organization.billingPlan === BillingPlan.PRO}

    On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. - {#if $useNewPricingModal} - ($showUsageRatesModal = true)}>Learn more - {:else} - - Learn more - - {/if} + resource listed below. ($showUsageRatesModal = true)} + >Learn more

    {:else if $organization.billingPlan === BillingPlan.FREE}

    diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 31fc89017..24b82ae7b 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -243,7 +243,7 @@ Executions - Calculated for all functions that are executed in this project. + Calculated for all functions that are executed in all projects in your project. {#if executions} {@const current = formatNum(executionsTotal)} diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index e5e1ee5a6..4afa9181a 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,4 +1,4 @@ -import type { AggregationTeam, Invoice, InvoiceUsage } from '$lib/sdk/billing'; +import type { Aggregation, Invoice } from '$lib/sdk/billing'; import { accumulateUsage } from '$lib/sdk/usage'; import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; @@ -11,7 +11,7 @@ export const load: PageLoad = async ({ params, parent }) => { let startDate: string = organization.billingCurrentInvoiceDate; let endDate: string = organization.billingNextInvoiceDate; let currentInvoice: Invoice = undefined; - let currentAggregation: AggregationTeam = undefined; + let currentAggregation: Aggregation = undefined; if (invoice) { currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice); @@ -22,15 +22,6 @@ export const load: PageLoad = async ({ params, parent }) => { startDate = currentInvoice.from; endDate = currentInvoice.to; - } else { - try { - currentAggregation = await sdk.forConsole.billing.getAggregation( - organization.$id, - organization.billingAggregationId - ); - } catch (e) { - // ignore error if no aggregation found - } } const [invoices, usage] = await Promise.all([ @@ -38,24 +29,10 @@ export const load: PageLoad = async ({ params, parent }) => { sdk.forProject(region, project).project.getUsage({ startDate, endDate }) ]); - if (currentAggregation) { - let projectSpecificData = null; - if (currentAggregation.breakdown) { - projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); - } - - if (projectSpecificData) { - const executionsResource = projectSpecificData.resources?.find?.( - (r: InvoiceUsage) => r.resourceId === 'executions' - ); - if (executionsResource) { - usage.executionsTotal = executionsResource.value || usage.executionsTotal; - } - } else { - usage.usersTotal = currentAggregation.usageUsers; - usage.executionsTotal = currentAggregation.usageExecutions; - usage.filesStorageTotal = currentAggregation.usageStorage; - } + if (invoice) { + usage.usersTotal = currentAggregation.usageUsers; + usage.executionsTotal = currentAggregation.usageExecutions; + usage.filesStorageTotal = currentAggregation.usageStorage; } usage.users = accumulateUsage(usage.users, usage.usersTotal); From a6eb2dad9fd5bc70dbcbacb82f5f0076f15fb0cc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Mon, 1 Sep 2025 17:59:29 +0200 Subject: [PATCH 59/69] Revert "Revert "Feat: New Billing UI changes"" --- package.json | 6 +- pnpm-lock.yaml | 30 +- src/lib/components/archiveProject.svelte | 292 +++++++ .../billing/alerts/limitReached.svelte | 11 +- .../billing/alerts/projectsLimit.svelte | 13 +- .../billing/alerts/selectProjectCloud.svelte | 8 +- .../billing/planComparisonBox.svelte | 1 + src/lib/components/billing/planExcess.svelte | 14 +- .../components/billing/planSelection.svelte | 93 +- src/lib/components/billing/usageRates.svelte | 8 +- src/lib/components/estimatedCard.svelte | 10 + src/lib/components/gridItem1.svelte | 2 +- src/lib/components/index.ts | 1 + src/lib/components/labelCard.svelte | 2 +- .../components/organizationUsageLimits.svelte | 351 ++++++++ .../components/progressbar/ProgressBar.svelte | 1 + src/lib/helpers/string.ts | 13 + src/lib/layout/createProject.svelte | 56 +- src/lib/sdk/billing.ts | 102 ++- src/lib/stores/billing.ts | 37 +- src/routes/(console)/+layout.ts | 2 +- .../(console)/create-organization/+page.ts | 7 +- .../organization-[organization]/+page.svelte | 59 +- .../billing/+page.svelte | 23 +- .../billing/addCreditModal.svelte | 2 +- .../billing/availableCredit.svelte | 14 +- .../billing/cancelDowngradeModal.svelte | 7 +- .../billing/planSummary.svelte | 817 ++++++++++++++---- .../billing/planSummaryOld.svelte | 254 ++++++ .../budgetLimitAlert.svelte | 11 +- .../change-plan/+page.svelte | 82 +- .../change-plan/+page.ts | 10 + .../organization-[organization]/header.svelte | 6 +- .../members/+page.svelte | 4 +- .../usage/[[invoice]]/+page.svelte | 29 +- .../settings/usage/[[invoice]]/+page.svelte | 2 +- .../settings/usage/[[invoice]]/+page.ts | 35 +- 37 files changed, 2034 insertions(+), 381 deletions(-) create mode 100644 src/lib/components/archiveProject.svelte create mode 100644 src/lib/components/estimatedCard.svelte create mode 100644 src/lib/components/organizationUsageLimits.svelte create mode 100644 src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte diff --git a/package.json b/package.json index 3104f604a..02cb4f171 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28f2888f7..a6ff6011a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428 - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428 + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134 + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: ^2.0.0-RC.1 - version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -260,8 +260,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9': - resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3700,13 +3700,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3719,7 +3719,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte new file mode 100644 index 000000000..3ebb938c1 --- /dev/null +++ b/src/lib/components/archiveProject.svelte @@ -0,0 +1,292 @@ + + +{#if projectsToArchive.length > 0} +

    + + + These projects have been archived and are read-only. You can view and migrate their + data. + + +
    + + {#each projectsToArchive as project} + {@const platforms = filterPlatforms( + project.platforms.map((platform) => getPlatformInfo(platform.type)) + )} + {@const formatted = formatName(project.name)} + + + {project?.platforms?.length ? project?.platforms?.length : 'No'} apps + + {formatted} + +
    + + { + e.preventDefault(); + e.stopPropagation(); + readOnlyInfoOpen = { + ...readOnlyInfoOpen, + [project.$id]: !readOnlyInfoOpen[project.$id] + }; + }}> + + Read only + + +
  • + + Archived projects are read-only. You can view + and migrate their data, but they no longer + accept edits or requests. + +
  • +
    +
    + + + + handleUnarchiveProject(project)} + >Unarchive project + handleMigrateProject(project)} + >Migrate project + + +
    +
    + + {#each platforms.slice(0, 2) as platform} + {@const icon = getIconForPlatform(platform.icon)} + + + + {/each} + + {#if platforms.length > 3} + + {/if} + + + {#if isCloud && $regionsStore?.regions} + {@const region = findRegion(project)} + {region?.name} + {/if} + +
    + {/each} +
    +
    +
    +
    +{/if} + + + +

    Are you sure you want to unarchive {projectToUnarchive?.name}?

    +

    This will move the project back to your active projects list.

    + + + + + + + +
    + + diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte index 11d832107..4885db75e 100644 --- a/src/lib/components/billing/alerts/limitReached.svelte +++ b/src/lib/components/billing/alerts/limitReached.svelte @@ -20,9 +20,14 @@ plan. Consider upgrading to increase your resource usage. - + {#if !page.data.currentPlan?.usagePerProject} + + {/if} + +
    + {/if} + +
    + + + Resource + Free limit + + + Excess usage + + + Usage beyond the Free plan limits. + + + + + + + + + + + Projects + {#if isLimitExceeded.projects} + + {/if} + + + + {formatNumber(allowedProjectsToKeep)} projects + + + {#if isLimitExceeded.projects} + + + + {formatNumber(excessUsage.projects)} projects + + + {:else} + + {formatNumber(currentUsage.projects)} / {formatNumber( + allowedProjectsToKeep + )} + + {/if} + + + {#if isLimitExceeded.projects} + + + + {/if} + + + + + + + Organization members + + + {formatNumber(freePlanLimits.members)} member + + + {#if isLimitExceeded.members} + + + + {formatNumber(excessUsage.members)} members + + + {:else} + + {formatNumber(currentUsage.members)} / {formatNumber( + freePlanLimits.members + )} + + {/if} + + + + + + + + Storage + + + {freePlanLimits.storage} GB + + + {#if isLimitExceeded.storage} + + + + {excessUsage.storage.toFixed(2)} GB + + + {:else} + + {storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB + + {/if} + + + + +
    + + + + + Choose which two projects to keep. Projects over the limit will be blocked after this date. + + + {#if error} + {error} + {/if} + +
    + + + Project Name + Created + + {#each projects as project} + + {project.name} + + {toLocaleDateTime(project.$createdAt)} + + + {/each} + +
    + {#if selectedProjects.length === allowedProjectsToKeep} + {@const difference = projects.length - selectedProjects.length} + {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`} + + {formatProjectsToArchive()} will be archived + + {/if} + + + + + +
    + + diff --git a/src/lib/components/progressbar/ProgressBar.svelte b/src/lib/components/progressbar/ProgressBar.svelte index 58b3a3a39..a4cebc8ee 100644 --- a/src/lib/components/progressbar/ProgressBar.svelte +++ b/src/lib/components/progressbar/ProgressBar.svelte @@ -66,6 +66,7 @@ flex-direction: row; gap: 2px; margin-top: 1rem; + overflow: hidden; } &__content { diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts index c138a7483..91dd9a37c 100644 --- a/src/lib/helpers/string.ts +++ b/src/lib/helpers/string.ts @@ -49,6 +49,19 @@ export function formatNum(number: number): string { return formatter.format(number); } +/** + * Format a string with optional mobile-aware truncation. + */ +export function formatName( + name: string, + limit: number = 19, + isSmallViewport: boolean = false +): string { + const mobileLimit = 16; + const actualLimit = isSmallViewport ? mobileLimit : limit; + return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; +} + /** * Returns a regex to check hostname validity. Supports wildcards too! */ diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 5d951babd..52915971d 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -4,13 +4,15 @@ import { CustomId } from '$lib/components/index.js'; import { getFlagUrl } from '$lib/helpers/flag'; import { isCloud } from '$lib/system.js'; - import { currentPlan } from '$lib/stores/organization'; + import { currentPlan, organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { base } from '$app/paths'; import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { filterRegions } from '$lib/helpers/regions'; import type { Snippet } from 'svelte'; + import { BillingPlan } from '$lib/constants'; + import { formatCurrency } from '$lib/helpers/numbers'; let { projectName = $bindable(''), @@ -18,6 +20,7 @@ regions = [], region = $bindable(''), showTitle = true, + billingPlan = undefined, projects = undefined, submit }: { @@ -26,13 +29,17 @@ regions: Array; region: string; showTitle: boolean; + billingPlan?: BillingPlan; projects?: number; submit?: Snippet; } = $props(); let showCustomId = $state(false); + let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO); let projectsLimited = $derived( - $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects + isProPlan + ? projects && projects >= 2 + : $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects ); @@ -46,26 +53,12 @@ {#if showTitle} Create your project {/if} - {#if projectsLimited} - - Extra projects are available on paid plans for an additional fee - - - - - {/if} + + {#if isCloud && regions.length > 0} Region cannot be changed after creation {/if} + {#if projectsLimited} + {#if isProPlan} + + Each added project comes with its own dedicated pool of resources. + + {:else} + + Extra projects are available on paid plans for an additional fee + + + + + {/if} + {/if} {@render submit?.()} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 3dbd0b9f9..7ae79e199 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -151,6 +151,79 @@ export type CreditList = { total: number; }; +export type AggregationTeam = { + $id: string; + /** + * Aggregation creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Aggregation update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Beginning date of the invoice. + */ + from: string; + /** + * End date of the invoice. + */ + to: string; + /** + * Total amount of the invoice. + */ + amount: number; + additionalMembers: number; + + /** + * Price for additional members + */ + additionalMemberAmount: number; + /** + * Total storage usage. + */ + usageStorage: number; + /** + * Total active users for the billing period. + */ + usageUsers: number; + /** + * Total number of executions for the billing period. + */ + usageExecutions: number; + /** + * Total bandwidth usage for the billing period. + */ + usageBandwidth: number; + /** + * Total realtime usage for the billing period. + */ + usageRealtime: number; + /** + * Usage logs for the billing period. + */ + resources: InvoiceUsage[]; + /** + * Aggregation billing plan + */ + plan: string; + breakdown: AggregationBreakdown[]; +}; + +export type AggregationBreakdown = { + $id: string; + name: string; + amount: number; + region: string; + resources: InvoiceUsage[]; +}; + +export type InvoiceUsage = { + resourceId: string; + value: number; + amount: number; +}; + export type AvailableCredit = { available: number; }; @@ -316,10 +389,13 @@ export type Plan = { projects: number; databases: number; databasesAllowEncrypt: boolean; + databasesReads: number; + databasesWrites: number; buckets: number; fileSize: number; functions: number; executions: number; + GBHours: number; realtime: number; logs: number; authPhone: number; @@ -330,9 +406,14 @@ export type Plan = { realtime: AdditionalResource; storage: AdditionalResource; users: AdditionalResource; + databasesReads: AdditionalResource; + databasesWrites: AdditionalResource; + GBHours: AdditionalResource; + imageTransformations: AdditionalResource; }; addons: { seats: PlanAddon; + projects: PlanAddon; }; trialDays: number; budgetCapEnabled: boolean; @@ -348,6 +429,7 @@ export type Plan = { supportsOrganizationRoles: boolean; buildSize: number; // in MB deploymentSize: number; // in MB + usagePerProject: boolean; }; export type PlanList = { @@ -355,7 +437,7 @@ export type PlanList = { total: number; }; -export type PlansMap = Map; +export type PlansMap = Map; export type Roles = { scopes: string[]; @@ -492,6 +574,22 @@ export class Billing { }); } + async listPlans(queries: string[] = []): Promise { + const path = `/console/plans`; + const uri = new URL(this.client.config.endpoint + path); + const params = { + queries + }; + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async getPlan(planId: string): Promise { const path = `/console/plans/${planId}`; const uri = new URL(this.client.config.endpoint + path); @@ -835,7 +933,7 @@ export class Billing { ); } - async getAggregation(organizationId: string, aggregationId: string): Promise { + async getAggregation(organizationId: string, aggregationId: string): Promise { const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; const params = { organizationId, diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 993e01fbe..2a88fa83a 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -14,7 +14,7 @@ import { cachedStore } from '$lib/helpers/cache'; import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion'; import type { AddressesList, - Aggregation, + AggregationTeam, Invoice, InvoiceList, PaymentList, @@ -69,7 +69,6 @@ export const roles = [ export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; -export const billingProjectsLimitDate = '2025-09-01'; export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList); export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList); @@ -162,8 +161,13 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan // the correct info for members/seats, resides in `addons`. // plan > addons > seats/others if (serviceId === 'members') { - // some don't include `limit`, so we fallback! - return plan?.['addons']['seats']['limit'] ?? 1; + // pro and scale plans have unlimited seats (per-project NEW pricing model) + const currentTier = tier ?? get(organization)?.billingPlan; + if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { + return Infinity; // unlimited seats for Pro and Scale plans + } + // Free plan still has 1 member limit + return (plan?.['addons']['seats'] || [])['limit'] ?? 1; } return plan?.[serviceId] ?? 0; @@ -235,6 +239,7 @@ export const tierEnterprise: TierData = { }; export const showUsageRatesModal = writable(false); +export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true); export function checkForUsageFees(plan: Tier, id: PlanServices) { if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) { @@ -253,11 +258,19 @@ export function checkForUsageFees(plan: Tier, id: PlanServices) { } export function checkForProjectLimitation(id: PlanServices) { + // Members are no longer limited on Pro and Scale plans (unlimited seats) + if (id === 'members') { + const currentTier = get(organization)?.billingPlan; + if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { + return false; // No project limitation for members on Pro/Scale plans + } + } + switch (id) { case 'databases': case 'functions': case 'buckets': - case 'members': + case 'members': // Only applies to Free plan now case 'platforms': case 'webhooks': case 'teams': @@ -325,7 +338,8 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?: if (!plan) return; if (plan.$id !== BillingPlan.FREE) return; - if (org.projects?.length > 0) return; + if (!org.projects) return; + if (org.projects.length > 0) return; const projectCount = orgProjectCount; if (projectCount === undefined) return; @@ -383,13 +397,8 @@ export async function checkForUsageLimit(org: Organization) { ]; const members = org.total; - const plan = get(currentPlan); - const membersOverflow = - // `plan` can be null on `onboarding/create-organization` route. - // nested null checks needed: GitHub Education plan have empty addons. - members > plan?.addons?.seats?.limit - ? members - (plan?.addons?.seats?.limit || members) - : 0; + const memberLimit = getServiceLimit('members'); + const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit); if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); @@ -609,7 +618,7 @@ export const billingURL = derived( export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account']; -export function calculateExcess(addon: Aggregation, plan: Plan) { +export function calculateExcess(addon: AggregationTeam, plan: Plan) { return { bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth), storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index 5aa188f90..3cf1478a3 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -1,7 +1,7 @@ +import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; import { isCloud } from '$lib/system'; import type { LayoutLoad } from './$types'; -import { Dependencies } from '$lib/constants'; import type { Tier } from '$lib/stores/billing'; import type { Plan, PlanList } from '$lib/sdk/billing'; import { Query } from '@appwrite.io/console'; diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts index 690baa2bb..57d6280fe 100644 --- a/src/routes/(console)/create-organization/+page.ts +++ b/src/routes/(console)/create-organization/+page.ts @@ -7,10 +7,10 @@ import type { Organization } from '$lib/stores/organization'; export const load: PageLoad = async ({ url, parent, depends }) => { const { organizations } = await parent(); depends(Dependencies.ORGANIZATIONS); - - const [coupon, paymentMethods] = await Promise.all([ + const [coupon, paymentMethods, plans] = await Promise.all([ getCoupon(url), - sdk.forConsole.billing.listPaymentMethods() + sdk.forConsole.billing.listPaymentMethods(), + sdk.forConsole.billing.listPlans() ]); let plan = getPlanFromUrl(url); const hasFreeOrganizations = organizations.teams?.some( @@ -24,6 +24,7 @@ export const load: PageLoad = async ({ url, parent, depends }) => { return { plan, coupon, + plans, hasFreeOrganizations, paymentMethods, name: url.searchParams.get('name') ?? '' diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 10169fe81..2339498ec 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -7,6 +7,7 @@ import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; import { page } from '$app/state'; import { registerCommands } from '$lib/commandCenter'; + import { formatName as formatNameHelper } from '$lib/helpers/string'; import { CardContainer, Empty, @@ -17,7 +18,7 @@ } from '$lib/components'; import { trackEvent, Click } from '$lib/actions/analytics'; import { type Models } from '@appwrite.io/console'; - import { billingProjectsLimitDate, readOnly, upgradeURL } from '$lib/stores/billing'; + import { readOnly, upgradeURL } from '$lib/stores/billing'; import { onMount, type ComponentType } from 'svelte'; import { canWriteProjects } from '$lib/stores/roles'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; @@ -34,8 +35,9 @@ } from '@appwrite.io/pink-icons-svelte'; import { getPlatformInfo } from '$lib/helpers/platform'; import CreateProjectCloud from './createProjectCloud.svelte'; - import { currentPlan, regions as regionsStore } from '$lib/stores/organization'; + import { currentPlan, organization, regions as regionsStore } from '$lib/stores/organization'; import SelectProjectCloud from '$lib/components/billing/alerts/selectProjectCloud.svelte'; + import ArchiveProject from '$lib/components/archiveProject.svelte'; import { toLocaleDate } from '$lib/helpers/date'; export let data; @@ -99,24 +101,16 @@ function isSetToArchive(project: Models.Project): boolean { if (!isCloud) return false; - if (data.organization.projects?.length === 0) return false; if (!project || !project.$id) return false; - return !data.organization.projects?.includes(project.$id); + return project.status !== 'active'; } - function formatName(name: string, limit: number = 19) { - const mobileLimit = 16; - const actualLimit = $isSmallViewport ? mobileLimit : limit; - return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; - } + $: projectsToArchive = data.projects.projects.filter((project) => project.status !== 'active'); + $: activeProjects = data.projects.projects.filter((project) => project.status === 'active'); function clearSearch() { searchQuery?.clearInput(); } - - $: projectsToArchive = data.projects.projects.filter( - (project) => !data.organization.projects?.includes(project.$id) - ); - {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > $currentPlan.projects && $canWriteProjects} + {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)} + {@const difference = projectsToArchive} + {@const messagePrefix = + difference.length > 1 ? `${difference} projects` : `${difference} project`} - - {#each projectsToArchive as project, index}{@const text = `${project.name}`} - {@html text}{index === projectsToArchive.length - 2 - ? ', and ' - : index < projectsToArchive.length - 1 - ? ', ' - : ''} - {/each} - will be archived - + title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}> + Upgrade your plan to restore archived projects - @@ -176,7 +166,7 @@ {/if} {:else} - Add credits + (show = true)}>Add credits {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte index 44c9c9710..ac649dd83 100644 --- a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte +++ b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte @@ -28,7 +28,12 @@ } - +

    Your organization is set to change to {tierToPlan($organization?.billingPlanDowngrade).name} diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 3f27119c9..51c39a931 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -1,180 +1,533 @@ {#if $organization} - - Payment estimates - A breakdown of your estimated upcoming payment for the current billing period. Totals displayed - exclude accumulated credits and applicable taxes. - -

    - Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} -

    - - - - - {currentPlan.name} plan - - - {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION - ? formatCurrency(0) - : currentPlan - ? formatCurrency(currentPlan?.price) - : ''} - - + + {currentPlan.name} plan - {#if currentPlan.budgeting && extraUsage > 0} - 0 - ? currentInvoice.usage.length + 1 - : currentInvoice.usage.length - ).toString()}> - - {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - - - {#if currentAggregation.additionalMembers} - - - Additional members - - {formatCurrency( - currentAggregation.additionalMemberAmount - )} - - - - {currentAggregation.additionalMembers} - - + {#if totalAmount > 0} + + Next payment of {formatCurrency(totalAmount)} + will occur on + {toLocaleDate($organization?.billingNextInvoiceDate)}. + + {/if} + +
    + + Current billing cycle ({new Date( + $organization?.billingCurrentInvoiceDate + ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}-{new Date( + $organization?.billingNextInvoiceDate + ).toLocaleDateString('en', { day: 'numeric', month: 'short' })}) + + + Estimate, subject to change based on usage. + +
    + +
    + {/each} + {/if} + + + {/each} + {#if availableCredit > 0} + + {}}> + + - {#if currentPlan.supportsCredits && availableCredit > 0} - - - Credits to be applied + >Credits - - -{formatCurrency( - Math.min(availableCredit, currentInvoice?.amount ?? 0) - )} + + {}}> + - - {/if} + + {}}> + + -{formatCurrency(creditsApplied)} + + + + {/if} - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} - - - - - Current total (USD) - - - - Estimates are updated daily and may differ from your - final invoice. - - - - - - {formatCurrency( - Math.max( - (currentInvoice?.amount ?? 0) - - Math.min(availableCredit, currentInvoice?.amount ?? 0), - 0 - ) - )} - - - {/if} - - - - + + {}}> + + Total + + + {}}> + + + + {}}> + + {formatCurrency(totalAmount)} + + + + + + + +
    {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
    - + class="u-flex u-cross-center u-gap-8 u-flex-wrap u-width-full-line u-main-end actions-mobile"> + {#if !currentPlan?.usagePerProject} + + {/if} {:else} @@ -204,20 +557,24 @@ Change plan {/if} - + {#if !currentPlan?.usagePerProject} + + {/if}
    {/if} - - +
    + {/if} diff --git a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte index ca84dbf21..338be255e 100644 --- a/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte +++ b/src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte @@ -17,9 +17,14 @@ Appwrite services, update the budget limit.
    - + {#if !page.data.currentPlan?.usagePerProject} + + {/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index b5ef36e43..934ddf3b5 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -4,7 +4,6 @@ import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { PlanComparisonBox, PlanSelection, SelectPaymentMethod } from '$lib/components/billing'; - import PlanExcess from '$lib/components/billing/planExcess.svelte'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants'; import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms'; @@ -33,7 +32,11 @@ import { onMount } from 'svelte'; import { loadAvailableRegions } from '$routes/(console)/regions'; import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte'; + import OrganizationUsageLimits from '$lib/components/organizationUsageLimits.svelte'; import { Query } from '@appwrite.io/console'; + import type { OrganizationUsage } from '$lib/sdk/billing'; + import type { Models } from '@appwrite.io/console'; + import { toLocaleDate } from '$lib/helpers/date'; export let data; @@ -43,6 +46,9 @@ let previousPage: string = base; let showExitModal = false; let formComponent: Form; + let usageLimitsComponent: + | { validateOrAlert: () => boolean; getSelectedProjects: () => string[] } + | undefined; let isSubmitting = writable(false); let collaborators: string[] = data?.members?.memberships @@ -55,6 +61,8 @@ let showCreditModal = false; let feedbackDowngradeReason: string; let feedbackMessage: string; + let orgUsage: OrganizationUsage; + let allProjects: { projects: Models.Project[] } | undefined; $: paymentMethods = null; @@ -91,6 +99,21 @@ selectedPlan = $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO; + + try { + orgUsage = await sdk.forConsole.billing.listUsage(data.organization.$id); + } catch { + orgUsage = undefined; + } + + try { + allProjects = await sdk.forConsole.projects.list([ + Query.equal('teamId', data.organization.$id), + Query.limit(1000) + ]); + } catch { + allProjects = { projects: [] }; + } }); async function loadPaymentMethods() { @@ -100,6 +123,13 @@ async function handleSubmit() { if (isDowngrade) { + // If target plan has a non-zero project limit, ensure selection made + const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; + if (targetProjectsLimit > 0 && usageLimitsComponent?.validateOrAlert) { + const ok = usageLimitsComponent.validateOrAlert(); + if (!ok) return; + } + await downgrade(); } else if (isUpgrade) { await upgrade(); @@ -136,6 +166,7 @@ async function downgrade() { try { + // 1) update the plan first await sdk.forConsole.billing.updatePlan( data.organization.$id, selectedPlan, @@ -143,6 +174,22 @@ null ); + // 2) If the target plan has a project limit, apply selected projects now + const targetProjectsLimit = $plansInfo?.get(selectedPlan)?.projects ?? 0; + if (targetProjectsLimit > 0 && usageLimitsComponent) { + const selected = usageLimitsComponent.getSelectedProjects(); + if (selected?.length) { + try { + await sdk.forConsole.billing.updateSelectedProjects( + data.organization.$id, + selected + ); + } catch (projectError) { + console.warn('Project selection failed after plan update:', projectError); + } + } + } + await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]); await goto(previousPage); @@ -308,14 +355,12 @@ {/if} {#if isDowngrade} - {#if selectedPlan === BillingPlan.FREE && !data.hasFreeOrgs} - - {:else if selectedPlan === BillingPlan.PRO && data.organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0} - {@const extraMembers = collaborators?.length ?? 0} - {@const price = formatCurrency( - extraMembers * - ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0) - )} + {@const extraMembers = collaborators?.length ?? 0} + {@const price = formatCurrency( + extraMembers * + ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0) + )} + {#if selectedPlan === BillingPlan.PRO} Your monthly payments will be adjusted for the Pro plan @@ -325,7 +370,26 @@ >you will be charged {price} monthly for {extraMembers} team members. This will be reflected in your next invoice. + {:else if selectedPlan === BillingPlan.FREE} + + You will retain access to {tierToPlan($organization.billingPlan) + .name} plan features until your billing period ends. After that, + all team members except the owner will be removed, + and service disruptions may occur if usage exceeds Free plan limits. + {/if} + + {/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index ecb712008..c6a137f96 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -1,11 +1,20 @@ import type { PageLoad } from './$types'; import type { Organization } from '$lib/stores/organization'; import { BillingPlan, Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); + let plans; + try { + plans = await sdk.forConsole.billing.listPlans(); + } catch (error) { + console.error('Failed to load billing plans:', error); + plans = { plans: {} }; + } + let plan: BillingPlan; if (currentPlan?.$id === BillingPlan.SCALE) { @@ -22,6 +31,7 @@ export const load: PageLoad = async ({ depends, parent }) => { return { members, plan, + plans, selfService, hasFreeOrgs }; diff --git a/src/routes/(console)/organization-[organization]/header.svelte b/src/routes/(console)/organization-[organization]/header.svelte index f114dcc22..1a81ae48c 100644 --- a/src/routes/(console)/organization-[organization]/header.svelte +++ b/src/routes/(console)/organization-[organization]/header.svelte @@ -66,7 +66,11 @@ event: 'usage', title: 'Usage', hasChildren: true, - disabled: !(isCloud && ($isOwner || $isBilling)) + disabled: !( + isCloud && + ($isOwner || $isBilling) && + !page.data.currentPlan?.usagePerProject + ) }, { href: `${path}/billing`, diff --git a/src/routes/(console)/organization-[organization]/members/+page.svelte b/src/routes/(console)/organization-[organization]/members/+page.svelte index 4a9f16b99..05c350080 100644 --- a/src/routes/(console)/organization-[organization]/members/+page.svelte +++ b/src/routes/(console)/organization-[organization]/members/+page.svelte @@ -47,9 +47,7 @@ // Calculate if button should be disabled and tooltip should show $: memberCount = data.organizationMembers?.total ?? 0; $: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1; - $: isButtonDisabled = isCloud - ? isFreeWithMembers || !$currentPlan?.addons?.seats?.supported - : false; + $: isButtonDisabled = isCloud ? isFreeWithMembers : false; const resend = async (member: Models.Membership) => { try { diff --git a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte index e27353624..a9422df60 100644 --- a/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -8,7 +8,8 @@ getServiceLimit, showUsageRatesModal, type Tier, - upgradeURL + upgradeURL, + useNewPricingModal } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import ProjectBreakdown from './ProjectBreakdown.svelte'; @@ -76,14 +77,32 @@ {#if $organization.billingPlan === BillingPlan.SCALE}

    On the Scale plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. ($showUsageRatesModal = true)} - >Learn more + resource listed below. + {#if $useNewPricingModal} + ($showUsageRatesModal = true)}>Learn more + {:else} + + Learn more + + {/if}

    {:else if $organization.billingPlan === BillingPlan.PRO}

    On the Pro plan, you'll be charged only for any usage that exceeds the thresholds per - resource listed below. ($showUsageRatesModal = true)} - >Learn more + resource listed below. + {#if $useNewPricingModal} + ($showUsageRatesModal = true)}>Learn more + {:else} + + Learn more + + {/if}

    {:else if $organization.billingPlan === BillingPlan.FREE}

    diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 24b82ae7b..31fc89017 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -243,7 +243,7 @@ Executions - Calculated for all functions that are executed in all projects in your project. + Calculated for all functions that are executed in this project. {#if executions} {@const current = formatNum(executionsTotal)} diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index 4afa9181a..e5e1ee5a6 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,4 +1,4 @@ -import type { Aggregation, Invoice } from '$lib/sdk/billing'; +import type { AggregationTeam, Invoice, InvoiceUsage } from '$lib/sdk/billing'; import { accumulateUsage } from '$lib/sdk/usage'; import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; @@ -11,7 +11,7 @@ export const load: PageLoad = async ({ params, parent }) => { let startDate: string = organization.billingCurrentInvoiceDate; let endDate: string = organization.billingNextInvoiceDate; let currentInvoice: Invoice = undefined; - let currentAggregation: Aggregation = undefined; + let currentAggregation: AggregationTeam = undefined; if (invoice) { currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice); @@ -22,6 +22,15 @@ export const load: PageLoad = async ({ params, parent }) => { startDate = currentInvoice.from; endDate = currentInvoice.to; + } else { + try { + currentAggregation = await sdk.forConsole.billing.getAggregation( + organization.$id, + organization.billingAggregationId + ); + } catch (e) { + // ignore error if no aggregation found + } } const [invoices, usage] = await Promise.all([ @@ -29,10 +38,24 @@ export const load: PageLoad = async ({ params, parent }) => { sdk.forProject(region, project).project.getUsage({ startDate, endDate }) ]); - if (invoice) { - usage.usersTotal = currentAggregation.usageUsers; - usage.executionsTotal = currentAggregation.usageExecutions; - usage.filesStorageTotal = currentAggregation.usageStorage; + if (currentAggregation) { + let projectSpecificData = null; + if (currentAggregation.breakdown) { + projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); + } + + if (projectSpecificData) { + const executionsResource = projectSpecificData.resources?.find?.( + (r: InvoiceUsage) => r.resourceId === 'executions' + ); + if (executionsResource) { + usage.executionsTotal = executionsResource.value || usage.executionsTotal; + } + } else { + usage.usersTotal = currentAggregation.usageUsers; + usage.executionsTotal = currentAggregation.usageExecutions; + usage.filesStorageTotal = currentAggregation.usageStorage; + } } usage.users = accumulateUsage(usage.users, usage.usersTotal); From 726986b052f97e49fa77c37c1969cf28a28ab30f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 1 Sep 2025 16:16:55 +0000 Subject: [PATCH 60/69] improve projects addon check on create project --- src/lib/layout/createProject.svelte | 52 +++++++++++++++-------------- src/lib/sdk/billing.ts | 1 + 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 52915971d..92899ae59 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -37,9 +37,12 @@ let showCustomId = $state(false); let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO); let projectsLimited = $derived( - isProPlan - ? projects && projects >= 2 - : $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects + $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects + ); + let isAddonProject = $derived( + $currentPlan?.addons?.projects?.supported && + projects && + projects >= $currentPlan?.addons?.projects?.planIncluded ); @@ -88,29 +91,28 @@ Region cannot be changed after creation {/if} + {#if isAddonProject} + + Each added project comes with its own dedicated pool of resources. + + {/if} {#if projectsLimited} - {#if isProPlan} - - Each added project comes with its own dedicated pool of resources. - - {:else} - - Extra projects are available on paid plans for an additional fee - - - - - {/if} + + Extra projects are available on paid plans for an additional fee + + + + {/if} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 7ae79e199..0361f5910 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -372,6 +372,7 @@ export type PlanAddon = { limit: number; value: number; type: string; + planIncluded: number; }; export type Plan = { From b322171e159457241c51086ec226f20a8d10e18b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 1 Sep 2025 16:21:01 +0000 Subject: [PATCH 61/69] updated typo --- .../organization-[organization]/change-plan/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index 934ddf3b5..12e5f880e 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -345,8 +345,8 @@ - To downgrade this organization, first migrate or delete one of your - existing paid organizations. + To downgrade this organization, first migrate or delete your + existing free organization. + {/if} From 25e3ef31a278b154b15d67f4d2d7d04ae2da2a00 Mon Sep 17 00:00:00 2001 From: Darshan Date: Tue, 2 Sep 2025 09:48:18 +0530 Subject: [PATCH 65/69] fix: form causing issue in wizard. --- .../components/organizationUsageLimits.svelte | 92 ++++++++++--------- .../change-plan/+page.svelte | 16 ++-- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/lib/components/organizationUsageLimits.svelte b/src/lib/components/organizationUsageLimits.svelte index 5fb85a6ae..37ce91b6c 100644 --- a/src/lib/components/organizationUsageLimits.svelte +++ b/src/lib/components/organizationUsageLimits.svelte @@ -261,52 +261,58 @@ - - - Choose which {freePlanLimits.projects} projects to keep. Projects over the limit will be blocked - after your billing cycle ends on {toLocaleDate($organization.billingNextInvoiceDate)}. - +{#if showSelectProject} + + + Choose which {freePlanLimits.projects} projects to keep. Projects over the limit will be + blocked after your billing cycle ends on {toLocaleDate( + $organization.billingNextInvoiceDate + )}. + - {#if error} - {error} - {/if} + {#if error} + {error} + {/if} -

    - - - Project Name - Created - - {#each projects as project} - - {project.name} - - {toLocaleDateTime(project.$createdAt)} - - - {/each} - -
    - {#if selectedProjects.length === allowedProjectsToKeep} - {@const difference = projects.length - selectedProjects.length} - {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`} - - {formatProjectsToArchive()} will be archived - - {/if} +
    + + + Project Name + Created + + {#each projects as project} + + {project.name} + + {toLocaleDateTime(project.$createdAt)} + + + {/each} + +
    + {#if selectedProjects.length === allowedProjectsToKeep} + {@const difference = projects.length - selectedProjects.length} + {@const messagePrefix = + difference > 1 ? `${difference} projects` : `${difference} project`} + + {formatProjectsToArchive()} will be archived + + {/if} - - - - - + + + + + +{/if}