-
Notifications
You must be signed in to change notification settings - Fork 208
Billing SDK Refactor #2722
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
base: main
Are you sure you want to change the base?
Billing SDK Refactor #2722
Conversation
replace: `billing.listOrganization` calls with `organizations.list`.
…tionDeleteOrganization`, `getOrganizationPlan` and `getRoles`.
Console (appwrite/console)Project ID: Tip Storage files get ClamAV malware scanning and encryption by default |
WalkthroughThis pull request performs a comprehensive architectural refactoring of the billing and organization management system. The changes migrate from custom billing SDK types to Appwrite Console's centralized Models namespace, consolidate API calls from the billing namespace to organizations/account/console namespaces, and introduce new billing helper functions for plan management. The refactoring includes removing deprecated types from the billing SDK, updating component prop types, restructuring API payloads across 100+ files, and introducing cloud-aware organization fetching via a new Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
…od and its usages.
…hods and its usages.
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: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/routes/(console)/organization-[organization]/billing/availableCredit.svelte (1)
36-51: Add error handling to the API request.The
request()function lacks error handling. If the API call fails, the UI remains in an inconsistent state with no feedback to the user. This is inconsistent with similar components in the billing directory, which all wrap SDK calls in try-catch blocks.🔎 Proposed fix to add error handling
async function request() { if (!$organization?.$id) return; // fast path return! if (!areCreditsSupported) return; /** * The initial `creditList` from `+page.ts` can include up to 25 items by default. * * Technically, we could reuse that for offsets < 25 (i.e., the first 5 pages with limit = 5) * to avoid an extra request. But for now, we always fetch fresh data. */ - creditList = await sdk.forConsole.organizations.listCredits({ - organizationId: $organization.$id, - queries: [Query.limit(limit), Query.offset(offset)] - }); + try { + creditList = await sdk.forConsole.organizations.listCredits({ + organizationId: $organization.$id, + queries: [Query.limit(limit), Query.offset(offset)] + }); + } catch (error) { + console.error('Failed to load credits:', error); + // Consider showing user feedback (toast/alert) here + return; + } creditList = {src/lib/components/billing/selectPaymentMethod.svelte (1)
25-29: Avoid manually constructing SDK model objects.Manually spreading and reconstructing
Models.PaymentMethodListviolates the principle that SDK models should be treated as immutable data from the API. This pattern can break if the SDK type has readonly properties, class methods, or internal state.Consider refetching the payment methods list after adding a new card, or check if the SDK provides a method to update the list. The existing invalidate calls on lines 32-35 suggest this data will be refreshed anyway.
💡 Suggested refactor
Since you're already invalidating dependencies, consider removing the manual update and relying on the data refetch:
async function cardSaved(card: Models.PaymentMethod) { value = card.$id; - if (value) { - methods = { - ...methods, - total: methods.total + 1, - paymentMethods: [...methods.paymentMethods, card] - }; - } - await Promise.all([ invalidate(Dependencies.UPGRADE_PLAN), invalidate(Dependencies.ORGANIZATION) ]); }If immediate UI update is required before refetch, verify that the SDK models support this mutation pattern or use a local state approach that doesn't mutate the SDK model.
src/lib/stores/billing.ts (1)
534-552: Useorganizations.getPaymentMethodinstead ofaccount.getPaymentMethod.
checkForMandateretrieves an organization's payment method but usessdk.forConsole.account.getPaymentMethodwithout passing the organization ID. This differs frompaymentExpired(line 470) which usessdk.forConsole.organizations.getPaymentMethodwithorganizationId. For consistency and to ensure proper organization-level access control, use the same endpoint pattern aspaymentExpired:const paymentMethod = await sdk.forConsole.organizations.getPaymentMethod({ organizationId: org.$id, paymentMethodId: paymentId });src/routes/(console)/create-organization/+page.ts (1)
45-57: Missingawaitin getCoupon causes the function to return a Promise instead of resolving it.The
try/catchblock won't catch any errors from the SDK call because the promise is returned directly without awaiting. This means errors will propagate as unhandled promise rejections rather than being caught and returningnull.🔎 Proposed fix
async function getCoupon(url: URL): Promise<Models.Coupon | null> { if (url.searchParams.has('code')) { const coupon = url.searchParams.get('code'); try { - return sdk.forConsole.account.getCoupon({ + return await sdk.forConsole.account.getCoupon({ couponId: coupon }); } catch (e) { return null; } } return null; }src/routes/(console)/organization-[organization]/billing/wizard/addCredit.svelte (1)
26-31: Incorrect error wrapping loses error details.
throw new Error(error)will callerror.toString()which typically results in[object Object]. This loses the original error's stack trace and structured information.🔎 Proposed fix
} catch (error) { couponData.code = coupon; couponData.status = 'error'; $wizard.interceptorNotificationEnabled = false; - throw new Error(error); + throw error; }src/routes/(public)/sites/deploy/+page.ts (1)
72-77: Inconsistent API call syntax - uses positional arguments instead of object syntax.This file uses positional arguments for
organizations.create:await sdk.forConsole.organizations.create( ID.unique(), 'Personal Projects', BillingPlan.FREE, null );Other files in this PR (e.g.,
src/routes/(public)/template-[template]/+page.tslines 43-47 andsrc/routes/(public)/sites/deploy/+page.tslines 88-92) use object syntax:await sdk.forConsole.organizations.create({ organizationId: ID.unique(), name: 'Personal Projects', billingPlan: BillingPlan.FREE });This positional syntax may be incorrect or deprecated. Please verify and align with the object-based syntax used elsewhere.
🔎 Suggested fix using object syntax
if (isCloud) { - await sdk.forConsole.organizations.create( - ID.unique(), - 'Personal Projects', - BillingPlan.FREE, - null - ); + await sdk.forConsole.organizations.create({ + organizationId: ID.unique(), + name: 'Personal Projects', + billingPlan: BillingPlan.FREE + }); } else {src/routes/(public)/functions/deploy/+page.ts (1)
67-85: Same API syntax inconsistency as sites/deploy.This file has the identical issue with positional arguments for
organizations.create(lines 72-77) that was flagged insrc/routes/(public)/sites/deploy/+page.ts. Please apply the same fix to use object syntax.🔎 Suggested fix using object syntax
if (isCloud) { - await sdk.forConsole.organizations.create( - ID.unique(), - 'Personal Projects', - BillingPlan.FREE, - null - ); + await sdk.forConsole.organizations.create({ + organizationId: ID.unique(), + name: 'Personal Projects', + billingPlan: BillingPlan.FREE + }); } else {
🧹 Nitpick comments (19)
src/lib/flags.ts (1)
8-20: Consider removing unused code or clarifying its purpose.The
isFlagEnabledfunction is marked as unused and never called or exported. The file also exports an emptyflagsobject (line 22), suggesting incomplete or placeholder implementation. Consider either:
- Removing the unused function to reduce clutter
- Adding a comment explaining why this code is being retained for future use
src/routes/(console)/organization-[organization]/billing/taxId.svelte (1)
20-23: LGTM! SDK refactor is correct.The migration from
sdk.forConsole.billing.updateTaxIdtosdk.forConsole.organizations.setBillingTaxIdwith named parameters is well-executed and aligns with the PR objectives.Optional: Consider adding null-safety for consistency.
Lines 15 and 49 use optional chaining on
$organization, but line 21 directly accesses$organization.$id. While the route likely ensures the organization is loaded, adding optional chaining or an early return would improve consistency and defensive coding:🔎 Optional refactor for null-safety
async function updateTaxId() { + if (!$organization) return; + try { await sdk.forConsole.organizations.setBillingTaxId({ organizationId: $organization.$id, taxId });src/routes/(public)/(guest)/login/+page.ts (1)
8-27: API migration looks good; consider explicit typing for consistency.The migration to
console.getCouponandconsole.getCampaignwith object-based parameters is correct and matches the patterns inregister/+page.tsandapply-credit/+page.ts.For consistency with similar files in this refactor (e.g.,
register/+page.tslines 7-8,apply-credit/+page.tslines 6-7), consider explicitly typingcouponDataasModels.Coupon.🔎 Optional: Add explicit type for consistency
if (url.searchParams.has('code')) { const code = url.searchParams.get('code'); let campaign: Models.Campaign; + let couponData: Models.Coupon; try { - const couponData = await sdk.forConsole.console.getCoupon({ + couponData = await sdk.forConsole.console.getCoupon({ couponId: code });src/lib/components/billing/alerts/paymentMandate.svelte (1)
10-15: Consider adding error handling for payment operations.Payment verification failures should provide clear feedback to users. Wrapping the async operations in try-catch would improve the user experience.
🔎 Suggested error handling
async function verifyPaymentMethod() { + try { const method = await sdk.forConsole.account.updatePaymentMethodMandateOptions({ paymentMethodId: $paymentMissingMandate.$id }); await confirmSetup(method.clientSecret, method.providerMethodId); + } catch (error) { + // Handle error appropriately (e.g., show toast notification) + console.error('Payment verification failed:', error); + } }src/lib/components/billing/alerts/selectProjectCloud.svelte (1)
45-47: Consider typing the caught error for type safety.The caught exception
eis not typed, and accessinge.messagedirectly is not type-safe. Consider typing it explicitly or using a type guard.🔎 Suggested improvement
-} catch (e) { - error = e.message; +} catch (e) { + error = e instanceof Error ? e.message : String(e); }src/routes/(console)/account/payments/editPaymentModal.svelte (1)
30-32: Consider awaitinginvalidatebefore showing the notification.The
invalidate(Dependencies.PAYMENT_METHODS)call is not awaited. If the user performs another action immediately after closing the modal, the payment methods list might not yet be refreshed, leading to stale data being displayed.🔎 Proposed fix
trackEvent(Submit.PaymentMethodUpdate); - invalidate(Dependencies.PAYMENT_METHODS); + await invalidate(Dependencies.PAYMENT_METHODS); show = false;src/lib/components/billing/emptyCardCloud.svelte (1)
20-20:proPlanNameis not reactive to store changes.
proPlanNameis assigned once when the component initializes. IfplansInfostore updates after mount (e.g., due to data refresh), this value won't update. Consider using$derivedif reactivity is needed:🔎 Proposed fix using $derived
- const proPlanName = getBasePlanFromGroup(BillingPlanGroup.Pro).name; + const proPlanName = $derived(getBasePlanFromGroup(BillingPlanGroup.Pro).name);However, if plan names are stable after initial load, the current implementation is acceptable.
src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (1)
18-37: Inconsistentawaitusage forinvalidate().In
removeDefaultMethod(line 30),invalidate()is called withoutawait, while inremoveBackupMethod(line 53), it's awaited. This inconsistency could lead to different behaviors—the backup method waits for cache invalidation before proceeding, while the default method does not.🔎 Proposed fix for consistency
trackEvent(Submit.OrganizationPaymentDelete); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.ORGANIZATION); showDelete = false;Also applies to: 40-59
src/routes/(console)/organization-[organization]/domains/domain-[domain]/settings/+page.ts (1)
8-16: Add type annotation for the load function.The
loadfunction is missing itsPageLoadtype annotation which other route loaders in the codebase use for type safety.Suggested fix
+import type { PageLoad } from './$types'; -export const load = async ({ params, parent, depends }) => { +export const load: PageLoad = async ({ params, parent, depends }) => {src/routes/(console)/create-organization/+page.ts (1)
35-43: Simplify redundant conditional check.The
if (plan)check is redundant sinceurl.searchParams.get('plan')returnsnullif the param doesn't exist, and the outerifalready confirms the param exists.🔎 Proposed simplification
-function getPlanFromUrl(url: URL): string | null { - if (url.searchParams.has('plan')) { - const plan = url.searchParams.get('plan'); - if (plan) { - return plan; - } - } - return BillingPlan.FREE; -} +function getPlanFromUrl(url: URL): string { + return url.searchParams.get('plan') ?? BillingPlan.FREE; +}src/routes/+page.ts (1)
37-49: Inconsistent path construction betweenresolve()andbase.Line 39 uses
resolve('/')while lines 43-48 use${base}/.... This inconsistency could lead to different behavior depending on how the app is deployed.Consider standardizing on one approach throughout the function for maintainability:
if (userVisitedEducationPage()) { await handleGithubEducationMembership(account.name, account.email); redirect(303, resolve('/')); } else if (organizations.total && !isApplyingCredit) { const teamId = account.prefs.organization ?? organizations.teams[0].$id; if (!teamId) { - redirect(303, `${base}/account/organizations${url.search}`); + redirect(303, `${resolve('/(console)/account/organizations')}${url.search}`); } else { - redirect(303, `${base}/organization-${teamId}${url.search}`); + redirect(303, `${resolve(`/(console)/organization-${teamId}`)}${url.search}`); } } else if (!isApplyingCredit) { - redirect(303, `${base}/onboarding/create-project${url.search}`); + redirect(303, `${resolve('/(console)/onboarding/create-project')}${url.search}`); }src/routes/(console)/account/organizations/+page.svelte (2)
40-47: Stale comment references old function name.The comment on line 41 says "use tierToPlan" but the code now uses
billingIdToPlan.🔎 Proposed fix
- // For known plans, use tierToPlan + // For known plans, use billingIdToPlan const tierData = billingIdToPlan(billingPlan);
76-84: Union typeModels.Preferences | Models.Organizationseems overly broad.The function accepts
Models.Preferencesbut then passes the value toisCloudOrgandisOrganizationOnTrialwhich expectModels.Organization. This works due to the type guard, but the signature is misleading since the function only returns a meaningful result forModels.Organization.Consider narrowing the parameter type or documenting why
Preferencesis accepted:function isPayingOrganization( - team: Models.Preferences | Models.Organization + team: Models.Organization ): Models.Organization | null {src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (1)
88-104: Missing error handling in reactive payment method fetches.The reactive blocks that fetch payment methods don't handle errors. If the API calls fail, the promises are silently rejected which could leave the UI in an inconsistent state.
🔎 Proposed fix
$: if (organization?.backupPaymentMethodId) { sdk.forConsole.organizations .getPaymentMethod({ organizationId: organization.$id, paymentMethodId: organization.backupPaymentMethodId }) - .then((res) => (backupPaymentMethod = res)); + .then((res) => (backupPaymentMethod = res)) + .catch(() => (backupPaymentMethod = null)); } $: if (organization?.paymentMethodId) { sdk.forConsole.organizations .getPaymentMethod({ organizationId: organization.$id, paymentMethodId: organization.paymentMethodId }) - .then((res) => (defaultPaymentMethod = res)); + .then((res) => (defaultPaymentMethod = res)) + .catch(() => (defaultPaymentMethod = null)); }src/routes/(console)/organization-[organization]/change-plan/+page.ts (1)
31-36: Address the TODO or remove it.The TODO comment questions the current implementation logic. If there's a known simplification, consider implementing it; otherwise, add context or remove the comment.
src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte (1)
127-134: Inconsistentinvalidateawait pattern.Line 97 awaits
invalidate(Dependencies.PAYMENT_METHODS), but lines 133-134 do not await the invalidation calls. Consider awaiting these for consistency, or if intentional fire-and-forget, add a brief comment explaining why.🔎 Suggested fix for consistent await pattern
- invalidate(Dependencies.ORGANIZATION); - invalidate(Dependencies.INVOICES); + await invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.INVOICES);src/lib/stores/stripe.ts (1)
88-94: Hardcoded base URL should use environment configuration.The URL
https://cloud.appwrite.io/consoleis hardcoded here and again at line 226. Consider using an environment variable or the existingbaseimport from$app/pathscombined withwindow.location.originfor consistency and flexibility across environments.🔎 Suggested approach
- const baseUrl = 'https://cloud.appwrite.io/console'; + const baseUrl = `${window.location.origin}${base}`; const accountUrl = `${baseUrl}/account/payments?clientSecret=${clientSecret}`; const orgUrl = `${baseUrl}/organization-${organizationId}/billing?clientSecret=${clientSecret}`;Apply the same pattern at line 226.
src/lib/components/billing/estimatedTotalBox.svelte (1)
27-31: Consider consistent couponId null handling.The two estimation functions handle empty couponId differently:
getEstimate:couponId === '' ? null : couponIdgetUpdatePlanEstimate:couponId && couponId.length > 0 ? couponId : nullBoth approaches work, but the first could pass
undefinedas-is while the second explicitly converts tonull. Consider unifying the logic for maintainability.🔎 Suggested unified approach
async function getEstimate( billingPlan: string, collaborators: string[], couponId: string | undefined ) { try { estimation = await sdk.forConsole.organizations.estimationCreateOrganization({ billingPlan, invites: collaborators ?? [], - couponId: couponId === '' ? null : couponId + couponId: couponId?.length ? couponId : null });Also applies to: 57-62
src/routes/(console)/onboarding/create-project/+page.ts (1)
64-64: Consider using union type for nullable assignment.The variable is typed as
Models.ProjectListbut initialized tonull. While TypeScript non-strict mode tolerates this, a more accurate type would beModels.ProjectList | null.🔎 Proposed fix
-let projects: Models.ProjectList = null; +let projects: Models.ProjectList | null = null;
src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte
Show resolved
Hide resolved
src/routes/(console)/organization-[organization]/settings/deleteOrganizationModal.svelte
Show resolved
Hide resolved
src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts
Show resolved
Hide resolved
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: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (1)
68-86: Add missing success analytics event for backup payment method.The
addBackupPaymentMethodfunction lacks thetrackEvent(Submit.OrganizationPaymentUpdate)call on success, while the identicaladdPaymentMethodfunction includes it on line 57. AddtrackEvent(Submit.OrganizationPaymentUpdate);after theinvalidatecall on line 78 for consistency.src/routes/(console)/create-organization/+page.svelte (1)
80-85: Potential null dereference when extracting URL parameters.Both
organizationIdandinvitesparameters are extracted without null checks. If a user navigates to?type=payment_confirmed&id=xxxwithout theinvitesparam, calling.split(',')onnullwill throw a TypeError.🛠️ Suggested fix with null guards
if (type === 'payment_confirmed') { const organizationId = page.url.searchParams.get('id'); - const invites = page.url.searchParams.get('invites').split(','); - await validate(organizationId, invites); + const invitesParam = page.url.searchParams.get('invites'); + if (organizationId && invitesParam) { + const invites = invitesParam.split(',').filter(Boolean); + await validate(organizationId, invites); + } }src/routes/(console)/account/payments/editPaymentModal.svelte (1)
37-51: Critical: DuplicatetrackEventandinvalidatecalls will cause double tracking and unnecessary cache invalidation.Lines 43-44 and 46-47 contain duplicated code that will:
- Track the
PaymentMethodUpdateanalytics event twice per submission- Call
invalidate(Dependencies.PAYMENT_METHODS)twice (once sync, once awaited)This appears to be a merge conflict artifact or copy-paste error.
🐛 Proposed fix
async function handleSubmit() { try { await sdk.forConsole.account.updatePaymentMethod({ paymentMethodId: selectedPaymentMethod.$id, expiryMonth: parseInt(month), expiryYear: year }); - trackEvent(Submit.PaymentMethodUpdate); - invalidate(Dependencies.PAYMENT_METHODS); - show = false; trackEvent(Submit.PaymentMethodUpdate); await invalidate(Dependencies.PAYMENT_METHODS); + show = false; addNotification({ type: 'success', message: 'Your payment method has been updated' }); } catch (e) { error = e.message; trackError(e, Submit.PaymentMethodUpdate); } }src/routes/(public)/sites/deploy/+page.ts (1)
100-105: Silent failure may leave user without feedback.If organization creation fails, the error is only logged to console. The user receives no notification, and
organizationsremains empty, which could cause confusion or downstream issues in the UI.Consider propagating the error or showing a user-facing notification.
src/routes/(public)/functions/deploy/+page.ts (1)
67-88: SamegetBasePlanFromGroupundefined risk and code duplication.This organization creation logic is nearly identical to
src/routes/(public)/sites/deploy/+page.ts(lines 83-105). Both have the same potentialTypeErrorifgetBasePlanFromGroupreturnsundefined.Consider extracting this shared logic into a helper function (e.g.,
ensureOrganizationExists()in$lib/stores/organization) to reduce duplication and centralize error handling.src/routes/(console)/organization-[organization]/billing/replaceCard.svelte (1)
97-117: Error handling doesn't propagate to caller.Both
addPaymentMethodandaddBackupPaymentMethodcatch errors and set theerrorstate but don't rethrow. InhandleSubmit(line 79-81), these functions are awaited but errors won't propagate to the outer try-catch, potentially allowing the success notification andshow = falseto execute even after a failure.🐛 Suggested fix
async function addPaymentMethod(paymentMethodId: string) { - try { - await sdk.forConsole.organizations.setDefaultPaymentMethod({ - organizationId: organization.$id, - paymentMethodId - }); - } catch (err) { - error = err.message; - } + await sdk.forConsole.organizations.setDefaultPaymentMethod({ + organizationId: organization.$id, + paymentMethodId + }); } async function addBackupPaymentMethod(paymentMethodId: string) { - try { - await sdk.forConsole.organizations.setBackupPaymentMethod({ - organizationId: organization.$id, - paymentMethodId - }); - } catch (err) { - error = err.message; - } + await sdk.forConsole.organizations.setBackupPaymentMethod({ + organizationId: organization.$id, + paymentMethodId + }); }
🤖 Fix all issues with AI agents
In @src/lib/stores/billing.ts:
- Around line 570-572: The call to
getBasePlanFromGroup(BillingPlanGroup.Starter).$id may be undefined; first call
getBasePlanFromGroup(BillingPlanGroup.Starter) into a local variable (e.g.,
basePlan) and check for null/undefined before using .$id, then build the queries
array conditionally so you only include Query.notEqual('billingPlan',
basePlan.$id) when basePlan and basePlan.$id exist (otherwise omit that filter
or use a safe default), and pass the resulting queries to
sdk.forConsole.organizations.list to avoid accessing .$id on undefined.
- Around line 532-540: In checkForMissingPaymentMethod, avoid directly accessing
getBasePlanFromGroup(BillingPlanGroup.Starter).$id because getBasePlanFromGroup
can return undefined; first assign const basePlan =
getBasePlanFromGroup(BillingPlanGroup.Starter) and if basePlan is undefined
either return/exit early or throw/log an error, then use basePlan.$id in the
Query.equal call passed to sdk.forConsole.organizations.list; alternatively
guard using optional chaining and a fallback value before building the query so
you never dereference undefined.
- Around line 88-100: getBasePlanFromGroup can return undefined when proPlans is
empty; change its signature to Models.BillingPlan | undefined and explicitly
handle the empty case instead of assuming [0] is defined. In
getBasePlanFromGroup, compute const base =
proPlans.sort((a,b)=>a.order-b.order)[0]; then return base ?? undefined (or
return a sensible fallback plan found from plansInfoStore if intended), and
update callers to handle a possible undefined result; reference: function
getBasePlanFromGroup, variable proPlans, plansInfoStore, and
BillingPlanGroup.Starter.
- Around line 102-111: The function billingIdToPlan silently falls back to
BillingPlanGroup.Pro when billingId is missing from plansInfo; instead make
failures explicit by validating plansInfo.has(billingId) and throwing a
descriptive error (or returning a Result/optional) rather than defaulting to
Pro. Update billingIdToPlan to check plansInfo (via get(plansInfo)) and if the
id is absent throw an Error like "Unknown billingId: <id>" (or change the
signature to return undefined/null or a Result type), and remove the silent
fallback to getBasePlanFromGroup(BillingPlanGroup.Pro) to avoid incorrect
billing/UI behavior.
In @src/routes/(console)/create-organization/+page.svelte:
- Around line 90-97: The validatePayment branch in the validate() flow currently
preloads and navigates to `${base}/console/organization-${org.$id}` which is
inconsistent with the create() flow; update the two references in the
validatePayment success path (the calls around
sdk.forConsole.organizations.validatePayment and the subsequent preloadData and
goto) to use `${base}/organization-${org.$id}` instead so both validate() and
create() navigate to the same canonical `/organization-{id}` URL.
In @src/routes/(console)/organization-[organization]/billing/+page.ts:
- Around line 15-20: The redirect uses a 301 permanent status in the
redirect(...) call which can be cached by browsers; change the status code from
301 to a temporary redirect such as 302 (or 307 if preserving method) in the
redirect(resolve(...)) invocation so authorization-based redirects aren’t
cached—update the redirect(301, resolve(...)) call to redirect(302,
resolve(...)) (or 307) where the redirect(...) and resolve(...) symbols are
used.
In
@src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte:
- Around line 36-39: The exported props backupMethod and primaryMethod are
declared as non-optional but are used with optional chaining (e.g.,
primaryMethod?.lastError, primaryMethod?.userId), so update their types to be
optional/nullable (e.g., Models.PaymentMethod | undefined) in the export
declarations (export let backupMethod and export let primaryMethod) to match
runtime usage and avoid type errors; ensure any other references to those
symbols (backupMethod, primaryMethod) continue to use safe checks or optional
chaining.
In @src/routes/(public)/sites/deploy/+page.ts:
- Around line 88-92: The create call uses
getBasePlanFromGroup(BillingPlanGroup.Starter).$id which can be undefined in the
(public) route because plansInfo is only populated in the (console) layout;
update the organization creation to guard for undefined by calling
getBasePlanFromGroup(BillingPlanGroup.Starter), check its return value (from the
getBasePlanFromGroup helper) and use a safe fallback billing plan id (e.g., a
DEFAULT_PLAN_ID constant or a known starter plan id) or fetch/populate plansInfo
before calling; ensure you reference getBasePlanFromGroup and the organization
creation block (await sdk.forConsole.organizations.create) when adding the
null-check/fallback and logging an informative error if neither a plan nor
fallback exist.
🧹 Nitpick comments (6)
src/routes/(console)/account/payments/editAddressModal.svelte (1)
36-46: API migration looks correct.The transition from
billing.updateAddresstoaccount.updateBillingAddresswith named parameters is properly implemented.Minor optional simplification for the ternary expressions:
✨ Optional simplification
await sdk.forConsole.account.updateBillingAddress({ billingAddressId: selectedAddress.$id, country: selectedAddress.country, streetAddress: selectedAddress.streetAddress, city: selectedAddress.city, state: selectedAddress.state, - postalCode: selectedAddress.postalCode ? selectedAddress.postalCode : undefined, - addressLine2: selectedAddress.addressLine2 - ? selectedAddress.addressLine2 - : undefined + postalCode: selectedAddress.postalCode || undefined, + addressLine2: selectedAddress.addressLine2 || undefined });src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (1)
40-59: Optional: Inconsistent dialog dismissal timing between the two methods.Pre-existing behavior:
removeBackupMethodsetsshowDelete = falseat line 42 before the async operation completes, causing the dialog to close immediately. In contrast,removeDefaultMethoduses afinallyblock to close the dialog after the operation.Consider aligning the two methods for consistent UX where the dialog remains open until the operation completes or fails.
♻️ Suggested alignment
async function removeBackupMethod() { if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return; - showDelete = false; try { await sdk.forConsole.organizations.deleteBackupPaymentMethod({ organizationId: $organization.$id }); addNotification({ type: 'success', message: `The payment method has been removed from ${$organization.name}` }); trackEvent(Submit.OrganizationBackupPaymentDelete); await invalidate(Dependencies.PAYMENT_METHODS); - showDelete = false; } catch (e) { error = e.message; trackError(e, Submit.OrganizationBackupPaymentDelete); + } finally { + showDelete = false; } }src/lib/components/support.svelte (1)
22-29: Type assertion is acceptable but consider adding a type guard for safety.The
(team as Models.Organization)assertions work because the runtime objects contain billing details when in cloud mode. However, this relies on implicit knowledge thatorganizationList.teamscontainsOrganizationobjects rather than plainTeamobjects.Consider extracting a type guard or helper function if this pattern is used elsewhere:
💡 Optional improvement
// In a shared helper file function hasOrganizationBilling(team: Models.Team): team is Models.Organization { return 'billingPlanDetails' in team; }src/routes/(console)/onboarding/create-organization/+page.svelte (1)
24-34: Inconsistent null safety between creation and analytics.Line 27 accesses
getBasePlanFromGroup(BillingPlanGroup.Starter).$idwithout null safety, while line 31 uses optional chaining (?.name). If the function can returnundefined(warranting the?.on line 31), the same protection should be applied on line 27.♻️ Suggested fix for consistency
organization = await sdk.forConsole.organizations.create({ organizationId: ID.unique(), name: organizationName, - billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id + billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter)?.$id });src/routes/(console)/organization-[organization]/billing/+page.ts (1)
31-39: Repeated type casts suggest incomplete type propagation.Multiple lines cast
organizationtoModels.Organization(lines 31, 53, 65). Iforganizationfromparent()is already typed correctly, these casts are unnecessary. If not, consider fixing the type at the source (in the parent layout's return type).Also applies to: 46-56, 63-66
src/lib/stores/billing.ts (1)
113-141: TODOs indicate incomplete refactoring.Lines 113 and 128 contain TODOs suggesting these functions should return
BillingPlanobjects instead of strings. Consider addressing these before merging or creating tracking issues.Would you like me to help refactor these functions to return
Models.BillingPlanobjects, or open an issue to track this work?
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
src/lib/components/archiveProject.sveltesrc/lib/components/billing/alerts/paymentMandate.sveltesrc/lib/components/billing/alerts/selectProjectCloud.sveltesrc/lib/components/support.sveltesrc/lib/stores/billing.tssrc/lib/stores/sdk.tssrc/routes/(console)/+layout.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/routes/(console)/create-organization/+page.sveltesrc/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/organization-[organization]/+layout.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/organization-[organization]/billing/replaceAddress.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/routes/(public)/sites/deploy/+page.ts
💤 Files with no reviewable changes (1)
- src/lib/stores/sdk.ts
✅ Files skipped from review due to trivial changes (1)
- src/routes/(console)/organization-[organization]/+layout.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/routes/(console)/organization-[organization]/billing/replaceAddress.svelte
- src/lib/components/archiveProject.svelte
- src/lib/components/billing/alerts/selectProjectCloud.svelte
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx,svelte}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx,js,jsx,svelte}: Import reusable modules from the src/lib directory using the $lib alias
Use minimal comments in code; reserve comments for TODOs or complex logic explanations
Use $lib, $routes, and $themes aliases instead of relative paths for module imports
Files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/lib/stores/billing.tssrc/routes/(console)/create-organization/+page.svelte
src/routes/**/*.svelte
📄 CodeRabbit inference engine (AGENTS.md)
Use SvelteKit file conventions: +page.svelte for components, +page.ts for data loaders, +layout.svelte for wrappers, +error.svelte for error handling, and dynamic route params in square brackets like [param]
Files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/+layout.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/routes/(console)/create-organization/+page.svelte
**/*.{ts,tsx,js,jsx,svelte,json}
📄 CodeRabbit inference engine (AGENTS.md)
Use 4 spaces for indentation, single quotes, 100 character line width, and no trailing commas per Prettier configuration
Files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/lib/stores/billing.tssrc/routes/(console)/create-organization/+page.svelte
**/*.svelte
📄 CodeRabbit inference engine (AGENTS.md)
Use Svelte 5 + SvelteKit 2 syntax with TypeScript for component development
Files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/+layout.sveltesrc/lib/components/support.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/routes/(console)/create-organization/+page.svelte
src/routes/**
📄 CodeRabbit inference engine (AGENTS.md)
Configure dynamic routes using SvelteKit convention with [param] syntax in route directory names
Files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(console)/+layout.sveltesrc/routes/(console)/organization-[organization]/billing/deleteOrgPayment.sveltesrc/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(console)/account/payments/editAddressModal.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/replaceCard.sveltesrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/routes/(console)/+layout.tssrc/routes/(console)/organization-[organization]/billing/paymentMethods.sveltesrc/routes/(console)/account/payments/editPaymentModal.sveltesrc/routes/(console)/create-organization/+page.svelte
src/lib/components/**/*.svelte
📄 CodeRabbit inference engine (AGENTS.md)
Use PascalCase for component file names and place them in src/lib/components/[feature]/ directory structure
Files:
src/lib/components/support.svelte
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
**/*.ts: Define types inline or in .d.ts files, avoid creating separate .types.ts files
Use TypeScript in non-strict mode; any type is tolerated in this project
Files:
src/routes/(console)/project-[region]-[project]/databases/+page.tssrc/routes/(console)/organization-[organization]/billing/+page.tssrc/routes/(public)/sites/deploy/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/+layout.tssrc/lib/stores/billing.ts
🧠 Learnings (7)
📚 Learning: 2025-12-05T09:24:15.846Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2670
File: src/routes/(console)/organization-[organization]/+layout.ts:134-150
Timestamp: 2025-12-05T09:24:15.846Z
Learning: In the Appwrite console codebase, `prefs.organization` is maintained to always reference an organization with Platform.Appwrite, so when using preferences.organization as a fallback in redirect logic, no additional platform validation is required.
Applied to files:
src/routes/(console)/onboarding/create-organization/+page.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(public)/functions/deploy/+page.tssrc/routes/(console)/create-organization/+page.svelte
📚 Learning: 2025-11-25T03:15:27.539Z
Learnt from: CR
Repo: appwrite/console PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:15:27.539Z
Learning: Applies to src/routes/**/*.svelte : Use SvelteKit file conventions: +page.svelte for components, +page.ts for data loaders, +layout.svelte for wrappers, +error.svelte for error handling, and dynamic route params in square brackets like [param]
Applied to files:
src/routes/(console)/+layout.svelte
📚 Learning: 2025-10-13T05:13:54.542Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2413
File: src/routes/(console)/project-[region]-[project]/databases/table.svelte:33-39
Timestamp: 2025-10-13T05:13:54.542Z
Learning: In Svelte 5, `import { page } from '$app/state'` provides a reactive state proxy that can be accessed directly (e.g., `page.params`), unlike the older `import { page } from '$app/stores'` which returns a readable store requiring the `$page` syntax for auto-subscription in components.
Applied to files:
src/routes/(console)/+layout.sveltesrc/routes/(console)/organization-[organization]/billing/+page.sveltesrc/lib/stores/billing.ts
📚 Learning: 2025-10-26T10:20:29.792Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2509
File: src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte:47-61
Timestamp: 2025-10-26T10:20:29.792Z
Learning: When deleting teams in the Appwrite Console codebase, only `Dependencies.TEAMS` needs to be invalidated. Additional invalidations for `Dependencies.TEAM` (detail route) and `Dependencies.MEMBERSHIPS` are not necessary because the deleted teams and their memberships no longer exist.
Applied to files:
src/lib/components/support.sveltesrc/routes/(public)/sites/deploy/+page.tssrc/routes/(public)/functions/deploy/+page.ts
📚 Learning: 2025-10-07T14:17:11.597Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2413
File: src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts:28-28
Timestamp: 2025-10-07T14:17:11.597Z
Learning: In src/routes/(console)/project-[region]-[project]/databases/database-[database]/(entity)/helpers/terminology.ts, the empty `vectordb: {}` entry in baseTerminology is intentional. The vectordb database type is not currently used because the backend API is not finalized, and no database type returns 'vectordb' at the moment.
Applied to files:
src/routes/(console)/project-[region]-[project]/databases/+page.ts
📚 Learning: 2025-10-26T10:21:24.499Z
Learnt from: ItzNotABug
Repo: appwrite/console PR: 2509
File: src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte:24-39
Timestamp: 2025-10-26T10:21:24.499Z
Learning: SDK methods from `sdk.forProject(...).users`, `sdk.forProject(...).teams`, and other Appwrite SDK endpoints always throw `AppwriteException` from `appwrite.io/console`, so caught errors can be returned directly without normalization in deletion handlers.
Applied to files:
src/routes/(console)/+layout.ts
📚 Learning: 2025-11-25T03:15:27.539Z
Learnt from: CR
Repo: appwrite/console PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T03:15:27.539Z
Learning: Applies to **/*.{ts,tsx,js,jsx,svelte} : Use $lib, $routes, and $themes aliases instead of relative paths for module imports
Applied to files:
src/lib/stores/billing.ts
🧬 Code graph analysis (5)
src/routes/(console)/project-[region]-[project]/databases/+page.ts (1)
src/lib/stores/organization.ts (1)
currentPlan(18-18)
src/routes/(console)/organization-[organization]/billing/+page.ts (4)
src/lib/constants.ts (1)
DEFAULT_BILLING_PROJECTS_LIMIT(4-4)src/lib/stores/organization.ts (1)
organization(14-17)src/lib/helpers/load.ts (3)
getPage(8-10)getLimit(12-16)pageToOffset(4-6)src/lib/stores/billing.ts (1)
paymentMethods(62-65)
src/routes/(public)/functions/deploy/+page.ts (4)
src/lib/stores/organization.ts (1)
getTeamOrOrganizationList(22-36)src/lib/system.ts (1)
isCloud(26-26)src/lib/stores/sdk.ts (1)
sdk(171-194)src/lib/stores/billing.ts (1)
getBasePlanFromGroup(88-100)
src/routes/(console)/+layout.ts (2)
src/lib/system.ts (1)
isCloud(26-26)src/lib/stores/sdk.ts (1)
sdk(171-194)
src/lib/stores/billing.ts (5)
src/lib/sdk/billing.ts (1)
BillingPlansMap(15-15)src/lib/stores/sdk.ts (1)
sdk(171-194)src/lib/stores/organization.ts (1)
currentPlan(18-18)src/lib/system.ts (1)
isCloud(26-26)src/lib/constants.ts (1)
NEW_DEV_PRO_UPGRADE_COUPON(6-6)
⏰ 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 (37)
src/routes/(console)/account/payments/editAddressModal.svelte (2)
15-15: LGTM!Type migration from custom
AddresstoModels.BillingAddressaligns with the centralized Models namespace refactoring.
49-52: Good fix for the notification message.Changing from "added" to "updated" is semantically correct for an edit modal.
src/routes/(console)/organization-[organization]/billing/deleteOrgPayment.svelte (2)
22-24: LGTM! API migration to organizations namespace is correct.The change from
billing.removeOrganizationPaymentMethodtoorganizations.deleteDefaultPaymentMethodwith the new object parameter structure aligns with the SDK refactor pattern.
45-47: LGTM! Consistent API migration pattern.The backup payment method deletion correctly migrates to
organizations.deleteBackupPaymentMethodwith the same object parameter structure.src/routes/(console)/project-[region]-[project]/databases/+page.ts (1)
47-49: Type migration is correct and verified.The change from
PlantoModels.BillingPlanon line 47 is correct and aligns with the store definition insrc/lib/stores/organization.ts. ThebackupsEnabledproperty exists onModels.BillingPlanand is safely accessed on line 49 with optional chaining and a nullish coalescing fallback.src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte (2)
34-34: LGTM!The type import for the centralized
Modelsnamespace is correctly added to support the type migration.
47-66: LGTM!The API call migration to
organizations.setDefaultPaymentMethodwith object-based parameters is clean. Error handling, analytics tracking, and cache invalidation are properly preserved.src/routes/(console)/create-organization/+page.svelte (6)
11-11: LGTM on import updates.The migration to
billingIdToPlanandisPaymentAuthenticationRequiredhelpers, along with theModelsnamespace from@appwrite.io/console, aligns with the SDK refactoring objectives.Also applies to: 15-15
26-28: LGTM on state variable type updates.Using
data.planforselectedPlanandPartial<Models.Coupon> | nullforselectedCouponproperly aligns with the new Models-based type system.
49-51: LGTM on coupon API migration.The migration from
getCouponAccounttosdk.forConsole.console.getCoupon({ couponId })follows the consolidated API pattern.
114-132: LGTM on create organization API migration.The migration to
sdk.forConsole.organizations.create()with the new payload structure (organizationId,name,billingPlan,couponId,invites,budget,taxId) correctly implements the refactored API. The conditional handling for free vs. paid plans is well-structured.
134-151: LGTM on payment authentication flow.The use of
isPaymentAuthenticationRequired(org)helper and accessingorg.organizationIdfor thePaymentAuthenticationtype is correct. The redirect URL construction and subsequent validation call are properly handled.
155-169: LGTM on tracking and post-create navigation.The tracking event properly uses
billingIdToPlan(selectedPlan)?.namewith optional chaining for safety. The conditional navigation based onisPaymentAuthenticationRequiredcorrectly handles the success flow.src/lib/components/support.svelte (1)
10-14: LGTM! Clean migration to Models namespace.The import changes correctly migrate from the legacy
plansInfostore lookup to using theModelstype from@appwrite.io/console. This aligns with the broader refactoring effort.src/routes/(console)/account/payments/editPaymentModal.svelte (1)
10-20: LGTM! Type migration to Models namespace.The import and prop type changes correctly align with the broader SDK refactoring effort.
src/routes/(console)/+layout.svelte (2)
9-9: LGTM! Import updates align with Models namespace migration.The import changes correctly support the type migration from custom types to
Models.Organization.Also applies to: 58-58
294-317: LGTM! Function signature and billing trial check updated correctly.The migration from
OrganizationtoModels.Organizationand the direct access toorg.billingTrialDays(instead of deriving fromplansInfo) simplifies the logic and aligns with the new data model.src/routes/(console)/+layout.ts (3)
4-5: LGTM! Import updates for the SDK migration.The imports correctly bring in
Models,Platform, andQueryfrom@appwrite.io/consoleto support the new API surface.
17-21: LGTM! API migration to console namespace with platform parameter.The change from
billing.getPlanstoconsole.getPlans({ platform: Platform.Appwrite })correctly implements the new API surface for plan retrieval.
65-72: LGTM! Type-safe plan mapping with Models namespace.The
toPlanMapfunction signature and implementation correctly useModels.BillingPlanListandModels.BillingPlantypes. Usingplan.$idas the string key is appropriate for the new model structure.src/routes/(console)/organization-[organization]/billing/+page.svelte (3)
11-16: LGTM! Import migration fromtierToPlantobillingIdToPlan.The import change correctly aligns with the renamed helper function in the billing store.
49-52: LGTM! API calls migrated to organizations namespace with object parameters.The changes correctly update:
billing.getInvoice(orgId, invoiceId)→organizations.getInvoice({ organizationId, invoiceId })billing.updateInvoiceStatus(...)→organizations.validateInvoice({ organizationId, invoiceId })The object parameter style aligns with the new SDK conventions.
Also applies to: 67-70, 80-83
126-134: LGTM! Plan name resolution updated to usebillingIdToPlan.The downgrade alert correctly uses
billingIdToPlan()for both the downgraded and current plan names, consistent with the Models-based typing.src/routes/(public)/sites/deploy/+page.ts (1)
5-10: LGTM! Imports align with the SDK refactor.The imports correctly bring in the unified helpers (
getTeamOrOrganizationList,getBasePlanFromGroup) and updated types (BillingPlanGroup,Models) from the appropriate modules using$libaliases as per coding guidelines.src/routes/(console)/onboarding/create-organization/+page.svelte (1)
4-5: LGTM! Imports correctly updated for the refactor.The imports properly use
$libaliases and align with the new billing SDK structure.Also applies to: 13-13
src/routes/(public)/functions/deploy/+page.ts (1)
5-10: LGTM! Imports align with the SDK refactor.Consistent with the sites deploy page refactor.
src/routes/(console)/organization-[organization]/billing/+page.ts (3)
1-9: LGTM! Imports properly structured.The imports correctly use
$libaliases and bring in the necessary types from@appwrite.io/console.
77-92: LGTM! API calls properly migrated to organizations namespace.The Promise.all correctly uses the new endpoints (
account.listPaymentMethods,account.listBillingAddresses,organizations.getAvailableCredits,console.getPlan) with proper payload shapes.
120-141: LGTM! Helper function properly typed with Models.The
getOrganizationPaymentMethodsfunction correctly usesModels.Organization,Models.PaymentMethodList, andModels.PaymentMethodtypes.src/routes/(console)/organization-[organization]/billing/replaceCard.svelte (1)
12-13: LGTM! Type imports and prop definitions properly updated.Good practice aliasing Stripe's
PaymentMethodasStripePaymentMethodto avoid confusion with Appwrite'sModels.PaymentMethod.Also applies to: 23-25, 31-31
src/lib/stores/billing.ts (7)
3-3: LGTM! Imports properly consolidated.Imports correctly use
$libaliases and bring in necessary types from@appwrite.io/console.Also applies to: 17-23, 27-27
62-70: LGTM! Derived stores properly typed with Models.The stores correctly derive from page data with appropriate Model types.
169-196: LGTM!getServiceLimitsignature properly updated.The function correctly accepts an optional
Models.BillingPlanparameter and maintains backward compatibility with the existing tier string parameter.
198-224: LGTM!failedInvoicestore updated to use organizations API.The API call correctly uses
sdk.forConsole.organizations.listInvoiceswith the new payload shape.
599-605: LGTM! URL helpers properly useresolve()for type-safe paths.The
upgradeURLderived store andhideBillingHeaderRoutescorrectly use SvelteKit'sresolve()function for path building.
607-610: LGTM! Type guard properly implemented.The
isPaymentAuthenticationRequiredtype guard correctly uses theinoperator to narrow the union type.
406-408: Undefined access concern is invalid.The
billingIdToPlan()function has a guaranteed return type ofModels.BillingPlanand cannot return undefined. It always returns either the mapped plan fromplansInfoStoreor falls back togetBasePlanFromGroup(BillingPlanGroup.Pro). Therefore, accessing.nameon the result is safe and will not cause an undefined access error.Likely an incorrect or invalid review comment.
| export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan { | ||
| const plansInfoStore = get(plansInfo); | ||
|
|
||
| // hot fix for now, starter doesn't have a group atm. | ||
| const correctBillingPlanGroup = | ||
| billingPlanGroup === BillingPlanGroup.Starter ? null : billingPlanGroup; | ||
|
|
||
| const proPlans = Array.from(plansInfoStore.values()).filter( | ||
| (plan) => plan.group === correctBillingPlanGroup | ||
| ); | ||
|
|
||
| return proPlans.sort((a, b) => a.order - b.order)[0]; | ||
| } |
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.
getBasePlanFromGroup can return undefined, causing runtime errors.
When proPlans is empty (no plans match the group), proPlans.sort(...)[0] returns undefined. The return type declares Models.BillingPlan but undefined is returned in practice. This causes TypeError when callers access .$id or .name.
🐛 Suggested fix with explicit undefined handling
-export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan {
+export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan | undefined {
const plansInfoStore = get(plansInfo);
+ if (!plansInfoStore?.size) return undefined;
// hot fix for now, starter doesn't have a group atm.
const correctBillingPlanGroup =
billingPlanGroup === BillingPlanGroup.Starter ? null : billingPlanGroup;
const proPlans = Array.from(plansInfoStore.values()).filter(
(plan) => plan.group === correctBillingPlanGroup
);
- return proPlans.sort((a, b) => a.order - b.order)[0];
+ if (proPlans.length === 0) return undefined;
+ return proPlans.sort((a, b) => a.order - b.order)[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.
| export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan { | |
| const plansInfoStore = get(plansInfo); | |
| // hot fix for now, starter doesn't have a group atm. | |
| const correctBillingPlanGroup = | |
| billingPlanGroup === BillingPlanGroup.Starter ? null : billingPlanGroup; | |
| const proPlans = Array.from(plansInfoStore.values()).filter( | |
| (plan) => plan.group === correctBillingPlanGroup | |
| ); | |
| return proPlans.sort((a, b) => a.order - b.order)[0]; | |
| } | |
| export function getBasePlanFromGroup(billingPlanGroup: BillingPlanGroup): Models.BillingPlan | undefined { | |
| const plansInfoStore = get(plansInfo); | |
| if (!plansInfoStore?.size) return undefined; | |
| // hot fix for now, starter doesn't have a group atm. | |
| const correctBillingPlanGroup = | |
| billingPlanGroup === BillingPlanGroup.Starter ? null : billingPlanGroup; | |
| const proPlans = Array.from(plansInfoStore.values()).filter( | |
| (plan) => plan.group === correctBillingPlanGroup | |
| ); | |
| if (proPlans.length === 0) return undefined; | |
| return proPlans.sort((a, b) => a.order - b.order)[0]; | |
| } |
🤖 Prompt for AI Agents
In @src/lib/stores/billing.ts around lines 88 - 100, getBasePlanFromGroup can
return undefined when proPlans is empty; change its signature to
Models.BillingPlan | undefined and explicitly handle the empty case instead of
assuming [0] is defined. In getBasePlanFromGroup, compute const base =
proPlans.sort((a,b)=>a.order-b.order)[0]; then return base ?? undefined (or
return a sensible fallback plan found from plansInfoStore if intended), and
update callers to handle a possible undefined result; reference: function
getBasePlanFromGroup, variable proPlans, plansInfoStore, and
BillingPlanGroup.Starter.
| export function billingIdToPlan(billingId: string): Models.BillingPlan { | ||
| const plansInfoStore = get(plansInfo); | ||
| if (plansInfoStore.has(billingId)) { | ||
| return plansInfoStore.get(billingId); | ||
| } else { | ||
| // fallback to PRO group's 1st item. | ||
| // TODO: @itznotabug, @dlohani - but should we fallback to PRO?. | ||
| return getBasePlanFromGroup(BillingPlanGroup.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.
🧩 Analysis chain
🏁 Script executed:
rg -n "billingIdToPlan" --type=ts --type=svelte -C2Repository: appwrite/console
Length of output: 90
🏁 Script executed:
rg -n "billingIdToPlan" --type=ts --type=js -C2Repository: appwrite/console
Length of output: 1989
🏁 Script executed:
rg -n "plansInfo" --type=ts --type=js -C1 | head -100Repository: appwrite/console
Length of output: 3972
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js -C2Repository: appwrite/console
Length of output: 810
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js | grep -v "export function"Repository: appwrite/console
Length of output: 42
🏁 Script executed:
wc -l src/lib/stores/billing.tsRepository: appwrite/console
Length of output: 89
🏁 Script executed:
sed -n '102,145p' src/lib/stores/billing.tsRepository: appwrite/console
Length of output: 1431
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=js --type=svelteRepository: appwrite/console
Length of output: 90
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=ts --type=jsRepository: appwrite/console
Length of output: 249
🏁 Script executed:
rg -n "getNextTierBillingPlan|getPreviousTierBillingPlan" --type=tsRepository: appwrite/console
Length of output: 249
🏁 Script executed:
git ls-files | xargs rg "getNextTierBillingPlan|getPreviousTierBillingPlan" 2>/dev/null | head -20Repository: appwrite/console
Length of output: 496
🏁 Script executed:
cat -n src/lib/components/emptyCardImageCloud.svelteRepository: appwrite/console
Length of output: 1652
🏁 Script executed:
rg -n "enum BillingPlan|class BillingPlan|type BillingPlan" --type=tsRepository: appwrite/console
Length of output: 199
🏁 Script executed:
sed -n '612,630p' src/lib/constants.tsRepository: appwrite/console
Length of output: 568
🏁 Script executed:
sed -n '20,90p' src/routes/\(console\)/+layout.tsRepository: appwrite/console
Length of output: 1680
🏁 Script executed:
sed -n '1,100p' src/routes/\(console\)/project-[region]-[project]/+layout.tsRepository: appwrite/console
Length of output: 3589
Make fallback behavior for unknown billing IDs explicit.
The function currently falls back to BillingPlanGroup.Pro for unknown billing IDs without raising an error. The TODO comment on line 108 indicates this fallback is deliberate but uncertain. If a billing ID becomes invalid or there's a mismatch between the server state and the plansInfo cache, silently returning Pro could cause incorrect billing behavior or UI display.
Either validate that the billing ID exists before returning it, or throw an error to make failures explicit rather than falling back silently.
🤖 Prompt for AI Agents
In @src/lib/stores/billing.ts around lines 102 - 111, The function
billingIdToPlan silently falls back to BillingPlanGroup.Pro when billingId is
missing from plansInfo; instead make failures explicit by validating
plansInfo.has(billingId) and throwing a descriptive error (or returning a
Result/optional) rather than defaulting to Pro. Update billingIdToPlan to check
plansInfo (via get(plansInfo)) and if the id is absent throw an Error like
"Unknown billingId: <id>" (or change the signature to return undefined/null or a
Result type), and remove the silent fallback to
getBasePlanFromGroup(BillingPlanGroup.Pro) to avoid incorrect billing/UI
behavior.
| export async function checkForMissingPaymentMethod() { | ||
| const orgs = await sdk.forConsole.billing.listOrganization([ | ||
| Query.notEqual('billingPlan', BillingPlan.FREE), | ||
| Query.isNull('paymentMethodId'), | ||
| Query.isNull('backupPaymentMethodId'), | ||
| Query.equal('platform', Platform.Appwrite) | ||
| ]); | ||
| const orgs = await sdk.forConsole.organizations.list({ | ||
| queries: [ | ||
| Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id), | ||
| Query.isNull('paymentMethodId'), | ||
| Query.isNull('backupPaymentMethodId'), | ||
| Query.equal('platform', Platform.Appwrite) | ||
| ] | ||
| }); |
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.
Query uses getBasePlanFromGroup which can return undefined.
Line 535 accesses getBasePlanFromGroup(BillingPlanGroup.Starter).$id. If the function returns undefined, this will throw a TypeError.
🐛 Suggested defensive fix
export async function checkForMissingPaymentMethod() {
+ const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
+ if (!starterPlan) return;
+
const orgs = await sdk.forConsole.organizations.list({
queries: [
- Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id),
+ Query.notEqual('billingPlan', starterPlan.$id),
Query.isNull('paymentMethodId'),
Query.isNull('backupPaymentMethodId'),
Query.equal('platform', Platform.Appwrite)
]
});📝 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 async function checkForMissingPaymentMethod() { | |
| const orgs = await sdk.forConsole.billing.listOrganization([ | |
| Query.notEqual('billingPlan', BillingPlan.FREE), | |
| Query.isNull('paymentMethodId'), | |
| Query.isNull('backupPaymentMethodId'), | |
| Query.equal('platform', Platform.Appwrite) | |
| ]); | |
| const orgs = await sdk.forConsole.organizations.list({ | |
| queries: [ | |
| Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id), | |
| Query.isNull('paymentMethodId'), | |
| Query.isNull('backupPaymentMethodId'), | |
| Query.equal('platform', Platform.Appwrite) | |
| ] | |
| }); | |
| export async function checkForMissingPaymentMethod() { | |
| const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter); | |
| if (!starterPlan) return; | |
| const orgs = await sdk.forConsole.organizations.list({ | |
| queries: [ | |
| Query.notEqual('billingPlan', starterPlan.$id), | |
| Query.isNull('paymentMethodId'), | |
| Query.isNull('backupPaymentMethodId'), | |
| Query.equal('platform', Platform.Appwrite) | |
| ] | |
| }); |
🤖 Prompt for AI Agents
In @src/lib/stores/billing.ts around lines 532 - 540, In
checkForMissingPaymentMethod, avoid directly accessing
getBasePlanFromGroup(BillingPlanGroup.Starter).$id because getBasePlanFromGroup
can return undefined; first assign const basePlan =
getBasePlanFromGroup(BillingPlanGroup.Starter) and if basePlan is undefined
either return/exit early or throw/log an error, then use basePlan.$id in the
Query.equal call passed to sdk.forConsole.organizations.list; alternatively
guard using optional chaining and a fallback value before building the query so
you never dereference undefined.
| const organizations = await sdk.forConsole.organizations.list({ | ||
| queries: [Query.notEqual('billingPlan', getBasePlanFromGroup(BillingPlanGroup.Starter).$id)] | ||
| }); |
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 getBasePlanFromGroup undefined risk.
Line 571 accesses getBasePlanFromGroup(BillingPlanGroup.Starter).$id without null safety.
🤖 Prompt for AI Agents
In @src/lib/stores/billing.ts around lines 570 - 572, The call to
getBasePlanFromGroup(BillingPlanGroup.Starter).$id may be undefined; first call
getBasePlanFromGroup(BillingPlanGroup.Starter) into a local variable (e.g.,
basePlan) and check for null/undefined before using .$id, then build the queries
array conditionally so you only include Query.notEqual('billingPlan',
basePlan.$id) when basePlan and basePlan.$id exist (otherwise omit that filter
or use a safe default), and pass the resulting queries to
sdk.forConsole.organizations.list to avoid accessing .$id on undefined.
| const org = await sdk.forConsole.organizations.validatePayment({ | ||
| organizationId, | ||
| invites | ||
| }); | ||
| if (!isPaymentAuthenticationRequired(org)) { | ||
| await preloadData(`${base}/console/organization-${org.$id}`); | ||
| await goto(`${base}/console/organization-${org.$id}`); |
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.
Inconsistent navigation path compared to create() function.
The validate function navigates to ${base}/console/organization-${org.$id} (lines 96-97), but the create function navigates to ${base}/organization-${org.$id} (lines 163-164). Since this file is located in src/routes/(console)/, the route group parentheses don't affect the URL path, so the correct path should be /organization-{id} without the /console segment.
🛠️ Suggested fix to align paths
- await preloadData(`${base}/console/organization-${org.$id}`);
- await goto(`${base}/console/organization-${org.$id}`);
+ await preloadData(`${base}/organization-${org.$id}`);
+ await goto(`${base}/organization-${org.$id}`);🤖 Prompt for AI Agents
In @src/routes/(console)/create-organization/+page.svelte around lines 90 - 97,
The validatePayment branch in the validate() flow currently preloads and
navigates to `${base}/console/organization-${org.$id}` which is inconsistent
with the create() flow; update the two references in the validatePayment success
path (the calls around sdk.forConsole.organizations.validatePayment and the
subsequent preloadData and goto) to use `${base}/organization-${org.$id}`
instead so both validate() and create() navigate to the same canonical
`/organization-{id}` URL.
| return redirect( | ||
| 301, | ||
| resolve('/(console)/organization-[organization]', { | ||
| organization: organization.$id | ||
| }) | ||
| ); |
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.
Verify 301 (permanent) redirect is intentional.
Using HTTP 301 for authorization-based redirects may cause browsers to cache the redirect permanently. If a user later gains billing.read scope, they might still be redirected due to browser cache.
Consider using 302 (temporary) or 307 for authorization-based redirects.
♻️ Suggested fix
if (!scopes.includes('billing.read')) {
return redirect(
- 301,
+ 302,
resolve('/(console)/organization-[organization]', {
organization: organization.$id
})
);
}📝 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.
| return redirect( | |
| 301, | |
| resolve('/(console)/organization-[organization]', { | |
| organization: organization.$id | |
| }) | |
| ); | |
| return redirect( | |
| 302, | |
| resolve('/(console)/organization-[organization]', { | |
| organization: organization.$id | |
| }) | |
| ); |
🤖 Prompt for AI Agents
In @src/routes/(console)/organization-[organization]/billing/+page.ts around
lines 15 - 20, The redirect uses a 301 permanent status in the redirect(...)
call which can be cached by browsers; change the status code from 301 to a
temporary redirect such as 302 (or 307 if preserving method) in the
redirect(resolve(...)) invocation so authorization-based redirects aren’t
cached—update the redirect(301, resolve(...)) call to redirect(302,
resolve(...)) (or 307) where the redirect(...) and resolve(...) symbols are
used.
| export let organization: Models.Organization; | ||
| export let methods: Models.PaymentMethodList; | ||
| export let backupMethod: Models.PaymentMethod; | ||
| export let primaryMethod: Models.PaymentMethod; |
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.
Prop types may need to be optional.
The props backupMethod and primaryMethod are typed as non-optional Models.PaymentMethod, but optional chaining is used throughout the component (e.g., primaryMethod?.lastError on lines 93-95, primaryMethod?.userId on line 129). This suggests these props can be undefined at runtime.
Consider updating the types to match actual usage:
Suggested fix
export let organization: Models.Organization;
export let methods: Models.PaymentMethodList;
-export let backupMethod: Models.PaymentMethod;
-export let primaryMethod: Models.PaymentMethod;
+export let backupMethod: Models.PaymentMethod | undefined;
+export let primaryMethod: Models.PaymentMethod | 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.
| export let organization: Models.Organization; | |
| export let methods: Models.PaymentMethodList; | |
| export let backupMethod: Models.PaymentMethod; | |
| export let primaryMethod: Models.PaymentMethod; | |
| export let organization: Models.Organization; | |
| export let methods: Models.PaymentMethodList; | |
| export let backupMethod: Models.PaymentMethod | undefined; | |
| export let primaryMethod: Models.PaymentMethod | undefined; |
🤖 Prompt for AI Agents
In
@src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte
around lines 36 - 39, The exported props backupMethod and primaryMethod are
declared as non-optional but are used with optional chaining (e.g.,
primaryMethod?.lastError, primaryMethod?.userId), so update their types to be
optional/nullable (e.g., Models.PaymentMethod | undefined) in the export
declarations (export let backupMethod and export let primaryMethod) to match
runtime usage and avoid type errors; ensure any other references to those
symbols (backupMethod, primaryMethod) continue to use safe checks or optional
chaining.
| await sdk.forConsole.organizations.create({ | ||
| organizationId: ID.unique(), | ||
| name: 'Personal Projects', | ||
| billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id | ||
| }); |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for getBasePlanFromGroup definition
rg -n "getBasePlanFromGroup" --type=ts -B 2 -A 8 | head -100Repository: appwrite/console
Length of output: 6624
🏁 Script executed:
#!/bin/bash
# Search for plansInfo store definition and initialization
rg -n "plansInfo" --type=ts -B 2 -A 3Repository: appwrite/console
Length of output: 6727
🏁 Script executed:
#!/bin/bash
# Check the PageLoad function in the file being reviewed
cat -n src/routes/\(public\)/sites/deploy/+page.ts | sed -n '1,110p'Repository: appwrite/console
Length of output: 4493
🏁 Script executed:
#!/bin/bash
# Check if there's a (public) layout that provides plansInfo
find src/routes -name "+layout.ts" | xargs grep -l "plansInfo"Repository: appwrite/console
Length of output: 150
🏁 Script executed:
#!/bin/bash
# Get the complete getBasePlanFromGroup function implementation
sed -n '88,111p' src/lib/stores/billing.tsRepository: appwrite/console
Length of output: 966
🏁 Script executed:
#!/bin/bash
# Check the (public) layout structure and what it provides
find src/routes/\(public\) -name "+layout.ts" -o -name "+layout.js" | head -5Repository: appwrite/console
Length of output: 153
🏁 Script executed:
#!/bin/bash
# Check if there's a root (public) layout that provides plansInfo
cat -n src/routes/\(public\)/\(guest\)/+layout.ts | head -50Repository: appwrite/console
Length of output: 412
🏁 Script executed:
#!/bin/bash
# Check for any +layout at src/routes/(public) root
ls -la src/routes/\(public\)/ | grep layoutRepository: appwrite/console
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Verify plansInfo initialization - check if parent() in deploy page has access to it
rg -n "parent()" src/routes/\(public\)/sites/deploy/+page.ts -B 5 -A 5Repository: appwrite/console
Length of output: 497
🏁 Script executed:
#!/bin/bash
# Check the root layout to understand the hierarchy
cat -n src/routes/+layout.ts 2>/dev/null || echo "No root +layout.ts"Repository: appwrite/console
Length of output: 3064
🏁 Script executed:
#!/bin/bash
# Verify that plansInfo is only provided by (console) layout
rg -n "plansInfo:" src/routes --type=ts | grep -E "return \{|plansInfo"Repository: appwrite/console
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Check if there's any mechanism in public routes that provides plansInfo
rg -n "plansInfo" src/routes/\(public\) --type=tsRepository: appwrite/console
Length of output: 42
🏁 Script executed:
#!/bin/bash
# Verify the exact context in deploy/+page.ts around line 91
sed -n '85,95p' src/routes/\(public\)/sites/deploy/+page.tsRepository: appwrite/console
Length of output: 507
🏁 Script executed:
#!/bin/bash
# Check what the root layout provides for public routes
sed -n '42,46p' src/routes/+layout.tsRepository: appwrite/console
Length of output: 184
🏁 Script executed:
#!/bin/bash
# Final verification: confirm that plansInfo would be empty for public routes
rg -n "derived.*plansInfo|export.*plansInfo" src/lib/stores/billing.ts -A 1Repository: appwrite/console
Length of output: 209
🏁 Script executed:
#!/bin/bash
# One final check: verify getBasePlanFromGroup is not being guarded anywhere
sed -n '85,105p' src/routes/\(public\)/sites/deploy/+page.tsRepository: appwrite/console
Length of output: 813
Potential runtime error: getBasePlanFromGroup() returns undefined for public routes.
getBasePlanFromGroup(BillingPlanGroup.Starter) accesses a store (plansInfo) that is only populated by the (console) layout (src/routes/(console)/+layout.ts). Since this page is in the (public) route group, plansInfo will be empty, causing the function to return undefined (line 99 in src/lib/stores/billing.ts returns proPlans.sort()[0] on an empty array). Accessing .$id on undefined throws a TypeError.
While the try-catch at lines 86–104 prevents the app from crashing, it silently fails and blocks organization creation. Either populate plansInfo in the public route context or provide a fallback billing plan ID.
🤖 Prompt for AI Agents
In @src/routes/(public)/sites/deploy/+page.ts around lines 88 - 92, The create
call uses getBasePlanFromGroup(BillingPlanGroup.Starter).$id which can be
undefined in the (public) route because plansInfo is only populated in the
(console) layout; update the organization creation to guard for undefined by
calling getBasePlanFromGroup(BillingPlanGroup.Starter), check its return value
(from the getBasePlanFromGroup helper) and use a safe fallback billing plan id
(e.g., a DEFAULT_PLAN_ID constant or a known starter plan id) or fetch/populate
plansInfo before calling; ensure you reference getBasePlanFromGroup and the
organization creation block (await sdk.forConsole.organizations.create) when
adding the null-check/fallback and logging an informative error if neither a
plan nor fallback exist.

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
Release Notes
New Features
Bug Fixes
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.