-
Notifications
You must be signed in to change notification settings - Fork 186
Feat: New Billing UI changes #2295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: New Billing UI changes #2295
Conversation
ConsoleProject ID: Sites (2)
Note Appwrite has a Discord community with over 16 000 members. |
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 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. 📒 Files selected for processing (2)
WalkthroughThis 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
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 AggregationTeamSingle 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/buildsMbSecondsTotalTypes 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 fromexecutions
incalculateExcess
so it uses the raw numbers.- Add a
users
field viacalculateResourceSurplus(addon.usageUsers, plan.users)
to satisfy the UI’sexcess.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 alertHandle 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 inequalityPrefer
!==
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 copyUse 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 invertedAdd 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 clampAvoid 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 ergonomicsAllow 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 branchDisabling 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 gatingBecause 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 assumptionsCurrent 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 booleanAvoid passing undefined; coerce the reactive flag.
- disabled={!isProPlan && projectsLimited} + disabled={!isProPlan && !!projectsLimited}
82-83
: Same boolean coercion for Region selectKeep the prop a strict boolean.
- disabled={!isProPlan && projectsLimited} + disabled={!isProPlan && !!projectsLimited}
91-114
: Use nullish coalescing for price default; guard copy against odd capsUsing || 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 archivedAvoid “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 valueMatch 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 listCleaner, 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 UIIf 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 getPlansInfoBoth 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 falsyWhen
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 falseSame 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;
tolet error: string | null = null;
in script.- Please confirm
Modal
indeed exports and handlessize="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’sPlanList
(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 returnsfalse
for non-finite numbers (includingInfinity
), you can drop the explicit equality check forInfinity
.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” branchThe 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
treatsundefined
asfalse
, enabling the tab before plan data loads. If the intent is to hide Usage unlessusagePerProject === 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 dataGuard against routes where
page.data.plans
isn’t populated to preventObject.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 betweenbillingPlan
and dynamicplan.$id
bind:group={billingPlan}
expects the same type asvalue={plan.$id}
. IfPlan.$id
goes beyond theBillingPlan
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 wiringYou set
tooltipShow
but no tooltip text/slot is provided here. EnsureLabelCard
shows a helpful message when Free is disabled (e.g., “An organization already has a Free plan”).
37-40
: Currency/label nuanceFor 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 projectsCounting 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 setshow
instead ofbind:show
.
244-249
: Badge count threshold off-by-oneYou 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 actionWhen 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 keysOnly 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
asproject.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.
⛔ 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 billingNextInvoiceDateGood 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 goodAdding 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: getAggregationConfirm 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 changeMap<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 LGTMClearer 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 mirrorsPlan.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 typedTier
aligns with the store changes.
19-19
: Type switch to AggregationTeam is consistent with SDK.No concerns.
31-31
: Please verify howcalculateExcess
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 relocationTop-level import of
Dependencies
is cleaner; no behavior change.src/lib/components/index.ts (1)
87-87
: LGTM: public export for EstimatedCardRe-export looks correct and keeps API consistent.
src/routes/(console)/organization-[organization]/change-plan/+page.ts (1)
4-4
: Import of sdk is correctNo issues with adding the SDK import.
src/routes/(console)/create-organization/+page.ts (1)
10-14
: LGTM: exposing plans in parallel.Parallelizing
listPlans()
and returningplans
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 modelImporting
AggregationTeam
andInvoiceUsage
matches the SDK changes. LGTM.
26-33
: Graceful fallback when no invoice paramThe 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 whenavailableCredit
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, guardcurrentPlan.addons
.Do
resourceId
values always matchObject.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).
"@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", |
There was a problem hiding this comment.
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.
{@const platforms = filterPlatforms( | ||
project.platforms.map((platform) => getPlatformInfo(platform.type)) | ||
)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
{@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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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".
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); | ||
|
There was a problem hiding this comment.
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.
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.
{#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"> |
There was a problem hiding this comment.
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.
{#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.
{#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} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
<Tooltip | ||
placement="bottom" | ||
disabled={excess.value <= 1000}> | ||
<svelte:fragment slot="tooltip"> | ||
{formatNumberWithCommas(excess.value)} | ||
</svelte:fragment> | ||
<span>{abbreviateNumber(excess.value)}</span> | ||
</Tooltip> | ||
</Layout.Stack> |
There was a problem hiding this comment.
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.
<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.
{formatCurrency( | ||
Math.max( | ||
(currentInvoice?.amount ?? 0) - | ||
Math.min(availableCredit, currentInvoice?.amount ?? 0), | ||
0 | ||
) | ||
)} | ||
</Typography.Text> | ||
</Layout.Stack> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
{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.
{#if $organization?.billingPlanDowngrade !== null} | ||
<Button text on:click={() => (showCancel = true)}>Cancel change</Button> | ||
{:else} | ||
<Button | ||
text | ||
disabled={$organization?.markedForDeletion} | ||
href={$upgradeURL} | ||
on:click={() => |
There was a problem hiding this comment.
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.
{#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.
There was a problem hiding this 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 AllowedRegionsUse 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 anAggregationBase
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 existinggetAggregation()
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.
📒 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 minimalNo issues from a typing perspective.
375-376
: Confirm server guarantees planIncluded; make optional if notIf 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 pathThe 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 of15
may not reflect the current project-addon pricing—confirm it against your pricing configuration.
databasesReads: number; | ||
databasesWrites: number; | ||
buckets: number; | ||
fileSize: number; | ||
functions: number; | ||
executions: number; | ||
GBHours: number; |
There was a problem hiding this comment.
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
, orGBHours
; add them sogetServiceLimit
(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
fromusagePerProject
; 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>; |
There was a problem hiding this comment.
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.
export type PlansMap = Map<string, Plan>; | |
// src/lib/sdk/billing.ts | |
export type PlansMap = Record<string, Plan>; |
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.
There was a problem hiding this 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 OrganizationUsageLimitsBinding 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
andorgUsage
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.
📒 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 downgradeValidating with
usageLimitsComponent.validateOrAlert()
when the target plan enforces a projects limit is the right UX guardrail.
35-35
: Formatting is up-to-date Runningpnpm 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
to avoid surfacing errors when reporting feedback or invalidating caches.- await Promise.all([trackDowngradeFeedback(), invalidate(Dependencies.ORGANIZATION)]); + // Don’t block success UX on telemetry/network hiccups + await Promise.allSettled([ + trackDowngradeFeedback(), + invalidate(Dependencies.ORGANIZATION) + ]);
let orgUsage: OrganizationUsage; | ||
let allProjects: { projects: Models.Project[] } | undefined; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
{@const extraMembers = collaborators?.length ?? 0} | ||
{@const price = formatCurrency( | ||
extraMembers * | ||
($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0) | ||
)} | ||
{#if selectedPlan === BillingPlan.PRO} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
{@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.
{: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} |
There was a problem hiding this comment.
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.
{: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.
Reverts #2294
Summary by CodeRabbit
New Features
Improvements
Chores