Skip to content

Conversation

TorstenDittmann
Copy link
Contributor

@TorstenDittmann TorstenDittmann commented Sep 1, 2025

Reverts #2294

Summary by CodeRabbit

  • New Features

    • Archived projects section with unarchive and migrate actions.
    • Organization usage limits dashboard with guided downgrade/archiving workflow.
    • Revamped billing summary: expandable per-project breakdown, progress visuals, and credit handling.
    • Dynamic plan selection sourced from live plans; PRO now lists unlimited seats.
    • New compact card component for consistent layouts.
  • Improvements

    • “View usage” and Usage tab respect per‑project usage setting.
    • Alerts use next-invoice date and clearer messaging.
    • Clarified Executions description and minor visual/layout fixes (progress clipping, z-index, modal sizing).
  • Chores

    • Updated dependencies.

Copy link

appwrite bot commented Sep 1, 2025

Console

Project ID: 688b7bf400350cbd60e9

Sites (2)
Site Status Logs Preview QR
 console-qa
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code
 console-cloud
688b7c18002b9b871a8f
Ready Ready View Logs Preview URL QR Code

Note

Appwrite has a Discord community with over 16 000 members.

Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Warning

Rate limit exceeded

@lohanidamodar has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between b322171 and 8079e99.

📒 Files selected for processing (2)
  • src/lib/components/organizationUsageLimits.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/change-plan/+page.svelte (9 hunks)

Walkthrough

This PR updates package.json dependency URLs and adds multiple UI components (ArchiveProject, EstimatedCard, OrganizationUsageLimits, PlanSummaryOld) and helper exports (formatName). It expands billing SDK/types (AggregationTeam, AggregationBreakdown, InvoiceUsage, Plan extensions) and adds billing.listPlans. Billing store logic is changed (getServiceLimit behavior, calculateExcess return shape, removal of billingProjectsLimitDate, new useNewPricingModal). Several pages/components load plans, adopt new types/stores, and conditionally switch UIs based on useNewPricingModal; many components receive minor UI/prop adjustments.

Possibly related PRs

  • Feat: New Billing UI changes #2249: Mirrors the same billing model/type additions, new components (archiveProject, estimatedCard, organizationUsageLimits), billing.store changes (useNewPricingModal, removal of billingProjectsLimitDate), and plan-summary refactor — strong code-level overlap.

Suggested reviewers

  • Meldiron
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch revert-2294-revert-2249-feat-SER-204-New-Archive-projects-ui-change

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/lib/components/billing/alerts/selectProjectCloud.svelte (2)

157-160: XSS risk: remove {@html} with untrusted project names

{@html formatProjectsToArchive()} renders unescaped names; a malicious project name could inject HTML/JS. Render names as text nodes instead and drop the helper.

-                <span>
-                    {@html formatProjectsToArchive()}
-                    will be archived.
-                </span>
+                <span>
+                    {#each projectsToArchive as project, index}
+                        {#if index > 0}{index === projectsToArchive.length - 1 ? ' and ' : ', '}{/if}
+                        <b>{project.name}</b>
+                    {/each}
+                    will be archived.
+                </span>

Also remove the now-unused formatter:

-    function formatProjectsToArchive() {
-        let result = '';
-
-        projectsToArchive.forEach((project, index) => {
-            const text = `${index === 0 ? '' : ' '}<b>${project.name}</b> `;
-            result += text;
-
-            if (index < projectsToArchive.length - 1) {
-                if (index == projectsToArchive.length - 2) {
-                    result += 'and ';
-                }
-                if (index < projectsToArchive.length - 2) {
-                    result += ', ';
-                }
-            }
-        });
-
-        return result;
-    }

33-39: Prevent crash when projects is empty before Save

projects[0] can be undefined if data hasn’t loaded; guard before calling the SDK.

     async function updateSelected() {
         try {
+            if (projects.length === 0) {
+                throw new Error('Projects not loaded yet. Please try again.');
+            }
             await sdk.forConsole.billing.updateSelectedProjects(
                 projects[0].teamId,
                 selectedProjects
             );
src/lib/sdk/billing.ts (2)

324-328: Align Aggregation list type with new AggregationTeam

Single fetch returns AggregationTeam, list still returns Aggregation. Inconsistent and error-prone.

Apply:

-export type AggregationList = {
-    aggregations: Aggregation[];
+export type AggregationList = {
+    aggregations: AggregationTeam[];
     total: number;
 };

And ensure callers expect AggregationTeam in list responses.


302-304: Fix casing: executionsMbSecondsTotal/buildsMbSecondsTotal

Types here don’t match usage in +page.svelte (and likely API). This will cause TS/runtime mismatches.

Apply:

-    executionsMBSecondsTotal: number;
-    buildsMBSecondsTotal: number;
+    executionsMbSecondsTotal: number;
+    buildsMbSecondsTotal: number;
src/lib/stores/billing.ts (1)

621-628: Fix execution unit conversion and restore users excess

  • Remove the 'GB' argument from executions in calculateExcess so it uses the raw numbers.
  • Add a users field via calculateResourceSurplus(addon.usageUsers, plan.users) to satisfy the UI’s excess.users.
 export function calculateExcess(addon: AggregationTeam, plan: Plan) {
   return {
     bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth),
     storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'),
-    executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'),
+    executions: calculateResourceSurplus(addon.usageExecutions, plan.executions),
+    users: calculateResourceSurplus(addon.usageUsers, plan.users),
     members: addon.additionalMembers
   };
 }
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)

89-103: URL-selected plan is overridden later.

selectedPlan from ?plan= is immediately reset to PRO/SCALE, ignoring the URL. Respect the param and only default when absent.

-        const plan = params.get('plan');
-        if (plan && plan in BillingPlan) {
-            selectedPlan = plan as BillingPlan;
-        }
+        const plan = params.get('plan');
+        if (plan && plan in BillingPlan) {
+            selectedPlan = plan as BillingPlan;
+        } else {
+            // fallback when no explicit plan is provided
+            selectedPlan =
+                $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
+        }
-
-        selectedPlan =
-            $currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;

358-385: Correct seat-cost calculation to account for included seats.

extraMembers = collaborators.length overstates cost; it should charge only for seats beyond the plan’s included amount.

-                        {@const extraMembers = collaborators?.length ?? 0}
-                        {@const price = formatCurrency(
-                            extraMembers *
-                                ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
-                        )}
+                        {@const membersCount = data?.members?.memberships?.length ?? 0}
+                        {@const includedSeats = $plansInfo?.get(selectedPlan)?.addons?.seats?.included ?? 0}
+                        {@const seatPrice = $plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0}
+                        {@const extraSeats = Math.max(0, membersCount - includedSeats)}
+                        {@const price = formatCurrency(extraSeats * seatPrice)}
...
-                                <b>you will be charged {price} monthly for {extraMembers} team members.</b>
+                                {#if seatPrice > 0 && extraSeats > 0}
+                                    <b>you will be charged {price} monthly for {extraSeats} additional seat{s => extraSeats === 1 ? '' : 's'}.</b>
+                                {:else}
+                                    <b>No additional seat charges apply.</b>
+                                {/if}
src/routes/(console)/organization-[organization]/+page.svelte (1)

220-223: Guard region lookup.

Avoid crash when region isn’t found.

-                            {@const region = findRegion(project)}
-                            <Typography.Text>{region.name}</Typography.Text>
+                            {@const region = findRegion(project)}
+                            <Typography.Text>{region?.name ?? project.region}</Typography.Text>
🧹 Nitpick comments (43)
src/lib/components/billing/alerts/selectProjectCloud.svelte (2)

151-162: Avoid “0 project will be archived”; derive count from projectsToArchive and guard alert

Handle the zero case and compute the count from projectsToArchive to avoid off-by-one and improve readability. Also add a safe fallback for a missing invoice date.

-        {#if selectedProjects.length === $currentPlan?.projects}
-            {@const difference = projects.length - selectedProjects.length}
-            {@const messagePrefix =
-                difference > 1 ? `${difference} projects` : `${difference} project`}
+        {#if selectedProjects.length === $currentPlan?.projects}
+            {@const difference = projectsToArchive.length}
+            {#if difference > 0}
+            {@const messagePrefix = difference === 1 ? '1 project' : `${difference} projects`}
             <Alert.Inline
                 status="warning"
-                title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}>
+                title={`${messagePrefix} will be archived on ${$organization?.billingNextInvoiceDate ? toLocaleDate($organization.billingNextInvoiceDate) : 'your next invoice date'}`}>
                 <span>
-                    {@html formatProjectsToArchive()}
+                    {@html formatProjectsToArchive()}
                     will be archived.
                 </span>
             </Alert.Inline>
+            {/if}
         {/if}

76-76: Use strict inequality

Prefer !== to avoid coercion surprises.

-        if (organizationId != teamIdInLoadedProjects) {
+        if (organizationId !== teamIdInLoadedProjects) {
src/lib/components/billing/planComparisonBox.svelte (1)

93-93: Make “seats” dynamic and align terminology with the Free tier copy

Use plan data instead of a hard-coded label and prefer “organization members” to match “Limited to 1 organization member” above.

Apply:

-                <li>Unlimited seats</li>
+                <li>
+                    {Number.isFinite(plan.members)
+                        ? `${formatNum(plan.members)} organization members`
+                        : 'Unlimited organization members'}
+                </li>

Note: Since SCALE already says “Everything in the Pro plan, plus:”, consider removing the duplicate “Unlimited seats” from the SCALE list to avoid redundancy.

src/lib/components/labelCard.svelte (1)

48-48: z-index won’t take effect without positioning; cursor for disabled is inverted

Add position so z-index applies, and use a non-interactive cursor when disabled.

-    <div style:cursor={disabled ? 'pointer' : ''} style:z-index="1">
+    <div
+        style:position="relative"
+        style:z-index="1"
+        style:cursor={disabled ? 'not-allowed' : 'pointer'}>
src/lib/helpers/string.ts (1)

52-63: Grapheme-safe truncation and minimum limit clamp

Avoid slicing in the middle of surrogate pairs/emoji and guard very small limits.

 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) : '-';
+    if (!name) return '-';
+    const mobileLimit = 16;
+    const actualLimit = Math.max(3, isSmallViewport ? mobileLimit : limit);
+    const chars = Array.from(name); // grapheme-ish (handles surrogate pairs)
+    return chars.length > actualLimit
+        ? `${chars.slice(0, actualLimit).join('')}...`
+        : name;
 }

I can add quick unit tests covering desktop/mobile limits and emoji handling if you’d like.

src/lib/components/estimatedCard.svelte (1)

1-10: Support class/rest attributes for better ergonomics

Allow consumers to style or target the wrapper without extra divs by forwarding class.

 <script lang="ts">
     import { Layout, Card } from '@appwrite.io/pink-svelte';
     export let gap: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl' = 'l';
+    // Allow passing class down to Card.Base
+    export let class: string | undefined = undefined;
 </script>

-<Card.Base>
+<Card.Base class={class}>
     <Layout.Stack {gap}>
         <slot />
     </Layout.Stack>
 </Card.Base>
src/routes/(console)/organization-[organization]/members/+page.svelte (2)

49-51: Gate “Invite” using the centralized members limit to avoid drift; fix dead tooltip branch

Disabling only on Free+≥1 member assumes all paid tiers are unlimited. If any tier has a finite cap (now or later), this will desync from billing logic. Use the store’s service limit for members and keep the tooltip consistent.

Apply within this hunk:

-    $: isButtonDisabled = isCloud ? isFreeWithMembers : false;
+    // Prefer centralized limit from billing store
+    $: membersLimit = getServiceLimit?.('members');
+    $: isButtonDisabled =
+        isCloud ? (Number.isFinite(membersLimit) && memberCount >= membersLimit) : false;

And import (outside this hunk):

import { getServiceLimit } from '$lib/stores/billing';

92-97: Tooltip branch is unreachable after the new gating

Because the button now disables only on Free, the “members limit for plan X” message never shows. Either remove it or key it off the computed limit.

Example tweak using the derived limit:

-                {$organization?.billingPlan === BillingPlan.FREE
-                    ? 'Upgrade to add more members'
-                    : `You've reached the members limit for the ${
-                          tierToPlan($organization?.billingPlan)?.name
-                      } plan`}
+                {#if $organization?.billingPlan === BillingPlan.FREE}
+                    Upgrade to add more members
+                {:else if Number.isFinite(membersLimit)}
+                    You've reached the members limit for the {tierToPlan($organization?.billingPlan)?.name} plan
+                {/if}
src/lib/layout/createProject.svelte (4)

38-43: Normalize project cap calc, coerce to boolean, and decouple from tier assumptions

Current logic can yield undefined and hard-codes PRO=2 while other paid tiers might differ. Derive a numeric cap and compute a strict boolean.

-    let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO);
-    let projectsLimited = $derived(
-        isProPlan
-            ? projects && projects >= 2
-            : $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects
-    );
+    let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO);
+    // If the store exposes a central projects limit, prefer that; otherwise fallback:
+    let projectsCap = $derived(
+        isProPlan ? 2 : ($currentPlan?.projects ?? Infinity)
+    );
+    let projectsLimited = $derived(
+        Number.isFinite(projectsCap) && (projects ?? 0) >= Number(projectsCap)
+    );

If SCALE (or others) share the “2 projects then add-on” rule, consider a helper from the billing store (e.g., getServiceLimit('projects')) to avoid duplicating tier rules here.


61-61: Ensure disabled is strictly boolean

Avoid passing undefined; coerce the reactive flag.

-                    disabled={!isProPlan && projectsLimited}
+                    disabled={!isProPlan && !!projectsLimited}

82-83: Same boolean coercion for Region select

Keep the prop a strict boolean.

-                        disabled={!isProPlan && projectsLimited}
+                        disabled={!isProPlan && !!projectsLimited}

91-114: Use nullish coalescing for price default; guard copy against odd caps

Using || can override a legitimate 0 price. Also consider guarding copy if the cap is Infinity.

-                        title="Expand for {formatCurrency(
-                            $currentPlan?.addons?.projects?.price || 15
-                        )}/project per month">
+                        title="Expand for {formatCurrency(
+                            $currentPlan?.addons?.projects?.price ?? 15
+                        )}/project per month">

Optional: if you adopt projectsCap above, you can also display it (when finite) in the non‑PRO warning for consistency.

src/lib/components/organizationUsageLimits.svelte (6)

294-302: Hide archive warning when nothing will be archived

Avoid “0 projects will be archived…”.

Apply:

-{#if selectedProjects.length === allowedProjectsToKeep}
+{#if selectedProjects.length === allowedProjectsToKeep && (projects.length - selectedProjects.length) > 0}
     {@const difference = projects.length - selectedProjects.length}
     {@const messagePrefix = difference > 1 ? `${difference} projects` : `${difference} project`}
     <Alert.Inline
         status="warning"
         title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}>
         {formatProjectsToArchive()} will be archived
     </Alert.Inline>
 {/if}

214-216: Pluralize “member(s)”

Minor copy tweak for correctness.

Apply:

-<Typography.Text>{formatNumber(freePlanLimits.members)} member</Typography.Text>
+<Typography.Text>
+    {formatNumber(freePlanLimits.members)} {freePlanLimits.members === 1 ? 'member' : 'members'}
+</Typography.Text>

241-243: Format storage limit value

Match number formatting used elsewhere.

Apply:

-<Typography.Text>{freePlanLimits.storage} GB</Typography.Text>
+<Typography.Text>{formatNumber(freePlanLimits.storage)} GB</Typography.Text>

67-81: Use Intl.ListFormat for project list

Cleaner, localized “A, B, and C”.

Apply:

-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 formatProjectsToArchive(): string {
+    const names = projectsToArchive.map((p) => p.name);
+    const lf = new Intl.ListFormat(undefined, { style: 'long', type: 'conjunction' });
+    return lf.format(names);
+}

107-116: Enforce selection cap in UI (optional)

Prevent over-selecting instead of erroring on save.

Example:

 function updateSelected() {
     error = null;
-    const filteredSelection = selectedProjects.filter((id) =>
+    const filteredSelection = selectedProjects.filter((id) =>
         projects.some((p) => p.$id === id)
     );
-    if (filteredSelection.length !== allowedProjectsToKeep) {
+    if (filteredSelection.length > allowedProjectsToKeep) {
+        selectedProjects = filteredSelection.slice(0, allowedProjectsToKeep);
+    }
+    if (selectedProjects.length !== allowedProjectsToKeep) {
         error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`;
         return;
     }

175-178: Guard against undefined limits in UI

If limits are still loading, render a placeholder to avoid “undefined” in the table.

Apply:

-<Typography.Text>{formatNumber(allowedProjectsToKeep)} projects</Typography.Text>
+<Typography.Text>
+    {allowedProjectsToKeep ? `${formatNumber(allowedProjectsToKeep)} projects` : '—'}
+</Typography.Text>
-{formatNumber(currentUsage.projects)} / {formatNumber(allowedProjectsToKeep)}
+{formatNumber(currentUsage.projects)} / {allowedProjectsToKeep ? formatNumber(allowedProjectsToKeep) : '—'}

Also applies to: 189-193

src/lib/sdk/billing.ts (1)

577-592: Deduplicate listPlans vs getPlansInfo

Both hit /console/plans. Keep one and alias the other.

Apply:

-async getPlansInfo(): Promise<PlanList> {
-    const path = `/console/plans`;
-    const params = {};
-    const uri = new URL(this.client.config.endpoint + path);
-    return await this.client.call(
-        'GET',
-        uri,
-        {
-            'content-type': 'application/json'
-        },
-        params
-    );
-}
+async getPlansInfo(): Promise<PlanList> {
+    return this.listPlans();
+}

Also applies to: 1429-1442

src/lib/stores/billing.ts (1)

154-154: Avoid null default for non-nullable Tier.

Using tier: Tier = null can trip strict TS configs. Make it optional/nullable.

-export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan?: Plan): number {
+export function getServiceLimit(serviceId: PlanServices, tier?: Tier | null, plan?: Plan): number {
src/lib/components/billing/planExcess.svelte (1)

73-74: Handle “Unlimited” gracefully.

If a non-Free tier ever slips in, avoid rendering “Infinity members”.

-                <Table.Cell {root}>{getServiceLimit('members', tier)} members</Table.Cell>
+                <Table.Cell {root}>
+                  {#if Number.isFinite(getServiceLimit('members', tier))}
+                    {getServiceLimit('members', tier)} members
+                  {:else}
+                    Unlimited members
+                  {/if}
+                </Table.Cell>
src/lib/components/billing/alerts/projectsLimit.svelte (1)

30-33: Guard against missing billingNextInvoiceDate.

Prevent “Invalid Date” if the value is absent.

-            Choose which projects to keep before {toLocaleDate(
-                $organization.billingNextInvoiceDate
-            )} or upgrade to Pro. Projects over the limit will be blocked after this date.
+            {#if $organization?.billingNextInvoiceDate}
+              Choose which projects to keep before {toLocaleDate($organization.billingNextInvoiceDate)}
+              or upgrade to Pro. Projects over the limit will be blocked after this date.
+            {:else}
+              Choose which projects to keep or upgrade to Pro. Projects over the limit may be blocked
+              after your current billing period.
+            {/if}
src/lib/components/billing/alerts/limitReached.svelte (1)

23-30: Avoid button flicker: check for explicit false instead of generic falsy

When page.data.currentPlan is still undefined, the current condition renders the button briefly. Gate on === false to avoid transient UI.

-            {#if !page.data.currentPlan?.usagePerProject}
+            {#if page.data.currentPlan?.usagePerProject === false}
src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte (1)

20-27: Prevent transient render by checking for explicit false

Same reasoning as the other banner: avoid showing “View usage” before currentPlan is resolved.

-            {#if !page.data.currentPlan?.usagePerProject}
+            {#if page.data.currentPlan?.usagePerProject === false}
src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte (1)

31-36: Modal size prop usage looks fine; make bound error nullable and verify size support

  • bind:error expects a nullable target. Change let error: string = null; to let error: string | null = null; in script.
  • Please confirm Modal indeed exports and handles size="s" consistently across breakpoints.
src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte (1)

39-39: Harden coupon handling: initialize to empty string and tighten disabled logic

  • Initialize coupon to an empty string and keep it as a string (avoid null) to prevent UI showing “null” and to simplify checks.
  • Disable the submit button when coupon is empty or whitespace.

Apply this diff:

-    let coupon: string = null;
-    let error: string = null;
+    let coupon = '';
+    let error: string | null = null;
@@
-<Button disabled={coupon === ''} submit>Add credits</Button>
+<Button disabled={!coupon || coupon.trim() === ''} submit>Add credits</Button>
@@
-            coupon = null;
+            coupon = '';

Optionally, trim before submit:

-            await sdk.forConsole.billing.addCredit($organization.$id, coupon);
+            await sdk.forConsole.billing.addCredit($organization.$id, coupon.trim());

Also applies to: 53-55

src/routes/(console)/organization-[organization]/change-plan/+page.ts (1)

10-16: Type the plans payload and ensure a safe fallback shape

  • Explicitly type plans to the SDK’s PlanList (or the correct type) and guarantee a consistent fallback to avoid downstream runtime/type issues.

Apply this diff:

-import { sdk } from '$lib/stores/sdk';
+import { sdk } from '$lib/stores/sdk';
+import type { PlanList } from '$lib/sdk/billing';
@@
-    let plans;
+    let plans: PlanList | undefined;
     try {
         plans = await sdk.forConsole.billing.listPlans();
     } catch (error) {
         console.error('Failed to load billing plans:', error);
-        plans = { plans: {} };
+        plans = { plans: {} } as PlanList;
     }
@@
-        plans,
+        plans: (plans ?? { plans: {} } as PlanList),

If you have a dependency key for plans, consider also calling depends(Dependencies.PLANS) here.

Also applies to: 34-34

src/lib/components/billing/usageRates.svelte (1)

7-7: Unlimited seats logic: simplify if helper already guards Infinity

  • If isWithinSafeRange() already returns false for non-finite numbers (including Infinity), you can drop the explicit equality check for Infinity.

Apply this diff if applicable:

-        const isUnlimited = count === Infinity || !isWithinSafeRange(count);
+        const isUnlimited = !isWithinSafeRange(count);

Minor: consider using abbreviateNumber for large finite seat counts for visual consistency with usage limits.

Also applies to: 24-27

src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte (1)

81-90: Avoid duplication of “Learn more” branch

The Scale/Pro paragraphs duplicate the same conditional for the “Learn more” link. Extract to a tiny component or inline fragment to keep it DRY.

Option sketch:

{#if $useNewPricingModal}
  <Link.Button on:click={() => ($showUsageRatesModal = true)}>Learn more</Link.Button>
{:else}
  <Link.Anchor href="https://appwrite.io/pricing" target="_blank" rel="noopener noreferrer">Learn more</Link.Anchor>
{/if}

Place once and reuse in both plan sections.

Also applies to: 95-105

src/routes/(console)/organization-[organization]/header.svelte (1)

69-74: Clarify Usage tab gating when plan is unknown.

!page.data.currentPlan?.usagePerProject treats undefined as false, enabling the tab before plan data loads. If the intent is to hide Usage unless usagePerProject === false, tighten the check.

-                disabled: !(
-                    isCloud &&
-                    ($isOwner || $isBilling) &&
-                    !page.data.currentPlan?.usagePerProject
-                )
+                disabled: !(
+                    isCloud &&
+                    ($isOwner || $isBilling) &&
+                    page.data.currentPlan?.usagePerProject === false
+                )
src/routes/(console)/organization-[organization]/billing/availableCredit.svelte (1)

109-113: Good switch to modal; remove leftover wizard wiring.

wizard import, reloadOnWizardClose, and the reactive watcher are now dead code.

-    import { wizard } from '$lib/stores/wizard';
...
-    let reloadOnWizardClose = false;
...
-    $: {
-        if (reloadOnWizardClose && !$wizard.show) {
-            request();
-            reloadOnWizardClose = false;
-        }
-    }

Also applies to: 169-170

src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)

103-117: Org usage and projects fetch: add resilience for large orgs.

Fetching up to 1000 projects in one call can be heavy. Consider pagination or lazy-loading to avoid blocking mount in very large orgs.


177-192: Surface non-blocking failure when applying selected projects.

Swallowing errors with console.warn may confuse users if project selection wasn’t applied. Add a warning notification.

-                if (selected?.length) {
-                    try {
-                        await sdk.forConsole.billing.updateSelectedProjects(
-                            data.organization.$id,
-                            selected
-                        );
-                    } catch (projectError) {
-                        console.warn('Project selection failed after plan update:', projectError);
-                    }
-                }
+                if (selected?.length) {
+                    try {
+                        await sdk.forConsole.billing.updateSelectedProjects(
+                            data.organization.$id,
+                            selected
+                        );
+                    } catch (projectError) {
+                        console.warn('Project selection failed after plan update:', projectError);
+                        addNotification({
+                            type: 'warning',
+                            message:
+                                'Plan updated, but applying selected projects failed. You can retry from the usage limits section.'
+                        });
+                    }
+                }
src/lib/components/billing/planSelection.svelte (4)

15-16: Defensive default for missing plans data

Guard against routes where page.data.plans isn’t populated to prevent Object.values(undefined) errors.

-$: plans = Object.values(page.data.plans.plans) as Plan[];
+$: plans = Object.values(page.data?.plans?.plans ?? {}) as Plan[];
 $: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id);

21-27: Type alignment between billingPlan and dynamic plan.$id

bind:group={billingPlan} expects the same type as value={plan.$id}. If Plan.$id goes beyond the BillingPlan enum, this will drift.

Option A (preferred): widen the prop type.

-export let billingPlan: BillingPlan;
+export let billingPlan: BillingPlan | string;

Option B: keep enum type and cast the value.

-            value={plan.$id}
+            value={plan.$id as BillingPlan}

24-26: Confirm tooltip content wiring

You set tooltipShow but no tooltip text/slot is provided here. Ensure LabelCard shows a helpful message when Free is disabled (e.g., “An organization already has a Free plan”).


37-40: Currency/label nuance

For zero-price plans, consider explicit “Free” instead of “$0.00” to match marketing tone; and ensure “per month + usage” is localized if needed.

src/lib/components/archiveProject.svelte (4)

82-94: Unarchive limit should consider only active projects

Counting all projects may include archived ones and block legitimate unarchives on Free. Filter by active status.

-        if (organization.billingPlan === BillingPlan.FREE) {
-            const currentProjectCount = organization.projects?.length || 0;
-            const projectLimit = currentPlan.projects || 0;
+        if (organization.billingPlan === BillingPlan.FREE) {
+            const currentProjectCount =
+                (organization.projects ?? []).filter((p) => p.status === Status.Active).length;
+            const projectLimit = currentPlan.projects ?? 0;
             return currentProjectCount >= projectLimit;
         }

176-193: Binding to an indexed object relies on deep reactivity

bind:show={readOnlyInfoOpen[project.$id]} mutates an object key in place. Without runes’ deep reactivity, the UI may not update. If you stick to classic Svelte, toggle via shallow copies (as you do on click) and set show instead of bind:show.


244-249: Badge count threshold off-by-one

You display “+N” only when platforms.length > 3, but you show two badges; with 3 platforms there’s one hidden. Consider > 2.

-                            {#if platforms.length > 3}
+                            {#if platforms.length > 2}
                                 <Badge
                                     variant="secondary"
                                     content={`+${platforms.length - 2}`}
                                     style="width: max-content;" />
                             {/if}

221-224: UX: explain disabled unarchive action

When disabled, consider showing a tooltip (“Upgrade plan or archive fewer projects to unarchive”) so users know why it’s disabled.

src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (1)

41-58: Broaden project-specific usage mapping (users/storage) and handle missing keys

Only executions are overridden from the breakdown. If breakdown.resources contains users/storage too, mirror them to keep totals consistent; also guard unknown resource IDs.

Example refactor:

-        if (projectSpecificData) {
-            const executionsResource = projectSpecificData.resources?.find?.(
-                (r: InvoiceUsage) => r.resourceId === 'executions'
-            );
-            if (executionsResource) {
-                usage.executionsTotal = executionsResource.value || usage.executionsTotal;
-            }
-        } else {
+        if (projectSpecificData?.resources?.length) {
+            for (const r of projectSpecificData.resources as InvoiceUsage[]) {
+                if (r.resourceId === 'executions') usage.executionsTotal = r.value ?? usage.executionsTotal;
+                if (r.resourceId === 'users') usage.usersTotal = r.value ?? usage.usersTotal;
+                if (r.resourceId === 'storage') usage.filesStorageTotal = r.value ?? usage.filesStorageTotal;
+            }
+        } else {
             usage.usersTotal = currentAggregation.usageUsers;
             usage.executionsTotal = currentAggregation.usageExecutions;
             usage.filesStorageTotal = currentAggregation.usageStorage;
         }

Please confirm the exact resourceId strings used by the API.

src/routes/(console)/organization-[organization]/+page.svelte (1)

179-183: Tooltip width nit.

maxWidth as project.name.length.toString() is odd; prefer a fixed px/em value or let it default.

-                        <Tooltip
-                            maxWidth={project.name.length.toString()}
+                        <Tooltip
+                            maxWidth="240"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2454d43 and a6eb2da.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (36)
  • package.json (1 hunks)
  • src/lib/components/archiveProject.svelte (1 hunks)
  • src/lib/components/billing/alerts/limitReached.svelte (1 hunks)
  • src/lib/components/billing/alerts/projectsLimit.svelte (2 hunks)
  • src/lib/components/billing/alerts/selectProjectCloud.svelte (2 hunks)
  • src/lib/components/billing/planComparisonBox.svelte (1 hunks)
  • src/lib/components/billing/planExcess.svelte (3 hunks)
  • src/lib/components/billing/planSelection.svelte (1 hunks)
  • src/lib/components/billing/usageRates.svelte (2 hunks)
  • src/lib/components/estimatedCard.svelte (1 hunks)
  • src/lib/components/gridItem1.svelte (1 hunks)
  • src/lib/components/index.ts (1 hunks)
  • src/lib/components/labelCard.svelte (1 hunks)
  • src/lib/components/organizationUsageLimits.svelte (1 hunks)
  • src/lib/components/progressbar/ProgressBar.svelte (1 hunks)
  • src/lib/helpers/string.ts (1 hunks)
  • src/lib/layout/createProject.svelte (5 hunks)
  • src/lib/sdk/billing.ts (6 hunks)
  • src/lib/stores/billing.ts (7 hunks)
  • src/routes/(console)/+layout.ts (1 hunks)
  • src/routes/(console)/create-organization/+page.ts (2 hunks)
  • src/routes/(console)/organization-[organization]/+page.svelte (7 hunks)
  • src/routes/(console)/organization-[organization]/billing/+page.svelte (3 hunks)
  • src/routes/(console)/organization-[organization]/billing/addCreditModal.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/billing/availableCredit.svelte (2 hunks)
  • src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/billing/planSummary.svelte (4 hunks)
  • src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/budgetLimitAlert.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/change-plan/+page.svelte (8 hunks)
  • src/routes/(console)/organization-[organization]/change-plan/+page.ts (2 hunks)
  • src/routes/(console)/organization-[organization]/header.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/members/+page.svelte (1 hunks)
  • src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte (2 hunks)
  • src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte (1 hunks)
  • src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/lib/helpers/string.ts (1)
src/lib/stores/viewport.ts (1)
  • isSmallViewport (3-3)
src/routes/(console)/create-organization/+page.ts (2)
src/lib/stores/billing.ts (1)
  • paymentMethods (73-73)
src/lib/stores/sdk.ts (1)
  • sdk (142-165)
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (3)
src/lib/sdk/billing.ts (2)
  • AggregationTeam (154-211)
  • InvoiceUsage (221-225)
src/lib/stores/sdk.ts (1)
  • sdk (142-165)
src/routes/(console)/project-[region]-[project]/overview/store.ts (1)
  • usage (8-26)
src/routes/(console)/organization-[organization]/change-plan/+page.ts (2)
src/routes/(console)/create-organization/+page.ts (1)
  • load (7-32)
src/lib/stores/sdk.ts (1)
  • sdk (142-165)
src/lib/stores/billing.ts (2)
src/lib/stores/organization.ts (3)
  • organization (62-62)
  • currentPlan (63-63)
  • members (64-64)
src/lib/sdk/billing.ts (2)
  • AggregationTeam (154-211)
  • Plan (377-433)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e
  • GitHub Check: build
🔇 Additional comments (28)
src/lib/components/billing/alerts/selectProjectCloud.svelte (1)

10-10: Switch to org store: verify readiness of billingNextInvoiceDate

Good move using the organization store. Ensure $organization.billingNextInvoiceDate is always populated when this modal is shown; otherwise template evaluation can throw. Consider a null-safe fallback in the alert title (see other comment).

src/lib/layout/createProject.svelte (1)

7-7: Imports and new prop look good

Adding org/currentPlan access, BillingPlan, formatCurrency, and the optional billingPlan prop is consistent with the new flow. No issues.

Also applies to: 14-15, 23-23, 32-32

src/lib/components/organizationUsageLimits.svelte (1)

87-92: Analytics event name/source

“OrganizationClickUpgrade” on a downgrade/manage flow is misleading. Consider dedicated event or add action metadata.

If “Upgrade” is intentional, please confirm. Otherwise, switch to a more specific event (e.g., OrganizationClickManageProjects) or include action: 'manage_projects_downgrade' in the payload.

src/lib/sdk/billing.ts (2)

936-951: Breaking return type change: getAggregation

Confirm all consumers updated from Aggregation to AggregationTeam.

If not, update their imports/usages accordingly. I can generate a repo scan to locate usages.


440-441: PlansMap key type change

Map<string, Plan> replaces Map<Tier, Plan>. Verify all stores/selectors (e.g., plansInfo) and lookups updated to string keys.

I can run a repo scan to find Map<Tier, Plan> usages if needed.

src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte (1)

245-247: Wording LGTM

Clearer scope: “in this project”. No further changes needed here.

src/lib/stores/billing.ts (5)

17-24: Type import switch to AggregationTeam looks consistent.

No issues spotted with the new type import.


242-243: New derived store for pricing modal looks good.

useNewPricingModal cleanly mirrors Plan.usagePerProject.


260-267: Exempting 'members' from project limitations on Pro/Scale is correct.

Early return prevents false limitation flags for unlimited seats.


273-274: Comment matches behavior.

Members limitation now only relevant on Free.


399-402: Infinity guard for members overflow is solid.

Prevents false positives on Pro/Scale.

src/lib/components/billing/planExcess.svelte (3)

2-8: Import updates look correct.

Switching to getServiceLimit and typed Tier aligns with the store changes.


19-19: Type switch to AggregationTeam is consistent with SDK.

No concerns.


31-31: Please verify how calculateExcess handles members by running:

rg -n -C5 'calculateExcess' src

and sharing the returned implementation.

src/lib/components/billing/alerts/projectsLimit.svelte (1)

6-8: Imports updated to use organization store and remove deprecated constant.

Looks good.

src/routes/(console)/+layout.ts (1)

1-1: LGTM: import relocation

Top-level import of Dependencies is cleaner; no behavior change.

src/lib/components/index.ts (1)

87-87: LGTM: public export for EstimatedCard

Re-export looks correct and keeps API consistent.

src/routes/(console)/organization-[organization]/change-plan/+page.ts (1)

4-4: Import of sdk is correct

No issues with adding the SDK import.

src/routes/(console)/create-organization/+page.ts (1)

10-14: LGTM: exposing plans in parallel.

Parallelizing listPlans() and returning plans is clean and consistent with existing data flow.

Also applies to: 27-27

src/routes/(console)/organization-[organization]/billing/+page.svelte (1)

5-5: LGTM: feature-flagged PlanSummary switch.

The $useNewPricingModal toggle cleanly selects between new/legacy components with appropriate props.

Also applies to: 11-11, 21-21, 131-142

src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)

126-132: Downgrade validation gate looks correct.

Validating project selection only when the target plan has a positive projects cap matches the “0 = unlimited” convention used elsewhere.


387-393: OrganizationUsageLimits wiring looks good.

Passing projects, members, and storage usage with a bound component reference aligns with the validation flow.

src/lib/components/archiveProject.svelte (1)

50-56: Remove runes configuration suggestion: SvelteKit with Svelte 5 auto-enables runes mode, so $props() and $state() compile without any <svelte:options runes> or classic-syntax fallback.

Likely an incorrect or invalid review comment.

src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts (2)

1-1: Type updates align with new billing model

Importing AggregationTeam and InvoiceUsage matches the SDK changes. LGTM.


26-33: Graceful fallback when no invoice param

The try/catch around billingAggregationId is a good guard for orgs without aggregations. LGTM.

src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte (1)

127-139: Avoid NaN when availableCredit is undefined.

Guard with default 0.

-                                -{formatCurrency(
-                                    Math.min(availableCredit, currentInvoice?.amount ?? 0)
-                                )}
+                                -{formatCurrency(
+                                    Math.min(availableCredit ?? 0, currentInvoice?.amount ?? 0)
+                                )}

Likely an incorrect or invalid review comment.

src/routes/(console)/organization-[organization]/billing/planSummary.svelte (2)

169-182: Filter add-ons only; don’t rely on plan addons keys alone.

Current filter is fine but ensure seats/projects mapping matches resourceId names. If those can be absent, guard currentPlan.addons.

Do resourceId values always match Object.keys(currentPlan.addons) (‘seats’, ‘projects’, etc.)? If not, we should maintain a mapping table.


521-567: Actions area parity with legacy component looks good.

Logic mirrors PlanSummaryOld (upgrade, change plan, view usage).

Comment on lines +25 to +29
"@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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Pin-to-URL deps: confirm policy, reproducibility, and peer compat before merge

You switched three deps to URL/commit sources. If these are meant only for preview builds, ensure this is allowed on main; otherwise replace with tagged releases to keep reproducible installs and clearer provenance. Also sanity-check peer deps (Svelte 5/SvelteKit/Vite) for these internal builds.

Run to verify lock entries and obvious peer issues:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Pinned deps:"
jq -r '.dependencies["@appwrite.io/console"], .dependencies["@appwrite.io/pink-svelte"], .dependencies["@appwrite.io/pink-icons-svelte"]' package.json

echo -e "\nLockfile refs:"
rg -nC1 -e '@appwrite.io/console' -e '@appwrite.io/pink-svelte' -e '@appwrite.io/pink-icons-svelte' pnpm-lock.yaml || true

echo -e "\nPeer requirements mentioning svelte/vite/kit:"
rg -nC2 'peerDependencies' pnpm-lock.yaml | rg -n 'svelte|vite|sveltekit' -n -C1 || true

Length of output: 9064


Confirm URL pinnings align with policy and fix Svelte peer mismatches

Lockfile confirms reproducible installs for the three URL-pinned deps (package.json lines 25–29). However, both @appwrite.io/[email protected] and @appwrite.io/[email protected] declare peerDependencies: svelte: ^4.0.0 while the project resolves Svelte v5.25.3→v5 is outside ^4.0.0. Upgrade these packages to Svelte 5–compatible releases or adjust their peer declarations. Also verify that pinning via commit URLs is permitted on main.

🤖 Prompt for AI Agents
In package.json around lines 25 to 29, two URL-pinned packages
(@appwrite.io/pink-icons-svelte and @appwrite.io/pink-svelte) declare
peerDependencies requiring Svelte ^4.0.0 while the project uses Svelte v5.x,
causing a peer mismatch; upgrade those two packages to releases that explicitly
support Svelte 5 (or update their peerDependencies to allow ^5.0.0 if you
control the packages), update the package.json entries to the
Svelte‑5‑compatible versions or corrected peer ranges, run npm/yarn install to
regenerate the lockfile, and verify that pinning via commit/URL is permitted on
main per repo policy before committing the lockfile and package.json changes.

Comment on lines +165 to +167
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential crash when project.platforms is undefined

Archived or legacy projects may not have platforms. Guard before mapping.

-                        {@const platforms = filterPlatforms(
-                            project.platforms.map((platform) => getPlatformInfo(platform.type))
-                        )}
+                        {@const platforms = filterPlatforms(
+                            (project.platforms ?? []).map((platform) => getPlatformInfo(platform.type))
+                        )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{@const platforms = filterPlatforms(
project.platforms.map((platform) => getPlatformInfo(platform.type))
)}
{@const platforms = filterPlatforms(
(project.platforms ?? []).map((platform) => getPlatformInfo(platform.type))
)}
🤖 Prompt for AI Agents
In src/lib/components/archiveProject.svelte around lines 165 to 167, mapping
over project.platforms can throw when project.platforms is undefined; guard the
access by checking project.platforms is an array (or use optional chaining) and
pass a safe default (e.g., an empty array) into the map/filter call so
filterPlatforms always receives an array of platform info; update the expression
to use project.platforms ?? [] or Array.isArray check before mapping to avoid
runtime crashes for archived/legacy projects.

import { currentPlan, organization } from '$lib/stores/organization';
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
import { LabelCard } from '..';
import type { Plan } from '$lib/sdk/billing';
import { page } from '$app/state';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix import: use public SvelteKit store path

$app/state isn’t a public API. Use $app/stores for the page store to avoid build/runtime breakage.

-import { page } from '$app/state';
+import { page } from '$app/stores';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { page } from '$app/state';
import { page } from '$app/stores';
🤖 Prompt for AI Agents
In src/lib/components/billing/planSelection.svelte around line 8, the file
imports the page store from the private path '$app/state'; replace that import
with the public API by importing page from '$app/stores' instead (i.e., change
the import line to use '$app/stores') so the build/runtime won't break.

@@ -1,6 +1,6 @@
<script lang="ts">
import { Card, Layout, Typography } from '@appwrite.io/pink-svelte';
export let href: string;
export let href: string = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type mismatch: string prop defaulted to null (will fail under strictNullChecks)

Declare the union or make the prop optional to avoid TS errors and accidental href="null".

-    export let href: string = null;
+    // Option A (recommended): optional prop, omitted when unset
+    export let href?: string;
+    // Option B: explicit union
+    // export let href: string | null = null;
🤖 Prompt for AI Agents
In src/lib/components/gridItem1.svelte around line 3, the prop is typed as
string but defaulted to null which fails under strictNullChecks; update the prop
declaration to either allow null (export let href: string | null = null) or make
it optional with an undefined default (export let href?: string = undefined) and
ensure any usage checks for href before using it to avoid rendering the literal
"null".

Comment on lines +33 to +41
let freePlanLimits = $derived({
projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
members: getServiceLimit('members', BillingPlan.FREE),
storage: getServiceLimit('storage', BillingPlan.FREE)
});

// When preparing to downgrade to Free, enforce Free plan limit locally (2)
let allowedProjectsToKeep = $derived(freePlanLimits.projects);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden free-plan limits with safe defaults

Undefined limits render NaN/undefined in UI and break comparisons.

Apply:

 let freePlanLimits = $derived({
-    projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
-    members: getServiceLimit('members', BillingPlan.FREE),
-    storage: getServiceLimit('storage', BillingPlan.FREE)
+    projects: $plansInfo?.get(BillingPlan.FREE)?.projects ?? 0,
+    members: getServiceLimit('members', BillingPlan.FREE) ?? 0,
+    storage: getServiceLimit('storage', BillingPlan.FREE) ?? 0
 });
 
-// When preparing to downgrade to Free, enforce Free plan limit locally (2)
-let allowedProjectsToKeep = $derived(freePlanLimits.projects);
+// When preparing to downgrade to Free, enforce Free plan limit locally
+let allowedProjectsToKeep = $derived(Number(freePlanLimits.projects ?? 0));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let freePlanLimits = $derived({
projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
members: getServiceLimit('members', BillingPlan.FREE),
storage: getServiceLimit('storage', BillingPlan.FREE)
});
// When preparing to downgrade to Free, enforce Free plan limit locally (2)
let allowedProjectsToKeep = $derived(freePlanLimits.projects);
let freePlanLimits = $derived({
projects: $plansInfo?.get(BillingPlan.FREE)?.projects ?? 0,
members: getServiceLimit('members', BillingPlan.FREE) ?? 0,
storage: getServiceLimit('storage', BillingPlan.FREE) ?? 0
});
// When preparing to downgrade to Free, enforce Free plan limit locally
let allowedProjectsToKeep = $derived(Number(freePlanLimits.projects ?? 0));
🤖 Prompt for AI Agents
In src/lib/components/organizationUsageLimits.svelte around lines 33-41,
freePlanLimits and allowedProjectsToKeep can be undefined causing NaN/undefined
in the UI and broken comparisons; ensure each limit value falls back to a safe
default (e.g. 0) when $plansInfo or getServiceLimit returns undefined by
applying nullish coalescing or explicit Number coercion (e.g. value ?? 0 or
Number(value) || 0), and update the derived for allowedProjectsToKeep to use the
safe numeric projects value and clamp it as needed so downstream
math/comparisons never receive undefined/NaN.

Comment on lines +62 to +70
{#if currentPlan.budgeting && extraUsage > 0}
<Accordion
hideDivider
title="Add-ons"
badge={(currentAggregation.additionalMembers > 0
? currentInvoice.usage.length + 1
: currentInvoice.usage.length
).toString()}>
<svelte:fragment slot="end">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Null-guard currentAggregation and currentInvoice in badge calculation.

Avoid runtime errors when either object is undefined.

-                            badge={(currentAggregation.additionalMembers > 0
-                                ? currentInvoice.usage.length + 1
-                                : currentInvoice.usage.length
-                            ).toString()}>
+                            badge={(
+                                ((currentAggregation?.additionalMembers ?? 0) > 0
+                                    ? (currentInvoice?.usage?.length ?? 0) + 1
+                                    : (currentInvoice?.usage?.length ?? 0))
+                            ).toString()}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{#if currentPlan.budgeting && extraUsage > 0}
<Accordion
hideDivider
title="Add-ons"
badge={(currentAggregation.additionalMembers > 0
? currentInvoice.usage.length + 1
: currentInvoice.usage.length
).toString()}>
<svelte:fragment slot="end">
{#if currentPlan.budgeting && extraUsage > 0}
<Accordion
hideDivider
title="Add-ons"
badge={(
((currentAggregation?.additionalMembers ?? 0) > 0
? (currentInvoice?.usage?.length ?? 0) + 1
: (currentInvoice?.usage?.length ?? 0))
).toString()}>
<svelte:fragment slot="end">
🤖 Prompt for AI Agents
In
src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
around lines 62 to 70, the badge calculation uses currentAggregation and
currentInvoice without null-guards which can throw if either is undefined;
update the badge expression to safely handle missing values by using optional
chaining and sensible defaults (e.g., treat missing additionalMembers as 0 and
missing invoice usage as an empty array) and compute the count as a number
before calling toString(); this prevents runtime errors and preserves the
existing logic of adding one when additionalMembers > 0.

Comment on lines +74 to +92
{#if currentAggregation.additionalMembers}
<Layout.Stack gap="xxxs">
<Layout.Stack
direction="row"
justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary"
>Additional members</Typography.Text>
<Typography.Text>
{formatCurrency(
currentAggregation.additionalMemberAmount
)}
</Typography.Text>
</Layout.Stack>
<Layout.Stack direction="row">
<Typography.Text
>{currentAggregation.additionalMembers}</Typography.Text>
</Layout.Stack>
</Layout.Stack>
{/if}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Optional-chain all currentAggregation usages in this block.

Direct property access can throw when undefined.

-                                {#if currentAggregation.additionalMembers}
+                                {#if (currentAggregation?.additionalMembers ?? 0) > 0}
...
-                                                {formatCurrency(
-                                                    currentAggregation.additionalMemberAmount
-                                                )}
+                                                {formatCurrency(currentAggregation?.additionalMemberAmount ?? 0)}
...
-                                                >{currentAggregation.additionalMembers}</Typography.Text>
+                                                >{currentAggregation?.additionalMembers ?? 0}</Typography.Text>
...
-                                        {#if i > 0 || currentAggregation.additionalMembers}
+                                        {#if i > 0 || (currentAggregation?.additionalMembers ?? 0) > 0}

Also applies to: 95-97

🤖 Prompt for AI Agents
In
src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
around lines 74 to 92 (and likewise lines 95 to 97), the template uses direct
property access on currentAggregation which can be undefined; update all usages
in this block to use optional chaining (e.g.,
currentAggregation?.additionalMembers,
currentAggregation?.additionalMemberAmount,
currentAggregation?.additionalMembers) so the conditional and displayed values
do not throw when currentAggregation is undefined. Ensure the if check itself
uses currentAggregation?.additionalMembers and every interpolation and property
access in the nested elements uses optional chaining as well.

Comment on lines +111 to +119
<Tooltip
placement="bottom"
disabled={excess.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess.value)}
</svelte:fragment>
<span>{abbreviateNumber(excess.value)}</span>
</Tooltip>
</Layout.Stack>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle possibly undefined excess.value.

Prevents NaN and tooltip misbehavior.

-                                                    <Tooltip
-                                                        placement="bottom"
-                                                        disabled={excess.value <= 1000}>
+                                                    <Tooltip
+                                                        placement="bottom"
+                                                        disabled={(excess?.value ?? 0) <= 1000}>
                                                         <svelte:fragment slot="tooltip">
-                                                        {formatNumberWithCommas(excess.value)}
+                                                        {formatNumberWithCommas(excess?.value ?? 0)}
                                                         </svelte:fragment>
-                                                    <span>{abbreviateNumber(excess.value)}</span>
+                                                    <span>{abbreviateNumber(excess?.value ?? 0)}</span>
                                                     </Tooltip>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tooltip
placement="bottom"
disabled={excess.value <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess.value)}
</svelte:fragment>
<span>{abbreviateNumber(excess.value)}</span>
</Tooltip>
</Layout.Stack>
<Tooltip
placement="bottom"
disabled={(excess?.value ?? 0) <= 1000}>
<svelte:fragment slot="tooltip">
{formatNumberWithCommas(excess?.value ?? 0)}
</svelte:fragment>
<span>{abbreviateNumber(excess?.value ?? 0)}</span>
</Tooltip>
🤖 Prompt for AI Agents
In
src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
around lines 111 to 119, excess.value can be undefined which causes NaN and
tooltip misbehavior; update the code to safely handle this by normalizing
excessValue = excess?.value ?? 0 (or explicitly check typeof excess?.value ===
'number') and use that normalized value for disabled, formatNumberWithCommas,
and abbreviateNumber, or conditionally render the Tooltip/span only when
excessValue is a finite number so the tooltip and displayed text never receive
undefined/NaN.

Comment on lines +158 to +166
{formatCurrency(
Math.max(
(currentInvoice?.amount ?? 0) -
Math.min(availableCredit, currentInvoice?.amount ?? 0),
0
)
)}
</Typography.Text>
</Layout.Stack>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same NaN issue in “Current total (USD)”.

Default availableCredit to 0.

-                                {formatCurrency(
-                                    Math.max(
-                                        (currentInvoice?.amount ?? 0) -
-                                            Math.min(availableCredit, currentInvoice?.amount ?? 0),
-                                        0
-                                    )
-                                )}
+                                {formatCurrency(
+                                    Math.max(
+                                        (currentInvoice?.amount ?? 0) -
+                                            Math.min(availableCredit ?? 0, currentInvoice?.amount ?? 0),
+                                        0
+                                    )
+                                )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{formatCurrency(
Math.max(
(currentInvoice?.amount ?? 0) -
Math.min(availableCredit, currentInvoice?.amount ?? 0),
0
)
)}
</Typography.Text>
</Layout.Stack>
{formatCurrency(
Math.max(
(currentInvoice?.amount ?? 0) -
Math.min(availableCredit ?? 0, currentInvoice?.amount ?? 0),
0
)
)}
🤖 Prompt for AI Agents
In
src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
around lines 158 to 166, the expression computing "Current total (USD)" can
produce NaN when availableCredit is undefined; default availableCredit to 0.
Update the expression to use a safe fallback (e.g., replace availableCredit with
(availableCredit ?? 0) or initialize the variable to 0 where it’s derived) so
Math.min and the subtraction always operate on numbers and formatCurrency never
receives NaN.

Comment on lines +194 to +201
{#if $organization?.billingPlanDowngrade !== null}
<Button text on:click={() => (showCancel = true)}>Cancel change</Button>
{:else}
<Button
text
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Treat both null and undefined as “no downgrade scheduled”.

Current strict check may show “Cancel change” prematurely.

-                    {#if $organization?.billingPlanDowngrade !== null}
+                    {#if $organization?.billingPlanDowngrade != null}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{#if $organization?.billingPlanDowngrade !== null}
<Button text on:click={() => (showCancel = true)}>Cancel change</Button>
{:else}
<Button
text
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
{#if $organization?.billingPlanDowngrade != null}
<Button text on:click={() => (showCancel = true)}>Cancel change</Button>
{:else}
<Button
text
disabled={$organization?.markedForDeletion}
href={$upgradeURL}
on:click={() =>
🤖 Prompt for AI Agents
In
src/routes/(console)/organization-[organization]/billing/planSummaryOld.svelte
around lines 194–201, the conditional currently only checks !== null for
billingPlanDowngrade causing undefined to be treated as a scheduled downgrade;
change the check to treat both null and undefined as "no downgrade scheduled"
(e.g., use a nullish check such as billingPlanDowngrade == null or explicitly
check both undefined and null) so the "Cancel change" button only appears when a
downgrade is actually scheduled.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/lib/sdk/billing.ts (3)

213-219: Constrain region to AllowedRegions

Use the existing AllowedRegions union for stricter typing and earlier detection of invalid regions.

 export type AggregationBreakdown = {
   $id: string;
   name: string;
   amount: number;
-  region: string;
+  region: AllowedRegions;
   resources: InvoiceUsage[];
 };

578-592: Avoid duplicate plans endpoints; delegate getPlansInfo() to listPlans(); align HTTP verb case

  • listPlans duplicates getPlansInfo functionality; prefer a single entry point with queries.
  • For consistency with the rest of the file, use 'GET'.
-        return await this.client.call(
-            'get',
+        return await this.client.call(
+            'GET',
             uri,
             {
                 'content-type': 'application/json'
             },
             params
         );

Also update getPlansInfo to delegate:

@@
-    async getPlansInfo(): Promise<PlanList> {
-        const path = `/console/plans`;
-        const params = {};
-        const uri = new URL(this.client.config.endpoint + path);
-        return await this.client.call(
-            'GET',
-            uri,
-            {
-                'content-type': 'application/json'
-            },
-            params
-        );
-    }
+    /**
+     * @deprecated Use listPlans(queries?: string[]) instead.
+     */
+    async getPlansInfo(): Promise<PlanList> {
+        return this.listPlans();
+    }

154-211: Extract common fields into a base aggregation type
Define an AggregationBase for the overlapping properties and extend it into a summary type (Aggregation) and a detailed type (AggregationTeam) so the shape of .resources is explicit. Verify all existing getAggregation() consumers still use the detailed model and update any (currently unused) listAggregation() consumers to the summary model.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a6eb2da and 726986b.

📒 Files selected for processing (2)
  • src/lib/layout/createProject.svelte (5 hunks)
  • src/lib/sdk/billing.ts (7 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: e2e
🔇 Additional comments (4)
src/lib/sdk/billing.ts (2)

221-225: LGTM: InvoiceUsage shape is clear and minimal

No issues from a typing perspective.


375-376: Confirm server guarantees planIncluded; make optional if not

If the backend doesn’t always return planIncluded, mark it optional to avoid widening with undefined at runtime in strict mode.

Option if needed:

-    planIncluded: number;
+    planIncluded?: number;
src/lib/layout/createProject.svelte (2)

103-116: LGTM! Informative project limit alert with upgrade path

The alert properly informs users when they've reached their project limit and provides a clear upgrade path via the billing page. The compact button design and external link to the billing section provide a good user experience for plan upgrades.


97-99: Verify fallback project addon price
File: src/lib/layout/createProject.svelte (lines 97-99): the hardcoded fallback value of 15 may not reflect the current project-addon pricing—confirm it against your pricing configuration.

Comment on lines +393 to +399
databasesReads: number;
databasesWrites: number;
buckets: number;
fileSize: number;
functions: number;
executions: number;
GBHours: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New Plan fields: verify API casing and downstream usage

  • Verify backend JSON uses these exact keys (databasesReads, databasesWrites, GBHours, imageTransformations) and that consumers access them with the same casing.
  • Confirm UI/store code accounts for usagePerProject when computing/visualizing limits.

Script to locate usages and potential mismatches:

Also applies to: 410-414, 434-434


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP -C2 '\b(databasesReads|databasesWrites|GBHours|imageTransformations|usagePerProject)\b' --type=ts --type=tsx

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -nP -C2 '\b(databasesReads|databasesWrites|GBHours|imageTransformations|usagePerProject)\b' --glob '*.ts' --glob '*.tsx' -n

Length of output: 5741


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '130,260p' src/lib/stores/billing.ts

Length of output: 4059


Add missing plan fields to UI store and verify API usage

  • PlanServices in src/lib/stores/billing.ts doesn’t include databasesReads, databasesWrites, or GBHours; add them so getServiceLimit (and related fee/limit logic) handles these metrics.
  • Confirm backend JSON uses the exact keys (databasesReads, databasesWrites, GBHours, imageTransformations) and that all consumers reference them with matching casing.
  • UI/store code already derives useNewPricingModal from usagePerProject; ensure any visualization or limit-calculation logic fully honors this flag.
🤖 Prompt for AI Agents
In src/lib/sdk/billing.ts around lines 393-399, the PlanServices type/listing is
missing fields used by the UI/store; add `databasesReads: number`,
`databasesWrites: number`, and `GBHours: number` to that definition, and confirm
`imageTransformations` exists if used. Then update src/lib/stores/billing.ts
(PlanServices) to include the same three fields with identical casing so
getServiceLimit and related fee/limit logic can consume them; audit all
consumers to reference these exact key names/casing (`databasesReads`,
`databasesWrites`, `GBHours`, `imageTransformations`) and adjust any mismatches.
Finally ensure any visualization or limit-calculation paths respect the existing
useNewPricingModal derived from usagePerProject (i.e., branch logic uses the
flag to select the new pricing logic and includes the new metrics in
calculations).

};

export type PlanList = {
plans: Plan[];
total: number;
};

export type PlansMap = Map<Tier, Plan>;
export type PlansMap = Map<string, Plan>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Map vs Record: consider serialization/reactivity and verify usage

Switching PlansMap to Map<string, Plan> can complicate JSON serialization, SSR hydration, and Svelte reactivity compared to a plain object/Record. If you don’t need Map semantics, prefer Record for simplicity.

-export type PlansMap = Map<string, Plan>;
+export type PlansMap = Record<string, Plan>;

To audit usage patterns:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Declarations/uses of PlansMap:"
rg -nP -C2 '\bPlansMap\b' --type=ts --type=tsx

echo "Bracket vs Map.get usage:"
rg -nP -C2 '\[[^\]]+\]|\.get\s*\(' --type=ts --type=tsx | sed -n '1,200p'

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for declarations/uses of PlansMap in .ts & .tsx files:"
rg -nC2 '\bPlansMap\b' -g '*.ts' -g '*.tsx'

echo "Searching for Map.get usage:"
rg -nC2 '\.get\s*\(' -g '*.ts' -g '*.tsx'

echo "Searching for bracket property access usage:"
rg -nC2 '\[[^]]+\]' -g '*.ts' -g '*.tsx'

Length of output: 231243


Migrate PlansMap to Record: update type and .get() usages

  • In src/lib/sdk/billing.ts change
    -export type PlansMap = Map<string, Plan>;
    +export type PlansMap = Record<string, Plan>;
  • In src/lib/stores/billing.ts (≈ line 159) replace
    -plan ??= info.get(tier ?? get(organization)?.billingPlan);
    +plan ??= info[tier ?? get(organization)?.billingPlan];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type PlansMap = Map<string, Plan>;
// src/lib/sdk/billing.ts
export type PlansMap = Record<string, Plan>;
Suggested change
export type PlansMap = Map<string, Plan>;
// src/lib/stores/billing.ts (≈ line 159)
plan ??= info[tier ?? get(organization)?.billingPlan];
🤖 Prompt for AI Agents
In src/lib/sdk/billing.ts around line 441 change the PlansMap exported type from
Map<string, Plan> to a plain object Record<string, Plan>, and then update all
usages that call .get(...) to use bracket/property access (e.g., plans[key] or
plansMap[id]) or optional chaining (plansMap[id]?.property) instead of Map API;
specifically update src/lib/stores/billing.ts (around line 159) to replace any
plansMap.get(key) calls with plansMap[key] and adjust any iteration or existence
checks (use Object.keys/plansMap[key] !== undefined or key in plansMap) to match
the Record type.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (2)

387-393: Nice: Centralizing downgrade constraints via OrganizationUsageLimits

Binding to the component and passing org, members, projects, and storage usage consolidates validation and selection in one place. Consider showing a loading/skeleton state or disabling actions until allProjects and orgUsage are loaded to avoid transient warnings.


103-117: Parallelize data fetch + handle projects pagination

  • Fetching usage and projects sequentially adds latency.
  • The projects list is capped at 1000; large orgs may exceed this.

Refactor to parallelize and paginate.

-        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: [] };
-        }
+        const [usageRes, firstPage] = await Promise.allSettled([
+            sdk.forConsole.billing.listUsage(data.organization.$id),
+            sdk.forConsole.projects.list([
+                Query.equal('teamId', data.organization.$id),
+                Query.limit(100)
+            ])
+        ]);
+        orgUsage = usageRes.status === 'fulfilled' ? usageRes.value : undefined;
+        // TODO: iterate with cursor to fetch all pages if needed
+        allProjects =
+            firstPage.status === 'fulfilled' ? firstPage.value : { projects: [] };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 726986b and b322171.

📒 Files selected for processing (1)
  • src/routes/(console)/organization-[organization]/change-plan/+page.svelte (9 hunks)
🧰 Additional context used
🪛 GitHub Actions: Tests
src/routes/(console)/organization-[organization]/change-plan/+page.svelte

[warning] 1-1: Prettier formatting issue detected. Run 'prettier --write' to fix.

🔇 Additional comments (3)
src/routes/(console)/organization-[organization]/change-plan/+page.svelte (3)

126-132: Good pre-check for project selection before downgrade

Validating with usageLimitsComponent.validateOrAlert() when the target plan enforces a projects limit is the right UX guardrail.


35-35: Formatting is up-to-date Running pnpm run format yields no changes—code is already correctly formatted.


169-169: Confirm backend guardrails & switch to non‐blocking telemetry

  • Verify the backend enforces new plan limits (e.g. truncates or blocks over‐limit projects) so a failed updateSelectedProjects call can’t leave the org exceeding its quota.
  • Replace the blocking call
    - await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]);
    + // Don’t block success UX on telemetry/network hiccups
    + await Promise.allSettled([
    +   trackDowngradeFeedback(),
    +   invalidate(Dependencies.ORGANIZATION)
    + ]);
    to avoid surfacing errors when reporting feedback or invalidating caches.

Comment on lines +64 to 66
let orgUsage: OrganizationUsage;
let allProjects: { projects: Models.Project[] } | undefined;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix: orgUsage type is assigned undefined but not typed as optional

orgUsage is set to undefined in the catch block, which violates its declared type and will fail TS checks.

Apply this diff:

-    let orgUsage: OrganizationUsage;
+    let orgUsage: OrganizationUsage | undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let orgUsage: OrganizationUsage;
let allProjects: { projects: Models.Project[] } | undefined;
let orgUsage: OrganizationUsage | undefined;
let allProjects: { projects: Models.Project[] } | undefined;
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/change-plan/+page.svelte
around lines 64 to 66, orgUsage is declared as OrganizationUsage but is later
assigned undefined in a catch block; update the declaration to allow undefined
(e.g., OrganizationUsage | undefined) or initialize it with a proper default
value so the variable's type matches all assignments and TypeScript checks pass.

Comment on lines +358 to +363
{@const extraMembers = collaborators?.length ?? 0}
{@const price = formatCurrency(
extraMembers *
($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
)}
{#if selectedPlan === BillingPlan.PRO}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: extraMembers uses “collaborators” (invite list), not current team seats

This will show $0 for many orgs and mislead users. Compute excess seats from current memberships (minus owners and included seats) instead.

-                        {@const extraMembers = collaborators?.length ?? 0}
-                        {@const price = formatCurrency(
-                            extraMembers *
-                                ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
-                        )}
+                        {@const teamMembers = data.members?.memberships ?? []}
+                        {@const includedSeats = $plansInfo?.get(selectedPlan)?.addons?.seats?.included ?? 0}
+                        {@const nonOwnerMembers = teamMembers.filter((m) => !m.roles?.includes?.('owner'))}
+                        {@const extraMembers = Math.max(0, nonOwnerMembers.length - includedSeats)}
+                        {@const price = formatCurrency(
+                            extraMembers * ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
+                        )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{@const extraMembers = collaborators?.length ?? 0}
{@const price = formatCurrency(
extraMembers *
($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
)}
{#if selectedPlan === BillingPlan.PRO}
{@const teamMembers = data.members?.memberships ?? []}
{@const includedSeats = $plansInfo?.get(selectedPlan)?.addons?.seats?.included ?? 0}
{@const nonOwnerMembers = teamMembers.filter((m) => !m.roles?.includes('owner'))}
{@const extraMembers = Math.max(0, nonOwnerMembers.length - includedSeats)}
{@const price = formatCurrency(
extraMembers * ($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
)}
{#if selectedPlan === BillingPlan.PRO}
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/change-plan/+page.svelte
around lines 358–363, extraMembers is incorrectly derived from the
invite/collaborators list; replace that with a calculation from the current
membership list: count active members excluding owners (e.g., filter memberships
by role !== 'owner' and active status), subtract the number of seats included in
the currently selectedPlan (from $plansInfo?.get(selectedPlan)?.includedSeats or
similar) and clamp the result to zero, then use that value to compute price via
the existing seats addon price; ensure the reactive const uses the actual
memberships variable and that the computation updates when memberships or
selectedPlan change.

Comment on lines +373 to 385
{:else if selectedPlan === BillingPlan.FREE}
<Alert.Inline
status="error"
title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${toLocaleDate(
$organization.billingNextInvoiceDate
)}`}>
You will retain access to {tierToPlan($organization.billingPlan)
.name} plan features until your billing period ends. After that,
<span class="u-bold"
>all team members except the owner will be removed,</span>
and service disruptions may occur if usage exceeds Free plan limits.
</Alert.Inline>
{/if}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard against missing billingNextInvoiceDate in downgrade notice

If $organization.billingNextInvoiceDate is null/undefined, toLocaleDate(...) may break or render “Invalid Date”. Provide a safe fallback.

-                            <Alert.Inline
-                                status="error"
-                                title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${toLocaleDate(
-                                    $organization.billingNextInvoiceDate
-                                )}`}>
+                            {@const nextInvoice = $organization.billingNextInvoiceDate
+                                ? toLocaleDate($organization.billingNextInvoiceDate)
+                                : null}
+                            <Alert.Inline
+                                status="error"
+                                title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${nextInvoice ?? 'the end of your current billing period'}`}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{:else if selectedPlan === BillingPlan.FREE}
<Alert.Inline
status="error"
title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${toLocaleDate(
$organization.billingNextInvoiceDate
)}`}>
You will retain access to {tierToPlan($organization.billingPlan)
.name} plan features until your billing period ends. After that,
<span class="u-bold"
>all team members except the owner will be removed,</span>
and service disruptions may occur if usage exceeds Free plan limits.
</Alert.Inline>
{/if}
{:else if selectedPlan === BillingPlan.FREE}
{@const nextInvoice = $organization.billingNextInvoiceDate
? toLocaleDate($organization.billingNextInvoiceDate)
: null}
<Alert.Inline
status="error"
title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${nextInvoice ?? 'the end of your current billing period'}`}>
You will retain access to {tierToPlan($organization.billingPlan)
.name} plan features until your billing period ends. After that,
<span class="u-bold"
>all team members except the owner will be removed,</span>
and service disruptions may occur if usage exceeds Free plan limits.
</Alert.Inline>
{/if}
🤖 Prompt for AI Agents
In src/routes/(console)/organization-[organization]/change-plan/+page.svelte
around lines 373 to 385, the downgrade notice calls
toLocaleDate($organization.billingNextInvoiceDate) directly which can throw or
render “Invalid Date” when billingNextInvoiceDate is null/undefined; fix by
computing a safe formattedDate variable before rendering (e.g., formattedDate =
$organization.billingNextInvoiceDate ?
toLocaleDate($organization.billingNextInvoiceDate) : 'N/A' or 'immediately') and
use that variable in the Alert title so the function is never called with a null
value and a sensible fallback is shown.

@lohanidamodar lohanidamodar merged commit f78cbef into main Sep 2, 2025
5 checks passed
@lohanidamodar lohanidamodar deleted the revert-2294-revert-2249-feat-SER-204-New-Archive-projects-ui-change branch September 2, 2025 02:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants