diff --git a/.env.example b/.env.example index 5a16f982f..9ba4011c0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ PUBLIC_CONSOLE_MODE=self-hosted -PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths +PUBLIC_CONSOLE_FEATURE_FLAGS= PUBLIC_APPWRITE_MULTI_REGION=false PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1 PUBLIC_STRIPE_KEY= diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a700a4add..8cfa0c03f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | "PUBLIC_CONSOLE_MODE=cloud" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}" @@ -79,7 +79,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | "PUBLIC_CONSOLE_MODE=cloud" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_APPWRITE_MULTI_REGION=true" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" @@ -118,7 +118,7 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=self-hosted" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" publish-cloud-no-regions: @@ -156,6 +156,6 @@ jobs: build-args: | "PUBLIC_CONSOLE_MODE=cloud" "PUBLIC_APPWRITE_MULTI_REGION=false" - "PUBLIC_CONSOLE_FEATURE_FLAGS=sites,index-lengths" + "PUBLIC_CONSOLE_FEATURE_FLAGS=" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}" "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" diff --git a/package.json b/package.json index 3104f604a..02cb4f171 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428", + "@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "^2.0.0-RC.1", + "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7", + "@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0", "@faker-js/faker": "^9.9.0", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28f2888f7..a6ff6011a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,20 +12,20 @@ importers: specifier: ^1.1.24 version: 1.1.24(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428 - version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428 + specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134 + version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: ^2.0.0-RC.1 - version: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7 - version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3) + specifier: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0 + version: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3) '@faker-js/faker': specifier: ^9.9.0 version: 9.9.0 @@ -260,8 +260,8 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428': - resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': + resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134} version: 1.10.0 '@appwrite.io/pink-icons-svelte@2.0.0-RC.1': @@ -269,8 +269,8 @@ packages: peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9': - resolution: {tarball: https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9} + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0} version: 2.0.0-RC.1 peerDependencies: svelte: ^4.0.0 @@ -284,8 +284,8 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7': - resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7} + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0': + resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0} version: 2.0.0-RC.2 peerDependencies: svelte: ^4.0.0 @@ -3700,13 +3700,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2428': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@6031134': {} '@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://try-module.cloud/module/@appwrite/%40appwrite.io%2Fpink-icons-svelte@12707b9(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@2cf27e0(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3719,7 +3719,7 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@18188b7(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@2cf27e0(svelte@5.25.3)': dependencies: '@appwrite.io/pink-icons-svelte': 2.0.0-RC.1(svelte@5.25.3) '@floating-ui/dom': 1.6.13 diff --git a/src/lib/components/archiveProject.svelte b/src/lib/components/archiveProject.svelte new file mode 100644 index 000000000..3ebb938c1 --- /dev/null +++ b/src/lib/components/archiveProject.svelte @@ -0,0 +1,292 @@ + + +{#if projectsToArchive.length > 0} +
+ + + These projects have been archived and are read-only. You can view and migrate their + data. + + +
+ + {#each projectsToArchive as project} + {@const platforms = filterPlatforms( + project.platforms.map((platform) => getPlatformInfo(platform.type)) + )} + {@const formatted = formatName(project.name)} + + + {project?.platforms?.length ? project?.platforms?.length : 'No'} apps + + {formatted} + +
+ + { + e.preventDefault(); + e.stopPropagation(); + readOnlyInfoOpen = { + ...readOnlyInfoOpen, + [project.$id]: !readOnlyInfoOpen[project.$id] + }; + }}> + + Read only + + +
  • + + Archived projects are read-only. You can view + and migrate their data, but they no longer + accept edits or requests. + +
  • +
    +
    + + + + handleUnarchiveProject(project)} + >Unarchive project + handleMigrateProject(project)} + >Migrate project + + +
    +
    + + {#each platforms.slice(0, 2) as platform} + {@const icon = getIconForPlatform(platform.icon)} + + + + {/each} + + {#if platforms.length > 3} + + {/if} + + + {#if isCloud && $regionsStore?.regions} + {@const region = findRegion(project)} + {region?.name} + {/if} + +
    + {/each} +
    +
    +
    +
    +{/if} + + + +

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

    +

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

    + + + + + + + +
    + + diff --git a/src/lib/components/billing/alerts/limitReached.svelte b/src/lib/components/billing/alerts/limitReached.svelte index 11d832107..4885db75e 100644 --- a/src/lib/components/billing/alerts/limitReached.svelte +++ b/src/lib/components/billing/alerts/limitReached.svelte @@ -20,9 +20,14 @@ plan. Consider upgrading to increase your resource usage. - + {#if !page.data.currentPlan?.usagePerProject} + + {/if} + + + {/if} + +
    + + + Resource + Free limit + + + Excess usage + + + Usage beyond the Free plan limits. + + + + + + + + + + + Projects + {#if isLimitExceeded.projects} + + {/if} + + + + {formatNumber(allowedProjectsToKeep)} projects + + + {#if isLimitExceeded.projects} + + + + {formatNumber(excessUsage.projects)} projects + + + {:else} + + {formatNumber(currentUsage.projects)} / {formatNumber( + allowedProjectsToKeep + )} + + {/if} + + + {#if isLimitExceeded.projects} + + + + {/if} + + + + + + + Organization members + + + {formatNumber(freePlanLimits.members)} member + + + {#if isLimitExceeded.members} + + + + {formatNumber(excessUsage.members)} members + + + {:else} + + {formatNumber(currentUsage.members)} / {formatNumber( + freePlanLimits.members + )} + + {/if} + + + + + + + + Storage + + + {freePlanLimits.storage} GB + + + {#if isLimitExceeded.storage} + + + + {excessUsage.storage.toFixed(2)} GB + + + {:else} + + {storageUsageGB.toFixed(2)} / {freePlanLimits.storage} GB + + {/if} + + + + +
    + + +{#if showSelectProject} + + + Choose which {freePlanLimits.projects} projects to keep. Projects over the limit will be + blocked after your billing cycle ends on {toLocaleDate( + $organization.billingNextInvoiceDate + )}. + + + {#if error} + {error} + {/if} + +
    + + + Project Name + Created + + {#each projects as project} + + {project.name} + + {toLocaleDateTime(project.$createdAt)} + + + {/each} + +
    + {#if selectedProjects.length === allowedProjectsToKeep} + {@const difference = projects.length - selectedProjects.length} + {@const messagePrefix = + difference > 1 ? `${difference} projects` : `${difference} project`} + + {formatProjectsToArchive()} will be archived + + {/if} + + + + + +
    +{/if} + + diff --git a/src/lib/components/progressbar/ProgressBar.svelte b/src/lib/components/progressbar/ProgressBar.svelte index 58b3a3a39..a4cebc8ee 100644 --- a/src/lib/components/progressbar/ProgressBar.svelte +++ b/src/lib/components/progressbar/ProgressBar.svelte @@ -66,6 +66,7 @@ flex-direction: row; gap: 2px; margin-top: 1rem; + overflow: hidden; } &__content { diff --git a/src/lib/elements/forms/inputNumber.svelte b/src/lib/elements/forms/inputNumber.svelte index f18aff0fa..43fb41538 100644 --- a/src/lib/elements/forms/inputNumber.svelte +++ b/src/lib/elements/forms/inputNumber.svelte @@ -18,20 +18,6 @@ let error: string; - function coerceToNumber(event: CustomEvent) { - const raw = event.detail ?? ''; - - if (raw === '') { - value = nullable ? null : (undefined as unknown as number); - return; - } - - const parsed = Number(raw); - if (Number.isFinite(parsed)) { - value = parsed; - } - } - const handleInvalid = (event: Event & { currentTarget: EventTarget & HTMLInputElement }) => { event.preventDefault(); @@ -74,8 +60,7 @@ autofocus={autofocus || undefined} helper={error || helper} state={error ? 'error' : 'default'} - on:invalid={handleInvalid} - on:change={coerceToNumber}> + on:invalid={handleInvalid}> diff --git a/src/lib/flags.ts b/src/lib/flags.ts index 70bba582a..dfbedecd9 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -5,6 +5,7 @@ import type { Organization } from './stores/organization'; // Parse feature flags from env as a string array (exact match only) const flagsRaw = (env.PUBLIC_CONSOLE_FEATURE_FLAGS ?? '').split(','); +// @ts-expect-error: unused method! function isFlagEnabled(name: string) { // loose generic to allow safe access while retaining type safety return (data: T) => { @@ -18,6 +19,4 @@ function isFlagEnabled(name: string) { }; } -export const flags = { - showSites: isFlagEnabled('sites') -}; +export const flags = {}; diff --git a/src/lib/helpers/string.ts b/src/lib/helpers/string.ts index c138a7483..91dd9a37c 100644 --- a/src/lib/helpers/string.ts +++ b/src/lib/helpers/string.ts @@ -49,6 +49,19 @@ export function formatNum(number: number): string { return formatter.format(number); } +/** + * Format a string with optional mobile-aware truncation. + */ +export function formatName( + name: string, + limit: number = 19, + isSmallViewport: boolean = false +): string { + const mobileLimit = 16; + const actualLimit = isSmallViewport ? mobileLimit : limit; + return name ? (name.length > actualLimit ? `${name.slice(0, actualLimit)}...` : name) : '-'; +} + /** * Returns a regex to check hostname validity. Supports wildcards too! */ diff --git a/src/lib/helpers/waitlist.ts b/src/lib/helpers/waitlist.ts deleted file mode 100644 index d0ff50e85..000000000 --- a/src/lib/helpers/waitlist.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sdk } from '$lib/stores/sdk'; -import { type Account } from '$lib/stores/user'; - -export const joinWaitlistSites = (user: Account) => { - const prefs = user.prefs; - const newPrefs = { - ...prefs, - joinWaitlistSites: true - }; - - sdk.forConsole.account.updatePrefs({ prefs: newPrefs }); - - if (sessionStorage) { - sessionStorage.setItem('joinWaitlistSites', 'true'); - } -}; - -export const isOnWaitlistSites = (user: Account): boolean => { - const prefs = user.prefs; - const joinedInPrefs = 'joinWaitlistSites' in prefs; - - let joinedInSession = false; - if (sessionStorage) { - joinedInSession = sessionStorage.getItem('joinWaitlistSites') === 'true'; - } - - return joinedInSession || joinedInPrefs; -}; diff --git a/src/lib/layout/createProject.svelte b/src/lib/layout/createProject.svelte index 5d951babd..92899ae59 100644 --- a/src/lib/layout/createProject.svelte +++ b/src/lib/layout/createProject.svelte @@ -4,13 +4,15 @@ import { CustomId } from '$lib/components/index.js'; import { getFlagUrl } from '$lib/helpers/flag'; import { isCloud } from '$lib/system.js'; - import { currentPlan } from '$lib/stores/organization'; + import { currentPlan, organization } from '$lib/stores/organization'; import { Button } from '$lib/elements/forms'; import { base } from '$app/paths'; import { page } from '$app/state'; import type { Models } from '@appwrite.io/console'; import { filterRegions } from '$lib/helpers/regions'; import type { Snippet } from 'svelte'; + import { BillingPlan } from '$lib/constants'; + import { formatCurrency } from '$lib/helpers/numbers'; let { projectName = $bindable(''), @@ -18,6 +20,7 @@ regions = [], region = $bindable(''), showTitle = true, + billingPlan = undefined, projects = undefined, submit }: { @@ -26,14 +29,21 @@ regions: Array; region: string; showTitle: boolean; + billingPlan?: BillingPlan; projects?: number; submit?: Snippet; } = $props(); let showCustomId = $state(false); + let isProPlan = $derived((billingPlan ?? $organization?.billingPlan) === BillingPlan.PRO); let projectsLimited = $derived( $currentPlan?.projects > 0 && projects && projects >= $currentPlan?.projects ); + let isAddonProject = $derived( + $currentPlan?.addons?.projects?.supported && + projects && + projects >= $currentPlan?.addons?.projects?.planIncluded + ); @@ -46,26 +56,12 @@ {#if showTitle} Create your project {/if} - {#if projectsLimited} - - Extra projects are available on paid plans for an additional fee - - - - - {/if} + + {#if isCloud && regions.length > 0} Region cannot be changed after creation {/if} + {#if isAddonProject} + + Each added project comes with its own dedicated pool of resources. + + {/if} + {#if projectsLimited} + + Extra projects are available on paid plans for an additional fee + + + + + {/if} {@render submit?.()} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 3dbd0b9f9..0361f5910 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -151,6 +151,79 @@ export type CreditList = { total: number; }; +export type AggregationTeam = { + $id: string; + /** + * Aggregation creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Aggregation update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Beginning date of the invoice. + */ + from: string; + /** + * End date of the invoice. + */ + to: string; + /** + * Total amount of the invoice. + */ + amount: number; + additionalMembers: number; + + /** + * Price for additional members + */ + additionalMemberAmount: number; + /** + * Total storage usage. + */ + usageStorage: number; + /** + * Total active users for the billing period. + */ + usageUsers: number; + /** + * Total number of executions for the billing period. + */ + usageExecutions: number; + /** + * Total bandwidth usage for the billing period. + */ + usageBandwidth: number; + /** + * Total realtime usage for the billing period. + */ + usageRealtime: number; + /** + * Usage logs for the billing period. + */ + resources: InvoiceUsage[]; + /** + * Aggregation billing plan + */ + plan: string; + breakdown: AggregationBreakdown[]; +}; + +export type AggregationBreakdown = { + $id: string; + name: string; + amount: number; + region: string; + resources: InvoiceUsage[]; +}; + +export type InvoiceUsage = { + resourceId: string; + value: number; + amount: number; +}; + export type AvailableCredit = { available: number; }; @@ -299,6 +372,7 @@ export type PlanAddon = { limit: number; value: number; type: string; + planIncluded: number; }; export type Plan = { @@ -316,10 +390,13 @@ export type Plan = { projects: number; databases: number; databasesAllowEncrypt: boolean; + databasesReads: number; + databasesWrites: number; buckets: number; fileSize: number; functions: number; executions: number; + GBHours: number; realtime: number; logs: number; authPhone: number; @@ -330,9 +407,14 @@ export type Plan = { realtime: AdditionalResource; storage: AdditionalResource; users: AdditionalResource; + databasesReads: AdditionalResource; + databasesWrites: AdditionalResource; + GBHours: AdditionalResource; + imageTransformations: AdditionalResource; }; addons: { seats: PlanAddon; + projects: PlanAddon; }; trialDays: number; budgetCapEnabled: boolean; @@ -348,6 +430,7 @@ export type Plan = { supportsOrganizationRoles: boolean; buildSize: number; // in MB deploymentSize: number; // in MB + usagePerProject: boolean; }; export type PlanList = { @@ -355,7 +438,7 @@ export type PlanList = { total: number; }; -export type PlansMap = Map; +export type PlansMap = Map; export type Roles = { scopes: string[]; @@ -492,6 +575,22 @@ export class Billing { }); } + async listPlans(queries: string[] = []): Promise { + const path = `/console/plans`; + const uri = new URL(this.client.config.endpoint + path); + const params = { + queries + }; + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async getPlan(planId: string): Promise { const path = `/console/plans/${planId}`; const uri = new URL(this.client.config.endpoint + path); @@ -835,7 +934,7 @@ export class Billing { ); } - async getAggregation(organizationId: string, aggregationId: string): Promise { + async getAggregation(organizationId: string, aggregationId: string): Promise { const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; const params = { organizationId, diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 993e01fbe..2a88fa83a 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -14,7 +14,7 @@ import { cachedStore } from '$lib/helpers/cache'; import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion'; import type { AddressesList, - Aggregation, + AggregationTeam, Invoice, InvoiceList, PaymentList, @@ -69,7 +69,6 @@ export const roles = [ export const teamStatusReadonly = 'readonly'; export const billingLimitOutstandingInvoice = 'outstanding_invoice'; -export const billingProjectsLimitDate = '2025-09-01'; export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList); export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList); @@ -162,8 +161,13 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan // the correct info for members/seats, resides in `addons`. // plan > addons > seats/others if (serviceId === 'members') { - // some don't include `limit`, so we fallback! - return plan?.['addons']['seats']['limit'] ?? 1; + // pro and scale plans have unlimited seats (per-project NEW pricing model) + const currentTier = tier ?? get(organization)?.billingPlan; + if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { + return Infinity; // unlimited seats for Pro and Scale plans + } + // Free plan still has 1 member limit + return (plan?.['addons']['seats'] || [])['limit'] ?? 1; } return plan?.[serviceId] ?? 0; @@ -235,6 +239,7 @@ export const tierEnterprise: TierData = { }; export const showUsageRatesModal = writable(false); +export const useNewPricingModal = derived(currentPlan, ($plan) => $plan?.usagePerProject === true); export function checkForUsageFees(plan: Tier, id: PlanServices) { if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) { @@ -253,11 +258,19 @@ export function checkForUsageFees(plan: Tier, id: PlanServices) { } export function checkForProjectLimitation(id: PlanServices) { + // Members are no longer limited on Pro and Scale plans (unlimited seats) + if (id === 'members') { + const currentTier = get(organization)?.billingPlan; + if (currentTier === BillingPlan.PRO || currentTier === BillingPlan.SCALE) { + return false; // No project limitation for members on Pro/Scale plans + } + } + switch (id) { case 'databases': case 'functions': case 'buckets': - case 'members': + case 'members': // Only applies to Free plan now case 'platforms': case 'webhooks': case 'teams': @@ -325,7 +338,8 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?: if (!plan) return; if (plan.$id !== BillingPlan.FREE) return; - if (org.projects?.length > 0) return; + if (!org.projects) return; + if (org.projects.length > 0) return; const projectCount = orgProjectCount; if (projectCount === undefined) return; @@ -383,13 +397,8 @@ export async function checkForUsageLimit(org: Organization) { ]; const members = org.total; - const plan = get(currentPlan); - const membersOverflow = - // `plan` can be null on `onboarding/create-organization` route. - // nested null checks needed: GitHub Education plan have empty addons. - members > plan?.addons?.seats?.limit - ? members - (plan?.addons?.seats?.limit || members) - : 0; + const memberLimit = getServiceLimit('members'); + const membersOverflow = memberLimit === Infinity ? 0 : Math.max(0, members - memberLimit); if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); @@ -609,7 +618,7 @@ export const billingURL = derived( export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account']; -export function calculateExcess(addon: Aggregation, plan: Plan) { +export function calculateExcess(addon: AggregationTeam, plan: Plan) { return { bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth), storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), diff --git a/src/routes/(authenticated)/git/+layout.ts b/src/routes/(authenticated)/git/+layout.ts index efd98b286..cd35d7478 100644 --- a/src/routes/(authenticated)/git/+layout.ts +++ b/src/routes/(authenticated)/git/+layout.ts @@ -2,10 +2,26 @@ import { redirect } from '@sveltejs/kit'; import { base } from '$app/paths'; import type { LayoutLoad } from './$types'; -export const load: LayoutLoad = async ({ parent }) => { +export const load: LayoutLoad = async ({ parent, url }) => { const { account } = await parent(); - if (!account) { - redirect(303, base); + const projectId = url.searchParams.get('projectId'); + const repositoryId = url.searchParams.get('repositoryId'); + const installationId = url.searchParams.get('installationId'); + const providerPullRequestId = url.searchParams.get('providerPullRequestId'); + + if (!installationId || !repositoryId || !providerPullRequestId) { + redirect(303, `${base}`); } + + if (!account) { + redirect(303, `${base}`); + } + + return { + projectId, + repositoryId, + installationId, + providerPullRequestId + }; }; diff --git a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte index 9a5d8e99e..23c1b34d5 100644 --- a/src/routes/(authenticated)/git/authorize-contributor/+page.svelte +++ b/src/routes/(authenticated)/git/authorize-contributor/+page.svelte @@ -1,5 +1,4 @@ - {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && data.projects.total > $currentPlan.projects && $canWriteProjects} + {#if isCloud && $currentPlan?.projects && $currentPlan?.projects > 0 && data.organization.projects.length > 0 && $canWriteProjects && (projectsToArchive.length > 0 || data.projects.total > $currentPlan.projects)} + {@const difference = projectsToArchive} + {@const messagePrefix = + difference.length > 1 ? `${difference} projects` : `${difference} project`} - - {#each projectsToArchive as project, index}{@const text = `${project.name}`} - {@html text}{index === projectsToArchive.length - 2 - ? ', and ' - : index < projectsToArchive.length - 1 - ? ', ' - : ''} - {/each} - will be archived - + title={`${messagePrefix} will be archived on ${toLocaleDate($organization.billingNextInvoiceDate)}`}> + Upgrade your plan to restore archived projects - @@ -176,7 +166,7 @@ {/if} {:else} - Add credits + (show = true)}>Add credits {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte index 44c9c9710..ac649dd83 100644 --- a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte +++ b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte @@ -28,7 +28,12 @@ } - +

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

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

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

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

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

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

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

    diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte index 24b82ae7b..31fc89017 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.svelte @@ -243,7 +243,7 @@ Executions - Calculated for all functions that are executed in all projects in your project. + Calculated for all functions that are executed in this project. {#if executions} {@const current = formatNum(executionsTotal)} diff --git a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts index 4afa9181a..e5e1ee5a6 100644 --- a/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/settings/usage/[[invoice]]/+page.ts @@ -1,4 +1,4 @@ -import type { Aggregation, Invoice } from '$lib/sdk/billing'; +import type { AggregationTeam, Invoice, InvoiceUsage } from '$lib/sdk/billing'; import { accumulateUsage } from '$lib/sdk/usage'; import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; @@ -11,7 +11,7 @@ export const load: PageLoad = async ({ params, parent }) => { let startDate: string = organization.billingCurrentInvoiceDate; let endDate: string = organization.billingNextInvoiceDate; let currentInvoice: Invoice = undefined; - let currentAggregation: Aggregation = undefined; + let currentAggregation: AggregationTeam = undefined; if (invoice) { currentInvoice = await sdk.forConsole.billing.getInvoice(organization.$id, invoice); @@ -22,6 +22,15 @@ export const load: PageLoad = async ({ params, parent }) => { startDate = currentInvoice.from; endDate = currentInvoice.to; + } else { + try { + currentAggregation = await sdk.forConsole.billing.getAggregation( + organization.$id, + organization.billingAggregationId + ); + } catch (e) { + // ignore error if no aggregation found + } } const [invoices, usage] = await Promise.all([ @@ -29,10 +38,24 @@ export const load: PageLoad = async ({ params, parent }) => { sdk.forProject(region, project).project.getUsage({ startDate, endDate }) ]); - if (invoice) { - usage.usersTotal = currentAggregation.usageUsers; - usage.executionsTotal = currentAggregation.usageExecutions; - usage.filesStorageTotal = currentAggregation.usageStorage; + if (currentAggregation) { + let projectSpecificData = null; + if (currentAggregation.breakdown) { + projectSpecificData = currentAggregation.breakdown.find((p) => p.$id === project); + } + + if (projectSpecificData) { + const executionsResource = projectSpecificData.resources?.find?.( + (r: InvoiceUsage) => r.resourceId === 'executions' + ); + if (executionsResource) { + usage.executionsTotal = executionsResource.value || usage.executionsTotal; + } + } else { + usage.usersTotal = currentAggregation.usageUsers; + usage.executionsTotal = currentAggregation.usageExecutions; + usage.filesStorageTotal = currentAggregation.usageStorage; + } } usage.users = accumulateUsage(usage.users, usage.usersTotal); diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte index 7cbb99660..caa180049 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.svelte +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.svelte @@ -11,14 +11,12 @@ import { isServiceLimited } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import { canWriteSites } from '$lib/stores/roles.js'; - import { Card, Icon, Layout, Typography } from '@appwrite.io/pink-svelte'; + import { Icon, Layout } from '@appwrite.io/pink-svelte'; import { Button } from '$lib/elements/forms'; import { app } from '$lib/stores/app'; import CreateSiteModal from './createSiteModal.svelte'; import EmptyLight from './(images)/empty-sites-light.svg'; import EmptyDark from './(images)/empty-sites-dark.svg'; - import EmptyLightMobile from './(images)/empty-sites-light-mobile.svg'; - import EmptyDarkMobile from './(images)/empty-sites-dark-mobile.svg'; import Grid from './grid.svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import { columns } from './store'; @@ -28,15 +26,10 @@ import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; - import { isSmallViewport } from '$lib/stores/viewport'; - import { addNotification } from '$lib/stores/notifications'; - import { isOnWaitlistSites, joinWaitlistSites } from '$lib/helpers/waitlist'; - import { user } from '$lib/stores/user'; export let data; let show = false; - let isOnWaitlist = isOnWaitlistSites($user); $: $registerCommands([ { @@ -57,118 +50,51 @@ onMount(() => { return sdk.forConsole.client.subscribe('console', (response) => { - if (response.events.includes(`sites.*`)) { + if (response.events.includes('sites.*')) { invalidate(Dependencies.SITES); } }); }); - - $: isDark = $app.themeInUse === 'dark'; - $: imgSrc = isDark - ? $isSmallViewport - ? EmptyDarkMobile - : EmptyDark - : $isSmallViewport - ? EmptyLightMobile - : EmptyLight; - $: imgClass = $isSmallViewport ? 'mobile' : 'desktop'; - - function addToWaitlist() { - joinWaitlistSites($user); - addNotification({ - type: 'success', - title: 'Waitlist joined', - message: "We'll let you know as soon as Appwrite Sites is ready for you." - }); - - isOnWaitlist = true; - } - {#if data.sitesLive} - - - - - - - {#if $canWriteSites} - - {/if} - + + + - {#if data.siteList.total} - {#if data.view === View.Grid} - - {:else} - + + + {#if $canWriteSites} + {/if} - - {:else if data.search} - + + + {#if data.siteList.total} + {#if data.view === View.Grid} + {:else} - (show = true)}> - +
    {/if} + + {:else if data.search} + {:else} - - - - - - {#if isOnWaitlist} - - You've successfully joined the Sites waitlist - - - - We can't wait for you to try out Sites on Cloud. You will get access - soon. - - {:else} - - - Appwrite Sites is in high demand - - -
    - - To ensure a smooth experience for everyone, we’re rolling out - access gradually. Join the waitlist and be one of the first to - deploy with Sites. - -
    - -
    - -
    -
    - {/if} -
    -
    -
    + (show = true)}> + {/if} diff --git a/src/routes/(console)/project-[region]-[project]/sites/+page.ts b/src/routes/(console)/project-[region]-[project]/sites/+page.ts index e1cc8f1b6..6e711b2d5 100644 --- a/src/routes/(console)/project-[region]-[project]/sites/+page.ts +++ b/src/routes/(console)/project-[region]-[project]/sites/+page.ts @@ -1,12 +1,9 @@ -import { Query, type Models } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; -import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; +import { Query } from '@appwrite.io/console'; import { CARD_LIMIT, Dependencies } from '$lib/constants'; -import { flags } from '$lib/flags'; - -export const load = async ({ url, depends, route, params, parent }) => { - const data = await parent(); +import { getLimit, getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; +export const load = async ({ url, depends, route, params }) => { depends(Dependencies.SITES); const page = getPage(url); const search = getSearch(url); @@ -14,22 +11,7 @@ export const load = async ({ url, depends, route, params, parent }) => { const offset = pageToOffset(page, limit); const view = getView(url, route, View.Grid, View.Grid); - if (!flags.showSites(data)) { - return { - sitesLive: false, - offset, - limit, - search, - view, - siteList: { - total: 0, - sites: [] - } as Models.SiteList - }; - } - return { - sitesLive: true, offset, limit, search,