Skip to content

Conversation

@ItzNotABug
Copy link
Member

@ItzNotABug ItzNotABug commented Dec 18, 2025

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

Summary by CodeRabbit

  • New Features

    • Unified org/team listing and creation with resolved navigation; improved plan-aware routing.
  • Bug Fixes & Improvements

    • Payments, invoices, addresses, coupons, credits and Stripe flows use account/organization APIs for more consistent billing and checkout UX.
    • Plan names, limits, trials and upgrade prompts now reflect dynamic plan details and plan groups.
  • Refactor

    • Centralized Models-based types and new billing helpers for consistent plan/group logic.
  • Removed

    • Legacy billing enums and deprecated plan-selection UI elements removed.

✏️ Tip: You can customize this high-level summary in your review settings.

@ItzNotABug ItzNotABug self-assigned this Dec 18, 2025
@appwrite
Copy link

appwrite bot commented Dec 18, 2025

Console (appwrite/console)

Project ID: 688b7bf400350cbd60e9

Sites (1)
Site Status Logs Preview QR
 console-stage
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code

Tip

JWT tokens let functions act on behalf of users while preserving their permissions

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

The PR migrates billing and organization code to Appwrite Console Models and organization-scoped APIs. It replaces many sdk.forConsole.billing.* calls with sdk.forConsole.organizations., sdk.forConsole.account., or sdk.forConsole.console.; consolidates team/organization listing via getTeamOrOrganizationList; removes the Billing class and BillingPlan enum; adds utilities (billingIdToPlan, planHasGroup, getBasePlanFromGroup, getNextTierBillingPlan, makePlansMap, IconsMap); standardizes routing with resolve/route helpers; updates Stripe/account payment flows and confirmPayment signature; and converts many components, stores, routes, and props to use Models. types.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Billing SDK Refactor' is directly related to the main focus of the changeset, which involves extensive refactoring of billing-related APIs, types, and components throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Caution

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

⚠️ Outside diff range comments (2)
src/lib/stores/billing.ts (2)

258-275: Null dereference risk in checkForUsageFees.

billingIdToPlan(plan) can return null, causing billingPlan.usage (line 260) to throw a TypeError.

🐛 Suggested fix
 export function checkForUsageFees(plan: string, id: PlanServices) {
     const billingPlan = billingIdToPlan(plan);
+    if (!billingPlan) return false;
     const supportsUsage = Object.keys(billingPlan.usage).length > 0;

277-300: Same null dereference risk in checkForProjectLimitation.

billingIdToPlan(plan) can return null, causing billingPlan.projects (line 280) to throw.

🐛 Suggested fix
 export function checkForProjectLimitation(plan: string, id: PlanServices) {
     if (id === 'members') {
         const billingPlan = billingIdToPlan(plan);
+        if (!billingPlan) return true; // Default to limited if plan unknown
         const hasUnlimitedProjects = billingPlan.projects === 0;
🤖 Fix all issues with AI agents
In `@src/lib/stores/billing.ts`:
- Around line 142-154: getNextTierBillingPlan currently assumes
billingIdToPlan(billingPlanId) returns a non-null value and directly accesses
currentPlanData.order; add a null check after calling billingIdToPlan and handle
the null case by returning a sensible default (e.g., call
getBasePlanFromGroup(BillingPlanGroup.Pro) or throw a clear error), then proceed
to compute currentOrder and search plans as before; reference the
billingIdToPlan call, the currentPlanData variable, the plans = get(plansInfo)
usage, and fallback to getBasePlanFromGroup(BillingPlanGroup.Pro) when null.
- Around line 156-168: getPreviousTierBillingPlan currently assumes
billingIdToPlan(billingPlanId) and its .order exist and can dereference them;
mirror the defensive checks used in getNextTierBillingPlan by validating
currentPlanData is non-null and has a numeric order before using it, and if
invalid return getBasePlanFromGroup(BillingPlanGroup.Starter); iterate plans
from get(plansInfo) only after the guard and compare plan.order to currentOrder
- 1 to find and return the previous plan.
🧹 Nitpick comments (2)
src/routes/(console)/regions.ts (1)

31-31: Inconsistent terminology in error message.

The error message references teamId, but the parameter is named orgId and the API uses organizationId. Consider updating for consistency.

Suggested fix
-        console.error(`Failed to load regions for teamId: ${orgId}`, error);
+        console.error(`Failed to load regions for organizationId: ${orgId}`, error);
src/lib/stores/billing.ts (1)

85-87: Null propagation risk from makeBillingPlan to consumer functions.

billingIdToPlan returns Models.BillingPlan | null, but makeBillingPlan has no null guard. Consumer functions like isStarterPlan, canUpgrade, canDowngrade, and planHasGroup access properties (e.g., .group, .$id) on the result without null checks.

If an invalid billingId is passed, this will throw a TypeError.

♻️ Suggested defensive handling
 function makeBillingPlan(billingPlanOrId: string | Models.BillingPlan): Models.BillingPlan {
-    return typeof billingPlanOrId === 'string' ? billingIdToPlan(billingPlanOrId) : billingPlanOrId;
+    const plan = typeof billingPlanOrId === 'string' ? billingIdToPlan(billingPlanOrId) : billingPlanOrId;
+    if (!plan) {
+        throw new Error(`Invalid billing plan: ${billingPlanOrId}`);
+    }
+    return plan;
 }

Alternatively, update the return type to Models.BillingPlan | null and add null checks in all consumer functions.

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: 11

Caution

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

⚠️ Outside diff range comments (5)
src/lib/stores/billing.ts (1)

258-285: Guard billingIdToPlan results before accessing fields.

Both checkForUsageFees and checkForProjectLimitation dereference billingPlan without null checks.

🛠️ Proposed fix
 export function checkForUsageFees(plan: string, id: PlanServices) {
     const billingPlan = billingIdToPlan(plan);
+    if (!billingPlan) return false;
-    const supportsUsage = Object.keys(billingPlan.usage).length > 0;
+    const supportsUsage = Object.keys(billingPlan.usage ?? {}).length > 0;
     ...
 }

 export function checkForProjectLimitation(plan: string, id: PlanServices) {
     if (id === 'members') {
         const billingPlan = billingIdToPlan(plan);
+        if (!billingPlan) return false;
         const hasUnlimitedProjects = billingPlan.projects === 0;
src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/createPolicy.svelte (1)

210-235: Guard $project before reading teamId in upgrade navigation.

Accessing $project.teamId can throw if the project store is briefly undefined.

🛠️ Proposed fix
-                            goto(getChangePlanUrl($project.teamId));
+                            goto(getChangePlanUrl($project?.teamId));
...
-                                goto(getChangePlanUrl($project.teamId));
+                                goto(getChangePlanUrl($project?.teamId));
src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte (1)

73-78: Avoid potential $project null dereference in upgrade action.

Use optional chaining to keep the CTA safe during brief store gaps.

🛠️ Proposed fix
-                                        goto(getChangePlanUrl($project.teamId));
+                                        goto(getChangePlanUrl($project?.teamId));
src/routes/(console)/organization-[organization]/usage/[[invoice]]/+page.svelte (1)

59-115: Guard billingPlanDetails for self‑hosted instances.

billingPlanDetails is cloud‑only, so direct access can throw when the organization exists but billing details are absent. Gate it with isCloud and avoid calling plan helpers with null.

🛠️ Suggested fix
-    import { onMount } from 'svelte';
+    import { onMount } from 'svelte';
+    import { isCloud } from '$lib/system';
@@
-    const currentBillingPlan = $organization.billingPlanDetails;
+    let currentBillingPlan = null;
+    $: currentBillingPlan = isCloud ? $organization.billingPlanDetails : null;
-        {`#if` isStarterPlan(currentBillingPlan)}
+        {`#if` currentBillingPlan && isStarterPlan(currentBillingPlan)}
@@
-    {`#if` planHasGroup(currentBillingPlan, BillingPlanGroup.Scale)}
+    {`#if` currentBillingPlan && planHasGroup(currentBillingPlan, BillingPlanGroup.Scale)}
@@
-    {:else if planHasGroup(currentBillingPlan, BillingPlanGroup.Pro)}
+    {:else if currentBillingPlan && planHasGroup(currentBillingPlan, BillingPlanGroup.Pro)}
@@
-    {:else if isStarterPlan(currentBillingPlan)}
+    {:else if currentBillingPlan && isStarterPlan(currentBillingPlan)}
Based on learnings: billingPlanDetails is cloud-only in the Appwrite Console and should be guarded with isCloud or defensive access.
src/lib/layout/containerHeader.svelte (1)

64-87: Guard billingPlanDetails access outside cloud.

planName is computed unconditionally from billingPlanDetails, which can be missing on self‑hosted deployments. Gate it with isCloud (or provide a safe fallback) to avoid runtime errors.

🛠️ Suggested fix
-    $: planName = $organization?.billingPlanDetails.name;
+    $: planName = isCloud ? $organization.billingPlanDetails.name : 'Self-hosted';
Based on learnings: billingPlanDetails is cloud-only in the Appwrite Console and should be guarded with isCloud or defensive access.
🤖 Fix all issues with AI agents
In `@src/lib/components/billing/alerts/projectsLimit.svelte`:
- Line 41: The upgrade link currently calls getChangePlanUrl(organizationId)
even when organizationId may be undefined; change the element to only set href
when organizationId is available and otherwise disable the control — e.g., set
href to undefined or an empty string when organizationId is falsy and add a
disabled/aria-disabled state on the button/link so it cannot be clicked (use the
existing organizationId variable and getChangePlanUrl function to implement this
guard).

In `@src/lib/components/billing/emptyCardCloud.svelte`:
- Around line 10-39: The default rendering uses organizationId directly in the
upgrade link which can call getChangePlanUrl(null) when organizationId is
omitted; update the component so that when children is falsy you either require
organizationId or guard the Button/link: check organizationId before calling
getChangePlanUrl (e.g., render the Button only if organizationId is present or
compute the href conditionally), and ensure the on:click/ href use the guarded
value so getChangePlanUrl is never invoked with null; modify the logic around
children, organizationId, and the Button rendering to implement this guard.

In `@src/lib/components/bottomModalAlert.svelte`:
- Around line 142-151: In triggerWindowLink, guard calls to canUpgrade by
ensuring billingPlanDetails is defined before invoking it: replace direct calls
like canUpgrade($organization?.billingPlanDetails) with a check that
$organization?.billingPlanDetails exists (e.g., const hasBilling =
!!$organization?.billingPlanDetails; const shouldShowUpgrade = hasBilling &&
canUpgrade($organization.billingPlanDetails)) and apply the same pattern at the
other location that calls canUpgrade (both uses in
triggerWindowLink/currentModalAlert handling) so canUpgrade never receives
undefined.

In `@src/lib/components/cardContainer.svelte`:
- Around line 12-18: serviceId is currently initialized once from service and
won't update when the prop changes; make serviceId a reactive derived value from
service so planLimit recalculates. Replace the static initialization of
serviceId with a reactive assignment (using Svelte reactivity) that sets
serviceId from service (and keep planLimit as $: planLimit =
getServiceLimit(serviceId) || Infinity) so updates to service propagate through
serviceId to getServiceLimit; reference the service, serviceId, and planLimit
symbols when applying this change.

In `@src/lib/stores/billing.ts`:
- Around line 81-121: Guard against a null billing plan returned by
billingIdToPlan (via makeBillingPlan) in canUpgrade and canDowngrade: check that
billingPlan is truthy before accessing billingPlan.$id and return false (or
appropriate safe value) if lookup failed; similarly ensure planHasGroup handles
a null billingPlan by returning false. Update canUpgrade to call
makeBillingPlan, if billingPlan is null return false, otherwise
getNextTierBillingPlan(billingPlan.$id) and compare IDs; do the analogous guard
in canDowngrade using getPreviousTierBillingPlan, and adjust planHasGroup to
safely return false when makeBillingPlan returns null.

In `@src/routes/`(console)/organization-[organization]/+page.svelte:
- Line 222: The conditional rendering for the free-plan alert can throw if
data.currentPlan is undefined; update the condition that currently reads
"isCloud && data.currentPlan.projects !== 0 && projectsToArchive.length === 0 &&
!freePlanAlertDismissed" to guard access to currentPlan (e.g., use optional
chaining or an explicit existence check) such as "isCloud &&
data.currentPlan?.projects !== 0 && projectsToArchive.length === 0 &&
!freePlanAlertDismissed" or "isCloud && data.currentPlan &&
data.currentPlan.projects !== 0 && projectsToArchive.length === 0 &&
!freePlanAlertDismissed" so hydration won't throw when currentPlan is absent.

In `@src/routes/`(console)/organization-[organization]/billing/budgetAlert.svelte:
- Around line 81-84: Guard access to organization.billingPlanDetails in the
budgetAlert Svelte view to avoid crashes in self-hosted contexts by checking
isCloud and providing a fallback: when isCloud is true use
organization.billingPlanDetails.name, otherwise use currentPlan.name (or a safe
default); update the template expression around the bold text to reference this
guarded value and ensure organization.billingPlanDetails is not accessed
directly when isCloud is false.

In
`@src/routes/`(console)/project-[region]-[project]/auth/templates/emailSignature.svelte:
- Line 58: Guard access to $project before reading teamId: change the href call
that currently passes $project.teamId to use optional chaining so
getChangePlanUrl receives undefined when $project is missing (e.g. call
getChangePlanUrl($project?.teamId) or compute a local teamId = $project?.teamId
and pass that). Update the template reference where
href={getChangePlanUrl($project.teamId)} so it reads
href={getChangePlanUrl($project?.teamId)} and ensure getChangePlanUrl safely
handles an undefined teamId.

In
`@src/routes/`(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte:
- Line 34: The expression that computes organizationId uses
page.data?.organization.$id which will throw if organization is undefined;
update the lookup to use optional chaining (page.data?.organization?.$id) so the
access is safe during transitions while keeping the existing fallback
(page.data?.project?.teamId) and ensure any other reads of
page.data.organization in the same component follow the same pattern.

In
`@src/routes/`(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte:
- Around line 54-55: currentPlanId and currentBillingPlan are declared as plain
consts so they won't react to changes in data or $organization; change them to
reactive declarations using $: so they update automatically (e.g., replace the
consts for currentPlanId and currentBillingPlan with $: currentPlanId = ... and
$: currentBillingPlan = billingIdToPlan(currentPlanId)), ensuring you still
reference data?.currentInvoice?.plan and $organization?.billingPlanId and call
billingIdToPlan(currentPlanId) to derive the plan reactively.

In
`@src/routes/`(console)/project-[region]-[project]/sites/site-[site]/settings/updateResourceLimits.svelte:
- Around line 88-94: Replace the direct access to billingPlanId when computing
isStarter so missing organization billing data is safe: update the isStarter
computation (symbol isStarter and function isStarterPlan) to pass the
organization billingPlanId using optional chaining (i.e., guard the access to
billingPlanId on $organization so undefined is passed to isStarterPlan when
absent) so the check matches the defensive pattern used elsewhere.
🧹 Nitpick comments (2)
src/lib/elements/table/tableScroll.svelte (1)

15-15: Align Action generic and node parameter type (line 15).
The action is declared as Action<HTMLDivElement, unknown> but the parameter is typed as Element. In Svelte 5, the first generic parameter of Action specifies the expected node type, so the parameter should match: change node: Element to node: HTMLDivElement.

Suggested fix
-    const hasOverflow: Action<HTMLDivElement, unknown> = (node: Element) => {
+    const hasOverflow: Action<HTMLDivElement, unknown> = (node: HTMLDivElement) => {
src/lib/components/billing/emptyCardCloud.svelte (1)

2-2: Prefer $lib alias for shared component imports.

This component lives under src/lib, so use the $lib alias for consistency with repo conventions.

🔧 Suggested fix
-import { Card } from '..';
+import { Card } from '$lib/components';

As per coding guidelines: Import reusable modules from the src/lib directory using the $lib alias.

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

Caution

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

⚠️ Outside diff range comments (2)
src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/containerHeader.svelte (1)

72-79: Grammar issue: "policy" should be pluralized conditionally.

Line 73 uses singular "policy" regardless of maxPolicies value. When maxPolicies > 1, the text should say "policies".

Proposed fix
                         <slot name="tooltip">
                             <span>
-                                You are limited to {maxPolicies} policy on your plan.
+                                You are limited to {maxPolicies} {maxPolicies === 1 ? 'policy' : 'policies'} on your plan.
                                 <button
                                     class="u-underline"
                                     on:click={() => {
                                         showDropdown = !showDropdown;
                                         goto(getChangePlanUrl(project.teamId));
                                     }}>Upgrade your plan</button> to add customized backup policies.
                             </span>
                         </slot>
src/routes/(console)/project-[region]-[project]/auth/templates/emailSessionAlertTemplate.svelte (1)

47-54: Typo in template variable values.

Lines 52-53 have malformed template variables with missing closing braces:

  • value={'{{ipAddress'} → should be value={'{{ipAddress}}'}
  • value={'{{country'} → should be value={'{{country}}'}

This causes the copied value to be incomplete when users click to copy.

🐛 Proposed fix
             <Id value={'{{device}}'}>{'{{device}}'}</Id>
-            <Id value={'{{ipAddress'}>{'{{ipAddress}}'}</Id>
-            <Id value={'{{country'}>{'{{country}}'}</Id>
+            <Id value={'{{ipAddress}}'}>{'{{ipAddress}}'}</Id>
+            <Id value={'{{country}}'}>{'{{country}}'}</Id>
🤖 Fix all issues with AI agents
In `@src/routes/`(console)/project-[region]-[project]/settings/smtp/+page.svelte:
- Around line 45-60: The template accesses billing-related properties on
$currentPlan (e.g., currentPlan.customSmtp) without guarding for undefined which
can crash in self-hosted or loading states; update the derived/computed logic
(e.g., isButtonDisabled) and any places referencing currentPlan.customSmtp to
use optional chaining and safe defaults (for example currentPlan?.customSmtp ??
false) or guard with an isCloud check, and consider exposing a small derived
boolean like hasCustomSmtp for reuse in the template to keep checks consistent
and avoid runtime errors.
- Line 136: The getChangePlanUrl function currently declares a parameter named
organizationId but callers (e.g., href={getChangePlanUrl(project.teamId)} in
+page.svelte) pass project.teamId, causing a semantic mismatch; either rename
the function parameter organizationId to teamId in getChangePlanUrl (and update
its internal references) to reflect the actual value being passed, or, if
project.teamId truly represents an organization ID, update all callers to pass a
properly named organization ID and adjust the function's docs/comments; update
the signature in src/lib/stores/billing.ts (getChangePlanUrl) and every call
site (including the +page.svelte usage) to keep the parameter name consistent
and clear.
🧹 Nitpick comments (14)
src/routes/(console)/project-[region]-[project]/functions/function-[function]/+layout.svelte (1)

48-170: Consider extracting the repeated base URL pattern to reduce duplication.

The URL pattern ${base}/project-${page.params.region}-${page.params.project}/functions/function-${$func.$id} is repeated 10 times. Extracting it into a derived variable would improve maintainability and reduce the chance of inconsistencies.

♻️ Suggested refactor
+    $: functionBase = `${base}/project-${page.params.region}-${page.params.project}/functions/function-${$func.$id}`;
+
     $: $registerCommands([
         {
             label: 'Create deployment',
             async callback() {
                 if (!page.url.pathname.endsWith($func.$id)) {
                     await goto(
-                        `${base}/project-${page.params.region}-${page.params.project}/functions/function-${$func.$id}`
+                        functionBase
                     );
                 }
                 showCreateDeployment.set(true);
             },
             // ... and so on for other commands
         },

Then update subsequent usages:

  • functionBase for deployments
  • `${functionBase}/settings#permissions` for permissions
  • `${functionBase}/settings#events` for events
  • `${functionBase}/settings#variables` for variables
  • `${functionBase}/settings#timeout` for timeout
  • `${functionBase}/settings#schedule` for schedule
  • `${functionBase}/usage` for usage
  • `${functionBase}/executions` for executions
  • `${functionBase}/settings` for settings
src/routes/(console)/project-[region]-[project]/databases/+layout.svelte (1)

39-41: Consider consolidating effects.

This effect has no reactive dependencies in its body, meaning it will re-run on every state change. If the intent is to set ranks once when the layout mounts, this could be combined with the command registration effect above to reduce redundant executions.

♻️ Optional: Consolidate into single effect
     $effect(() => {
         $registerCommands([
             {
                 label: 'Go to usage',
                 callback: () => {
                     goto(withPath(databasesProjectRoute, 'usage'));
                 },
                 keys: ['g', 'u'],
                 disabled:
                     page.url.pathname.includes('usage') || page.url.pathname.includes('database-'),
                 group: 'databases'
             },
             {
                 label: 'Find databases',
                 callback: () => {
                     addSubPanel(DatabasesPanel);
                 },
                 group: 'databases',
                 rank: -1
             }
         ]);
+        $updateCommandGroupRanks({ databases: 200, navigation: 100 });
     });
-
-    $effect(() => {
-        $updateCommandGroupRanks({ databases: 200, navigation: 100 });
-    });
src/routes/(console)/project-[region]-[project]/auth/user-[user]/deleteUser.svelte (2)

25-25: Minor inconsistency: fallback pattern differs from template.

Line 25 uses a ternary (user.name ? user.name : 'User') while line 42 uses the OR operator (user.name || 'User'). Consider using the same pattern for consistency.

🔧 Suggested fix
-                message: `${user.name ? user.name : 'User'} has been deleted`
+                message: `${user.name || 'User'} has been deleted`

33-36: Consider adding type narrowing for the caught exception.

The caught exception e is of type unknown. Accessing e.message directly may cause TypeScript errors in strict mode.

🔧 Suggested fix
         } catch (e) {
-            error = e.message;
+            error = e instanceof Error ? e.message : 'An unknown error occurred';
             trackError(e, Submit.UserDelete);
         }
src/routes/(console)/project-[region]-[project]/auth/breadcrumbs.svelte (1)

7-20: Inconsistent Svelte version syntax: use $derived instead of $:.

The file imports page from $app/state (Svelte 5) but uses $: reactive statement (Svelte 4). For consistency with Svelte 5 patterns used elsewhere in this PR, consider using $derived.

♻️ Suggested refactor
-    $: breadcrumbs = [
+    const breadcrumbs = $derived([
         {
             href: `${base}/organization-${$organization?.$id}`,
             title: $organization?.name
         },
         {
             href: `${base}/project-${page.params.region}-${page.params.project}`,
             title: page.data?.project?.name
         },
         {
             href: `${base}/project-${page.params.region}-${page.params.project}/auth`,
             title: 'Auth'
         }
-    ];
+    ]);

As per coding guidelines: "Use Svelte 5 + SvelteKit 2 syntax with TypeScript for component development".

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

106-113: Consider migrating remaining URLs to use resolve() for consistency.

The Empty component's on:click handler (line 111-112) still uses template literal URL construction while the new webhooksCreateUrl uses resolve(). Same applies to line 66. Consider migrating these for consistency with the new pattern.

♻️ Suggested refactor
     {:else}
         <Empty
             single
             href="https://appwrite.io/docs/advanced/platform/webhooks"
             target="webhook"
-            on:click={() =>
-                goto(
-                    `${base}/project-${page.params.region}-${page.params.project}/settings/webhooks/create`
-                )} />
+            on:click={() => goto(webhooksCreateUrl)} />
     {/if}
src/routes/(console)/project-[region]-[project]/databases/database-[database]/backups/upgradeCard.svelte (1)

83-95: Deduplicate upgrade URL computation.

The same href logic is repeated for mobile and desktop buttons. Consider computing it once in the script and reusing it.

♻️ Proposed refactor
 const themeClass = isDark ? 'u-only-dark' : 'u-only-light';
 const mobileImg = isDark ? EmptyDarkMobile : EmptyLightMobile;
 const desktopImg = isDark ? EmptyDark : EmptyLight;
 const tabletImg = isDark ? EmptyDarkTablet : EmptyLightTablet;
+const upgradeHref = isCloud
+    ? getChangePlanUrl(project.teamId)
+    : 'https://cloud.appwrite.io/register';
-    <Button
-        external={!isCloud}
-        href={isCloud
-            ? getChangePlanUrl(project.teamId)
-            : 'https://cloud.appwrite.io/register'}>
+    <Button
+        external={!isCloud}
+        href={upgradeHref}>
         {isCloud ? 'Upgrade plan' : 'Sign up'}
     </Button>
-    <Button
-        external={!isCloud}
-        href={isCloud
-            ? getChangePlanUrl(project.teamId)
-            : 'https://cloud.appwrite.io/register'}>
+    <Button
+        external={!isCloud}
+        href={upgradeHref}>
         {isCloud ? 'Upgrade plan' : 'Sign up'}
     </Button>
src/routes/(console)/project-[region]-[project]/auth/security/updateSessionsLimit.svelte (1)

22-25: Minor inconsistency in optional chaining usage.

Line 23 accesses project.$id without optional chaining, while lines 18 and 58 use project?.authSessionsLimit. Since project is passed from the page's data.project, it should always be defined, so the optional chaining on lines 18/58 is defensive but unnecessary. Consider using consistent access patterns throughout.

src/routes/(console)/project-[region]-[project]/auth/security/updateSessionLength.svelte (1)

19-20: Inconsistent optional chaining between derived values and button disabled check.

Line 19 uses project?.authDuration but line 55 uses project.authDuration without optional chaining. Consider making this consistent - either guard all accesses or none (since the prop should always be defined when coming from page data).

src/routes/(console)/project-[region]-[project]/auth/security/updateMockNumbers.svelte (1)

27-30: Minor inconsistency in optional chaining.

Line 27 uses project?.authMockNumbers, line 28 uses project?.$id, but line 30 accesses project.authMockNumbers directly without optional chaining. Consider making this consistent to avoid potential runtime errors if project is unexpectedly undefined.

♻️ Suggested consistency fix
-    const initialNumbers = $derived(project.authMockNumbers?.map((n) => ({ ...n })) ?? []);
+    const initialNumbers = $derived(project?.authMockNumbers?.map((n) => ({ ...n })) ?? []);
src/routes/(console)/project-[region]-[project]/auth/settings/+page.svelte (2)

40-46: Consider adding error type guard.

The caught error is typed unknown, but error.message is accessed directly. This could fail if a non-Error value is thrown.

Suggested improvement
         } catch (error) {
             box.value = !box.value;
             addNotification({
                 type: 'error',
-                message: error.message
+                message: error instanceof Error ? error.message : 'An error occurred'
             });
             trackError(error, Submit.AuthStatusUpdate);
         }

18-19: Consider using $derived for explicit data dependency.

While the current pattern works correctly (SvelteKit re-renders the component when props change, reassigning the const), using $derived makes the dependency relationship more explicit and aligns with Svelte 5 best practices:

 let { data }: PageProps = $props();
-const { project } = data;
+const project = $derived(data.project);

This clarifies that project is a reactive derivation of data.project, which is especially useful since the $effect on line 50 depends on this value.

src/routes/(console)/project-[region]-[project]/auth/templates/localeOptions.svelte (1)

6-11: Type and optional chaining inconsistency.

The localeCodes prop is typed as Models.LocaleCode[] (non-optional), but line 8 uses optional chaining (localeCodes?.map). This is inconsistent:

  • If localeCodes is always provided, remove the optional chaining.
  • If it can be undefined, either add a default value (= []) or update the type to Models.LocaleCode[] | undefined.

Additionally, options is computed once at component initialization. If localeCodes changes after mount, options won't update. Consider using $derived for reactivity.

♻️ Suggested fix using $derived
 export let value = 'en';
 export let localeCodes: Models.LocaleCode[];

-const options = localeCodes?.map((code) => ({
+const options = $derived(localeCodes?.map((code) => ({
     label: code.name,
     value: code.code
-}));
+})) ?? []);
src/routes/(console)/project-[region]-[project]/settings/smtp/+page.svelte (1)

62-75: Avoid clearing SMTP fields on toggle; it drops state before submit.

Clearing values whenever enabled is false makes toggling back on in the same session require re-entering all fields. If the goal is to avoid sending SMTP data when disabled, gate the payload by enabled instead.

♻️ Suggested refactor
     async function updateSmtp() {
         try {
             await sdk.forConsole.projects.updateSMTP({
                 projectId: project.$id,
                 enabled,
-                senderName: senderName || undefined,
-                senderEmail: senderEmail || undefined,
-                replyTo: replyTo || undefined,
-                host: host || undefined,
-                port: port || undefined,
-                username: username || undefined,
-                password: password || undefined,
-                secure: secure ? (secure as SMTPSecure) : undefined
+                senderName: enabled ? senderName || undefined : undefined,
+                senderEmail: enabled ? senderEmail || undefined : undefined,
+                replyTo: enabled ? replyTo || undefined : undefined,
+                host: enabled ? host || undefined : undefined,
+                port: enabled ? port || undefined : undefined,
+                username: enabled ? username || undefined : undefined,
+                password: enabled ? password || undefined : undefined,
+                secure: enabled && secure ? (secure as SMTPSecure) : undefined
             });
-    $effect(() => {
-        if (!enabled) {
-            senderName = undefined;
-            senderEmail = undefined;
-            replyTo = undefined;
-            host = undefined;
-            port = undefined;
-            username = undefined;
-            password = undefined;
-            secure = undefined;
-        }
-    });

Also applies to: 104-115

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

🤖 Fix all issues with AI agents
In `@src/routes/`(console)/project-[region]-[project]/functions/+page.svelte:
- Around line 51-64: The code uses templateConfig directly in the query which
can be null or contain unsafe characters; update the navigation logic around
template/templateConfig: read templateConfig from
page.url.searchParams.get('templateConfig'), and when building the URL for
goto(resolveRoute(...)) use encodeURIComponent(templateConfig) and only append
`?templateConfig=...` when templateConfig is truthy (non-null/non-empty); adjust
the call that creates templateUrl/withPath so it passes the encoded query string
conditionally and leave goto(createFunctionsUrl) unchanged.
- Around line 36-43: createFunctionsUrl is initialized once and becomes stale
when navigating between projects; make it reactive by deriving it from the page
params. Replace the constant init using resolveRoute with a derived store that
depends on page (or page.params) — e.g., use the existing $derived helper or
svelte's derived(page, p => ...) to compute
resolveRoute('/(console)/project-[region]-[project]/functions/create-function',
{ ...p.params }) so createFunctionsUrl updates whenever page.params changes.
🧹 Nitpick comments (2)
src/routes/(console)/project-[region]-[project]/overview/platforms/action.svelte (1)

36-42: Remove redundant $canWritePlatforms check.

The {#if $canWritePlatforms} guard on line 37 is redundant since this entire block is already inside the {#if $canWritePlatforms} check from line 21.

Suggested fix
     {:else}
         <Popover let:toggle padding="none" placement="bottom-end">
-            {`#if` $canWritePlatforms}
-                <Button on:click={toggle}>
-                    <Icon icon={IconPlus} slot="start" />
-                    <span class="text">Add platform</span>
-                </Button>
-            {/if}
+            <Button on:click={toggle}>
+                <Icon icon={IconPlus} slot="start" />
+                <span class="text">Add platform</span>
+            </Button>
             <div style:min-width="232px" slot="tooltip">
src/routes/(console)/project-[region]-[project]/updateVariables.svelte (1)

234-240: Inconsistent route resolution pattern.

This goto call still uses string interpolation while lines 278-283 and 333-338 now use resolveRoute. Consider aligning for consistency.

♻️ Suggested fix
                 buttons: [
                     {
                         method: () =>
                             goto(
-                                `${base}/project-${page.params.region}-${page.params.project}/settings`
+                                resolveRoute('/(console)/project-[region]-[project]/settings', {
+                                    ...page.params
+                                })
                             ),
                         name: 'Go to settings'
                     }
                 ]

@ItzNotABug ItzNotABug merged commit fee67a1 into main Jan 28, 2026
4 checks passed
@ItzNotABug ItzNotABug deleted the billing-sdk-refactor branch January 28, 2026 12:41
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.

4 participants