Merge branch 'feat-profiles' of https://github.com/appwrite/console into feat-profiles-polishing

This commit is contained in:
Torsten Dittmann
2025-11-17 21:10:49 +04:00
60 changed files with 321 additions and 134 deletions
+5
View File
@@ -57,3 +57,8 @@ EXPOSE 80
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html/console
# feat-profiles
RUN mkdir -p /app
COPY docker/generate-env.sh /app/generate-env.sh
RUN chmod +x /app/generate-env.sh
+86
View File
@@ -0,0 +1,86 @@
#!/bin/sh
set -e
# Regenerate env.js from runtime PUBLIC_ environment variables
echo "========================================="
echo "Regenerating env.js from environment variables..."
echo "========================================="
# Check if any PUBLIC_ env vars exist
public_vars=$(printenv | grep '^PUBLIC_' | awk -F= '{print $1}' | sort)
if [ -z "$public_vars" ]; then
echo "⚠ WARNING: No PUBLIC_ environment variables found!"
echo "The application may not work correctly."
echo "Expected variables like:"
echo " - PUBLIC_APPWRITE_ENDPOINT"
echo " - PUBLIC_CONSOLE_MODE"
echo " - PUBLIC_CONSOLE_PROFILE"
echo "========================================="
fi
# Build JSON object from PUBLIC_ env vars
env_json="{"
first=true
for var in $public_vars; do
value=$(printenv "$var")
# Escape special characters for JSON
escaped_value=$(echo "$value" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
if [ "$first" = true ]; then
first=false
else
env_json="$env_json,"
fi
env_json="$env_json\"$var\":\"$escaped_value\""
echo " ✓ $var=$value"
done
env_json="$env_json}"
# Write to env.js
# Use CONSOLE_BUILD_PATH env var if set, otherwise try common locations
if [ -n "$CONSOLE_BUILD_PATH" ]; then
env_file="$CONSOLE_BUILD_PATH/_app/env.js"
elif [ -f "/usr/share/nginx/html/console/_app/env.js" ]; then
env_file="/usr/share/nginx/html/console/_app/env.js"
elif [ -f "./build/_app/env.js" ]; then
env_file="./build/_app/env.js"
elif [ -f "./_app/env.js" ]; then
env_file="./_app/env.js"
else
echo "⚠ Error: Cannot find env.js file. Set CONSOLE_BUILD_PATH env var to the build directory."
echo "Searched in:"
echo " - /usr/share/nginx/html/console/_app/env.js"
echo " - ./build/_app/env.js"
echo " - ./_app/env.js"
exit 1
fi
echo "Writing to: $env_file"
echo "export const env=$env_json" > "$env_file"
# Verify the file was written
if [ ! -f "$env_file" ]; then
echo "⚠ Error: Failed to write $env_file"
exit 1
fi
echo "✓ Successfully generated $env_file"
echo "File contents:"
cat "$env_file"
# NUKE all pre-compressed files so nginx serves fresh content
echo "Nuking all .br and .gz files..."
find /usr/share/nginx/html/console -type f \( -name "*.br" -o -name "*.gz" \) -delete
echo "✓ Nuked all compressed files"
echo "========================================="
echo "Starting nginx..."
echo "========================================="
exec "$@"
+1 -1
View File
@@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@4472521",
"@appwrite.io/pink-legacy": "^1.0.3",
+5 -5
View File
@@ -12,8 +12,8 @@ importers:
specifier: ^1.1.24
version: 1.1.24(svelte@5.43.6)(zod@3.24.3)
'@appwrite.io/console':
specifier: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752
version: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7
'@appwrite.io/pink-icons':
specifier: 0.25.0
version: 0.25.0
@@ -272,8 +272,8 @@ packages:
'@analytics/type-utils@0.6.4':
resolution: {integrity: sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw==}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752':
resolution: {tarball: https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7':
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7}
version: 1.10.0
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
@@ -3648,7 +3648,7 @@ snapshots:
'@analytics/type-utils@0.6.4': {}
'@appwrite.io/console@https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752': {}
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5ce6dc7': {}
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.43.6)':
dependencies:
@@ -3,11 +3,15 @@ import { base } from '$app/paths';
import { sdk } from '$lib/stores/sdk';
import type { Searcher } from '../commands';
import { isCloud } from '$lib/system';
import { Query } from '@appwrite.io/console';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const orgSearcher = (async (query: string) => {
const { teams } = !isCloud
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization();
: await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
return teams
.filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase()))
+2 -2
View File
@@ -29,7 +29,6 @@
import { getPlatformInfo } from '$lib/helpers/platform';
import { Status, type Models } from '@appwrite.io/console';
import type { ComponentType } from 'svelte';
import { BillingPlan } from '$lib/constants';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { sdk } from '$lib/stores/sdk';
@@ -97,7 +96,7 @@
function isUnarchiveDisabled(): boolean {
if (!organization || !currentPlan) return true;
if (organization.billingPlan === BillingPlan.FREE) {
if (isFreePlan(organization.billingPlan)) {
const currentProjectCount = organization.projects?.length || 0;
const projectLimit = currentPlan.projects || 0;
@@ -189,6 +188,7 @@
}
import { formatName as formatNameHelper } from '$lib/helpers/string';
import { isFreePlan } from '$lib/helpers/billing';
function formatName(name: string, limit: number = 19) {
return formatNameHelper(name, limit, $isSmallViewport);
}
@@ -1,6 +1,5 @@
<script lang="ts">
import { page } from '$app/state';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { HeaderAlert } from '$lib/layout';
@@ -10,6 +9,7 @@
import { backupsBannerId, showPolicyAlert } from '$lib/stores/database';
import { IconX } from '@appwrite.io/pink-icons-svelte';
import { Icon } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
function handleClose() {
showPolicyAlert.set(false);
@@ -18,14 +18,14 @@
</script>
{#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)}
{@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
{@const isFree = isFreePlan($organization?.billingPlan)}
{@const subtitle = isFreePlan
{@const subtitle = isFree
? 'Upgrade your plan to ensure your data stays safe and backed up'
: 'Protect your data by quickly adding a backup policy'}
{@const ctaText = isFreePlan ? 'Upgrade plan' : 'Create policy'}
{@const ctaURL = isFreePlan ? $upgradeURL : `${page.url.pathname}/backups`}
{@const ctaText = isFree ? 'Upgrade plan' : 'Create policy'}
{@const ctaURL = isFree ? $upgradeURL : `${page.url.pathname}/backups`}
<HeaderAlert type="warning" title="Your database has no backup policy">
<svelte:fragment>{subtitle}</svelte:fragment>
@@ -35,7 +35,7 @@
href={ctaURL}
secondary
fullWidthMobile
event={isFreePlan ? 'backup_banner_upgrade' : 'backup_banner_add'}>
event={isFree ? 'backup_banner_upgrade' : 'backup_banner_add'}>
<span class="text">{ctaText}</span>
</Button>
+3 -2
View File
@@ -4,7 +4,7 @@
import { onMount } from 'svelte';
import { isCloud, isSelfHosted } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups';
import { goto, invalidate } from '$app/navigation';
import { page } from '$app/state';
@@ -13,6 +13,7 @@
import { getProjectId } from '$lib/helpers/project';
import { toLocaleDate } from '$lib/helpers/date';
import { Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
const backupRestoreItems: {
archives: Map<string, BackupArchive>;
@@ -125,7 +126,7 @@
onMount(() => {
// fast path: don't subscribe if org is on a free plan or is self-hosted.
if (isSelfHosted || (isCloud && $organization?.billingPlan === BillingPlan.FREE)) return;
if (isSelfHosted || (isCloud && isFreePlan($organization?.billingPlan))) return;
return realtime.forProject(page.params.region, 'console', (response) => {
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
@@ -2,14 +2,14 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { isFreePlan } from '$lib/helpers/billing';
import { HeaderAlert } from '$lib/layout';
import { hideBillingHeaderRoutes, readOnly, tierToPlan, upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
</script>
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
{#if $organization?.$id && isFreePlan($organization?.billingPlan) && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
<HeaderAlert
type="error"
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
@@ -2,7 +2,7 @@
import { base } from '$app/paths';
import { page } from '$app/state';
import { Click, trackEvent } from '$lib/actions/analytics';
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { activeHeaderAlert } from '$routes/(console)/store';
@@ -10,6 +10,7 @@
import { isSmallViewport } from '$lib/stores/viewport';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing';
let show = true;
@@ -24,7 +25,7 @@
}
</script>
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes(base + '/account')}
{#if show && $organization?.$id && isFreePlan($organization?.billingPlan) && !page.url.pathname.includes(base + '/account')}
<GradientBanner on:close={handleClose}>
<Layout.Stack
gap="m"
+2 -1
View File
@@ -7,6 +7,7 @@
import { abbreviateNumber, formatCurrency, isWithinSafeRange } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import { Table, Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let show = false;
export let org: Organization;
@@ -17,7 +18,7 @@
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
: org?.billingNextInvoiceDate;
$: isFree = org.billingPlan === BillingPlan.FREE;
$: isFree = isFreePlan(org.billingPlan);
// equal or above means unlimited!
const getCorrectSeatsCountValue = (count: number): string | number => {
+2 -2
View File
@@ -3,15 +3,15 @@
import { upgradeURL } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
import Button from '$lib/elements/forms/button.svelte';
import { Badge, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
</script>
<Base>
<Layout.Stack gap="s">
{#if isCloud}
{#if $organization?.billingPlan !== BillingPlan.FREE}
{#if !isFreePlan($organization?.billingPlan)}
<Typography.Text variant="m-600">Roles</Typography.Text>
<Typography.Text>Owner, Developer, Editor, Analyst and Billing.</Typography.Text>
<Typography.Text>
+11
View File
@@ -0,0 +1,11 @@
import { BillingPlan } from '@appwrite.io/console';
export function isFreePlan(plan: BillingPlan | string): boolean {
switch (plan) {
case BillingPlan.Imaginebasic:
case BillingPlan.Tier0:
return true;
default:
return false;
}
}
+6 -7
View File
@@ -1,17 +1,16 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { isFreePlan } from '$lib/helpers/billing';
import { tierToPlan } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { Tooltip } from '@appwrite.io/pink-svelte';
export let title: string;
export let tooltipContent =
$organization?.billingPlan === BillingPlan.FREE
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization?.billingPlan)?.name
} plan`;
export let tooltipContent = isFreePlan($organization?.billingPlan)
? `Upgrade to add more ${title.toLocaleLowerCase()}`
: `You've reached the ${title.toLocaleLowerCase()} limit for the ${
tierToPlan($organization?.billingPlan)?.name
} plan`;
export let disabled: boolean;
export let buttonText: string;
export let buttonMethod: () => void | Promise<void> = () => {};
+5 -4
View File
@@ -21,6 +21,7 @@
import { ContainerButton } from '.';
import { goto } from '$app/navigation';
import { Layout, Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let title: string;
export let serviceId = title.toLocaleLowerCase() as PlanServices;
@@ -68,7 +69,7 @@
// these can be organization level limitations as well.
// we need to migrate this sometime later, but soon!
$: hasProjectLimitation =
checkForProjectLimitation(serviceId) && $organization?.billingPlan === BillingPlan.FREE;
checkForProjectLimitation(serviceId) && isFreePlan($organization?.billingPlan);
$: hasUsageFees = hasProjectLimitation
? checkForUsageFees($organization?.billingPlan, serviceId)
: false;
@@ -99,7 +100,7 @@
{#if services.length}
<slot name="alert" {limit} {tier} {title} {upgradeMethod} {hasUsageFees} {services}>
{#if $organization?.billingPlan !== BillingPlan.FREE && hasUsageFees}
{#if !isFreePlan($organization?.billingPlan) && hasUsageFees}
<Alert.Inline status="info">
<span class="text">
You've reached the {services} limit for the {tier} plan.
@@ -149,7 +150,7 @@
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per project on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.FREE}<Link
{#if isFreePlan($organization?.billingPlan)}<Link
href={$upgradeURL}
event="organization_upgrade"
eventData={{ from: 'button', source: 'resource_limit_tag' }}
@@ -169,7 +170,7 @@
<p class="text">
You are limited to {limit}
{title.toLocaleLowerCase()} per organization on the {tier} plan.
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Link href={$upgradeURL}>Upgrade</Link>
for additional {title.toLocaleLowerCase()}.
{/if}
+2 -2
View File
@@ -14,13 +14,13 @@
import SideNavigation from '$lib/layout/navigation.svelte';
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
import { isSidebarOpen, noWidthTransition } from '$lib/stores/sidebar';
import { BillingPlan } from '$lib/constants';
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { getSidebarState, isInDatabasesRoute, updateSidebarState } from '$lib/helpers/sidebar';
import { isTabletViewport } from '$lib/stores/viewport';
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
import { app } from '$lib/stores/app';
import { isFreePlan } from '$lib/helpers/billing';
export let showHeader = true;
export let showFooter = true;
@@ -165,7 +165,7 @@
return {
name: org.name,
$id: org.$id,
showUpgrade: billingPlan === BillingPlan.FREE,
showUpgrade: isFreePlan(billingPlan),
tierName: isCloud ? tierToPlan(billingPlan).name : null,
isSelected: $organization?.$id === org.$id
};
+10 -2
View File
@@ -1,6 +1,8 @@
import { asset, resolve } from '$app/paths';
import type { ResolvedPathname } from '$app/types';
import { PUBLIC_CONSOLE_PROFILE } from '$env/static/public';
import { Platform } from '@appwrite.io/console';
import { env } from '$env/dynamic/public';
import { BillingPlan } from '@appwrite.io/console';
export const enum ProfileMode {
STUDIO = 'studio',
@@ -10,6 +12,8 @@ export const enum ProfileMode {
export type Profile = {
id: ProfileMode;
platform: string;
organizationPlatform: Platform;
freeTier: BillingPlan;
logo: {
src: {
dark: string;
@@ -42,6 +46,8 @@ export type Profile = {
export const base: Profile = {
id: ProfileMode.CONSOLE,
platform: 'Appwrite',
organizationPlatform: Platform.Appwrite,
freeTier: BillingPlan.Tier0,
logo: {
src: {
dark: asset('/images/appwrite-logo-dark.svg'),
@@ -79,6 +85,8 @@ export const base: Profile = {
export const studio: Profile = {
id: ProfileMode.STUDIO,
platform: 'Imagine',
organizationPlatform: Platform.Imagine,
freeTier: BillingPlan.Imaginebasic,
logo: {
src: {
dark: asset('/images/imagine-logo-dark.svg'),
@@ -114,7 +122,7 @@ export const studio: Profile = {
};
const resolver = $derived(() => {
switch (PUBLIC_CONSOLE_PROFILE) {
switch (env.PUBLIC_CONSOLE_PROFILE) {
case 'studio':
return studio;
default:
+3 -1
View File
@@ -1,6 +1,6 @@
import type { Tier } from '$lib/stores/billing';
import type { Campaign } from '$lib/stores/campaigns';
import type { Client, Models } from '@appwrite.io/console';
import type { Client, Models, Platform } from '@appwrite.io/console';
import type { PaymentMethod } from '@stripe/stripe-js';
import type { Organization, OrganizationError, OrganizationList } from '../stores/organization';
@@ -490,6 +490,7 @@ export class Billing {
name: string,
billingPlan: string,
paymentMethodId: string,
platform: Platform,
billingAddressId: string = null,
couponId: string = null,
invites: Array<string> = [],
@@ -500,6 +501,7 @@ export class Billing {
const params = {
organizationId,
name,
platform,
billingPlan,
paymentMethodId,
billingAddressId,
+14 -8
View File
@@ -41,6 +41,8 @@ import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budg
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
import EnterpriseTrial from '$routes/(console)/organization-[organization]/enterpriseTrial.svelte';
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing';
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
@@ -321,7 +323,7 @@ export function calculateEnterpriseTrial(org: Organization) {
}
export function calculateTrialDay(org: Organization) {
if (org?.billingPlan === BillingPlan.FREE) return false;
if (isFreePlan(org?.billingPlan)) return false;
const endDate = new Date(org?.billingStartDate);
const today = new Date();
@@ -341,7 +343,7 @@ export async function checkForProjectsLimit(org: Organization, orgProjectCount?:
const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id);
if (!plan) return;
if (plan.$id !== BillingPlan.FREE) return;
if (!isFreePlan(plan.$id)) return;
if (!org.projects) return;
if (org.projects.length > 0) return;
@@ -373,7 +375,7 @@ export async function checkForUsageLimit(org: Organization) {
readOnly.set(false);
return;
}
if (org?.billingPlan !== BillingPlan.FREE) {
if (!isFreePlan(org?.billingPlan)) {
const { budgetLimit } = org?.billingLimits ?? {};
if (budgetLimit && budgetLimit >= 100) {
@@ -453,7 +455,7 @@ export async function checkForUsageLimit(org: Organization) {
}
export async function checkPaymentAuthorizationRequired(org: Organization) {
if (org.billingPlan === BillingPlan.FREE) return;
if (isFreePlan(org.billingPlan)) return;
const invoices = await sdk.forConsole.billing.listInvoices(org.$id, [
Query.equal('status', 'requires_authentication')
@@ -555,9 +557,10 @@ export async function checkForMandate(org: Organization) {
export async function checkForMissingPaymentMethod() {
const orgs = await sdk.forConsole.billing.listOrganization([
Query.notEqual('billingPlan', BillingPlan.FREE),
Query.notEqual('billingPlan', resolvedProfile.freeTier),
Query.isNull('paymentMethodId'),
Query.isNull('backupPaymentMethodId')
Query.isNull('backupPaymentMethodId'),
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
if (orgs?.total) {
orgMissingPaymentMethod.set(orgs.teams[0]);
@@ -572,8 +575,10 @@ export async function checkForMissingPaymentMethod() {
// Display upgrade banner for new users after 1 week for 30 days
export async function checkForNewDevUpgradePro(org: Organization) {
if (resolvedProfile.id !== ProfileMode.CONSOLE) return;
// browser or plan check.
if (!browser || org?.billingPlan !== BillingPlan.FREE) return;
if (!browser || !isFreePlan(org?.billingPlan)) return;
// already dismissed by user!
if (localStorage.getItem('newDevUpgradePro')) return;
@@ -588,7 +593,8 @@ export async function checkForNewDevUpgradePro(org: Organization) {
if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return;
const organizations = await sdk.forConsole.billing.listOrganization([
Query.notEqual('billingPlan', BillingPlan.FREE)
Query.notEqual('billingPlan', resolvedProfile.freeTier),
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
if (organizations?.total) return;
+5 -1
View File
@@ -7,6 +7,7 @@ import BackupDatabase from '$lib/components/backupDatabaseAlert.svelte';
import { shouldShowNotification } from '$lib/helpers/notifications';
import { isCloud } from '$lib/system';
import { currentPlan } from '$lib/stores/organization';
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
export const database = derived(page, ($page) => $page.data?.database as Models.Database);
@@ -19,7 +20,10 @@ export async function checkForDatabaseBackupPolicies(
projectId: string,
database: Models.Database
) {
// fast path: return if user dismissed the banner
// fast path: not the valid profile to show this!
if (resolvedProfile.id !== ProfileMode.CONSOLE) return;
// fast path: return if user dismissed the banner.
if (!shouldShowNotification(backupsBannerId)) return;
let total = 0;
+2 -1
View File
@@ -1,6 +1,6 @@
import { page } from '$app/stores';
import { derived, writable } from 'svelte/store';
import type { Models } from '@appwrite.io/console';
import type { Models, Platform } from '@appwrite.io/console';
import type { Tier } from './billing';
import type { Plan } from '$lib/sdk/billing';
@@ -35,6 +35,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
status: string;
remarks: string;
projects: string[];
platform: Platform;
};
export type OrganizationList = {
+4 -2
View File
@@ -22,7 +22,8 @@ import {
Tokens,
TablesDB,
Domains,
Realtime
Realtime,
Organizations
} from '@appwrite.io/console';
import { Billing } from '../sdk/billing';
import { Backups } from '../sdk/backups';
@@ -95,7 +96,8 @@ function createConsoleSdk(client: Client) {
sites: new Sites(client),
domains: new Domains(client),
storage: new Storage(client),
realtime: new Realtime(client)
realtime: new Realtime(client),
organizations: new Organizations(client)
};
}
+3 -4
View File
@@ -1,4 +1,3 @@
import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_AI_SERVICE_BASE_URL } from '$env/static/public';
import { env } from '$env/dynamic/public';
import { app } from '$lib/stores/app';
import { get } from 'svelte/store';
@@ -287,9 +286,9 @@ export async function initImagine(
if (!configInitialized) {
initImagineConfig(
{
AI_SERVICE_ENDPOINT: PUBLIC_AI_SERVICE_BASE_URL,
APPWRITE_ENDPOINT: PUBLIC_APPWRITE_ENDPOINT,
APPWRITE_SITES_BASE_URL: 'https://stage.appwrite.network'
AI_SERVICE_ENDPOINT: env.PUBLIC_AI_SERVICE_BASE_URL,
APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT,
APPWRITE_SITES_BASE_URL: ''
},
{
initialTheme: get(app).themeInUse,
+3 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { BillingPlan, INTERVAL } from '$lib/constants';
import { INTERVAL } from '$lib/constants';
import Footer from '$lib/layout/footer.svelte';
import Shell from '$lib/layout/shell.svelte';
@@ -58,6 +58,7 @@
} from '@appwrite.io/pink-icons-svelte';
import type { LayoutData } from './$types';
import Studio from '$lib/studio/studio.svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let data: LayoutData;
@@ -306,7 +307,7 @@
checkForMarkedForDeletion(org);
await checkForNewDevUpgradePro(org);
if (org?.billingPlan !== BillingPlan.FREE) {
if (!isFreePlan(org.billingPlan)) {
await paymentExpired(org);
await checkPaymentAuthorizationRequired(org);
await checkForMandate(org);
@@ -22,6 +22,7 @@
import { goto } from '$app/navigation';
import { Icon, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { isFreePlan } from '$lib/helpers/billing';
const {
data
@@ -39,14 +40,14 @@
function isOrganizationOnTrial(organization: Organization): boolean {
if (!organization?.billingTrialStartDate) return false;
if ($daysLeftInTrial <= 0) return false;
if (organization.billingPlan === BillingPlan.FREE) return false;
if (isFreePlan(organization.billingPlan)) return false;
return !!$plansInfo.get(organization.billingPlan)?.trialDays;
}
function isNonPayingOrganization(organization: Organization): boolean {
return (
organization?.billingPlan === BillingPlan.FREE ||
isFreePlan(organization?.billingPlan) ||
organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
);
}
@@ -4,13 +4,19 @@ import { getLimit, getPage, pageToOffset } from '$lib/helpers/load';
import { CARD_LIMIT } from '$lib/constants';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const load: PageLoad = async ({ url, route }) => {
const page = getPage(url);
const limit = getLimit(url, route, CARD_LIMIT);
const offset = pageToOffset(page, limit);
const queries = [Query.offset(offset), Query.limit(limit), Query.orderDesc('')];
const queries = [
Query.offset(offset),
Query.limit(limit),
Query.orderDesc(''),
Query.equal('platform', resolvedProfile.organizationPlatform)
];
const organizations = !isCloud
? await sdk.forConsole.teams.list({ queries })
@@ -135,6 +135,7 @@
name,
billingPlan,
paymentMethodId,
resolvedProfile.organizationPlatform,
null,
couponData.code ? couponData.code : null,
collaborators,
+4
View File
@@ -3,6 +3,7 @@ import { isSameDay } from '$lib/helpers/date';
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
import DbOperatorsDark from '$lib/images/promos/db-operators-dark.png';
import DbOperatorsLight from '$lib/images/promos/db-operators-light.png';
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
const listOfPromotions: BottomModalAlertItem[] = [];
@@ -30,6 +31,9 @@ if (isCloud) {
}
export function addBottomModalAlerts() {
// fast path: not the valid profile to show this!
if (resolvedProfile.id !== ProfileMode.CONSOLE) return;
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
}
@@ -21,6 +21,7 @@
import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte';
import { onMount } from 'svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing.js';
export let data;
@@ -108,12 +109,13 @@
try {
let org: Organization | OrganizationError;
if (selectedPlan === BillingPlan.FREE) {
if (isFreePlan(selectedPlan)) {
org = await sdk.forConsole.billing.createOrganization(
ID.unique(),
name,
BillingPlan.FREE,
resolvedProfile.freeTier,
null,
resolvedProfile.organizationPlatform,
null
);
} else {
@@ -122,6 +124,7 @@
name,
selectedPlan,
paymentMethodId,
resolvedProfile.organizationPlatform,
null,
selectedCoupon?.code,
collaborators,
@@ -205,7 +208,7 @@
<PlanSelection bind:billingPlan={selectedPlan} isNewOrg />
</Layout.Stack>
</Fieldset>
{#if selectedPlan !== BillingPlan.FREE}
{#if !isFreePlan(selectedPlan)}
<Fieldset legend="Payment">
<Layout.Stack gap="s" alignItems="flex-start">
<SelectPaymentMethod
@@ -242,7 +245,7 @@
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
{#if selectedPlan !== BillingPlan.FREE}
{#if !isFreePlan(selectedPlan)}
<EstimatedTotalBox
billingPlan={selectedPlan}
{collaborators}
@@ -25,8 +25,9 @@
organization = await sdk.forConsole.billing.createOrganization(
ID.unique(),
organizationName,
BillingPlan.FREE,
resolvedProfile.freeTier,
null,
resolvedProfile.organizationPlatform,
null
);
@@ -44,8 +44,9 @@ export const load: PageLoad = async ({ parent }) => {
const org = await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal projects',
BillingPlan.FREE,
resolvedProfile.freeTier,
null,
resolvedProfile.organizationPlatform,
null
);
trackEvent(Submit.OrganizationCreate, {
@@ -6,6 +6,7 @@ import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { isCloud } from '$lib/system';
import { base } from '$app/paths';
import { isFreePlan } from '$lib/helpers/billing';
export const load: PageLoad = async ({ parent, depends }) => {
const { organization, scopes, currentPlan, countryList, locale } = await parent();
@@ -54,7 +55,7 @@ export const load: PageLoad = async ({ parent, depends }) => {
const areCreditsSupported = isCloud
? (currentPlan?.supportsCredits ??
(organization.billingPlan !== BillingPlan.FREE &&
(!isFreePlan(organization.billingPlan) &&
organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION))
: false;
@@ -10,13 +10,13 @@
import { Button } from '$lib/elements/forms';
import AddCreditModal from './addCreditModal.svelte';
import { formatCurrency } from '$lib/helpers/numbers';
import { BillingPlan } from '$lib/constants';
import { Click, trackEvent } from '$lib/actions/analytics';
import { upgradeURL } from '$lib/stores/billing';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Alert, Badge, Icon, Link, Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let areCreditsSupported: boolean;
@@ -82,7 +82,7 @@
}
</script>
<CardGrid hideFooter={$organization?.billingPlan !== BillingPlan.FREE}>
<CardGrid hideFooter={!isFreePlan($organization?.billingPlan)}>
<svelte:fragment slot="title">
{!areCreditsSupported ? 'Credits' : 'Available credit'}
</svelte:fragment>
@@ -172,7 +172,7 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Button
secondary
href={$upgradeURL}
@@ -13,6 +13,7 @@
import { IconTrash } from '@appwrite.io/pink-icons-svelte';
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import type { Plan } from '$lib/sdk/billing';
import { isFreePlan } from '$lib/helpers/billing';
export let organization: Organization;
export let currentPlan: Plan;
@@ -148,7 +149,7 @@
<svelte:fragment slot="actions">
<Form onSubmit={updateBudget}>
{#if organization?.billingPlan === BillingPlan.FREE || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
{#if isFreePlan(organization?.billingPlan) || organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<Button
secondary
href={$upgradeURL}
@@ -2,7 +2,7 @@
import { invalidate } from '$app/navigation';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputNumber, InputSwitch } from '$lib/elements/forms';
import { showUsageRatesModal, upgradeURL } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
@@ -11,6 +11,7 @@
import { Alert, Link } from '@appwrite.io/pink-svelte';
import BudgetAlert from './budgetAlert.svelte';
import type { Plan } from '$lib/sdk/billing';
import { isFreePlan } from '$lib/helpers/billing';
export let currentPlan: Plan;
export let organization: Organization;
@@ -89,7 +90,7 @@
</svelte:fragment>
<svelte:fragment slot="actions">
{#if organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan(organization?.billingPlan)}
<Button
secondary
href={$upgradeURL}
@@ -4,9 +4,10 @@
import { sdk } from '$lib/stores/sdk';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import Confirm from '$lib/components/confirm.svelte';
import { Typography } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let showDelete = false;
export let isBackup = false;
@@ -16,7 +17,7 @@
let error: string;
async function removeDefaultMethod() {
if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return;
if (!isFreePlan($organization?.billingPlan) && !hasOtherMethod) return;
try {
await sdk.forConsole.billing.removeOrganizationPaymentMethod($organization.$id);
@@ -35,7 +36,7 @@
}
}
async function removeBackuptMethod() {
if ($organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod) return;
if (!isFreePlan($organization?.billingPlan) && !hasOtherMethod) return;
showDelete = false;
try {
@@ -3,7 +3,7 @@
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, CreditCardBrandImage, CreditCardInfo } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { type Organization } from '$lib/stores/organization';
import { Button } from '$lib/elements/forms';
@@ -33,6 +33,7 @@
IconSwitchHorizontal,
IconTrash
} from '@appwrite.io/pink-icons-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let organization: Organization;
export let methods: PaymentList;
@@ -332,5 +333,5 @@
bind:showDelete
{hasOtherMethod}
isBackup={isSelectedBackup}
disabled={organization?.billingPlan !== BillingPlan.FREE && !hasOtherMethod} />
disabled={!isFreePlan(organization?.billingPlan) && !hasOtherMethod} />
{/if}
@@ -22,6 +22,7 @@
import { isSmallViewport, isTabletViewport } from '$lib/stores/viewport';
import CancelDowngradeModel from './cancelDowngradeModal.svelte';
import { IconTag } from '@appwrite.io/pink-icons-svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let currentPlan: Plan;
export let nextPlan: Plan | null = null;
@@ -497,7 +498,7 @@
<!-- Actions -->
<div class="actions-container">
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
{#if isFreePlan($organization?.billingPlan) || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div
class="u-flex u-cross-center u-gap-8 u-flex-wrap u-width-full-line u-main-end actions-mobile">
{#if !currentPlan?.usagePerProject}
@@ -20,6 +20,7 @@
} from '@appwrite.io/pink-svelte';
import { IconInfo, IconTag } from '@appwrite.io/pink-icons-svelte';
import CancelDowngradeModel from './cancelDowngradeModal.svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let currentPlan: Plan;
export let currentInvoice: Invoice | undefined = undefined;
@@ -139,7 +140,7 @@
</Layout.Stack>
{/if}
{#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
{#if !isFreePlan($organization?.billingPlan) && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
<Divider />
<Layout.Stack direction="row" justifyContent="space-between">
<Typography.Text color="--fgcolor-neutral-primary" variant="m-500">
@@ -169,7 +170,7 @@
</Card.Base>
</svelte:fragment>
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
{#if isFreePlan($organization?.billingPlan) || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION}
<div
class="u-flex u-flex-vertical-mobile u-cross-center u-gap-16 u-flex-wrap u-width-full-line u-main-end">
{#if !currentPlan?.usagePerProject}
@@ -1,17 +1,17 @@
<script lang="ts">
import { page } from '$app/state';
import { BillingPlan } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { HeaderAlert } from '$lib/layout';
import { hideBillingHeaderRoutes, readOnly, showBudgetAlert } from '$lib/stores/billing';
import { base } from '$app/paths';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing';
$: redirectUrl = `${base}/organization-${$organization.$id}/billing#update-budget`;
</script>
{#if $showBudgetAlert && $organization?.$id && $organization?.billingPlan !== BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
{#if $showBudgetAlert && $organization?.$id && !isFreePlan($organization?.billingPlan) && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
<HeaderAlert type="error" title="Budget limit reached">
<svelte:fragment>
This organization has reached its budget limit and is now blocked. To continue using
@@ -38,6 +38,7 @@
import type { Models } from '@appwrite.io/console';
import { toLocaleDate } from '$lib/helpers/date';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing.js';
export let data;
@@ -315,7 +316,7 @@
$: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan?.order;
$: isButtonDisabled =
$organization?.billingPlan === selectedPlan ||
(isDowngrade && selectedPlan === BillingPlan.FREE && data.hasFreeOrgs);
(isDowngrade && isFreePlan(selectedPlan) && data.hasFreeOrgs);
</script>
<svelte:head>
@@ -345,7 +346,7 @@
<PlanSelection bind:billingPlan={selectedPlan} selfService={data.selfService} />
{#if isDowngrade && selectedPlan === BillingPlan.FREE && data.hasFreeOrgs}
{#if isDowngrade && isFreePlan(selectedPlan) && data.hasFreeOrgs}
<Alert.Inline
status="warning"
title="You can only have one free organization per account">
@@ -377,7 +378,7 @@
>you will be charged {price} monthly for {extraMembers} team members.</b>
This will be reflected in your next invoice.
</Alert.Inline>
{:else if selectedPlan === BillingPlan.FREE}
{:else if isFreePlan(selectedPlan)}
<Alert.Inline
status="error"
title={`Your organization will switch to ${tierToPlan(selectedPlan).name} plan on ${toLocaleDate(
@@ -445,7 +446,7 @@
id="members" />
</Fieldset>
{/if}
{#if isDowngrade && selectedPlan === BillingPlan.FREE && !data.hasFreeOrgs}
{#if isDowngrade && isFreePlan(selectedPlan) && !data.hasFreeOrgs}
<Fieldset legend="Feedback">
<Layout.Stack gap="xl">
<InputSelect
@@ -467,7 +468,7 @@
</Layout.Stack>
</Form>
<svelte:fragment slot="aside">
{#if selectedPlan !== BillingPlan.FREE && data.organization.billingPlan !== selectedPlan && data.organization.billingPlan !== BillingPlan.CUSTOM}
{#if !isFreePlan(selectedPlan) && data.organization.billingPlan !== selectedPlan && data.organization.billingPlan !== BillingPlan.CUSTOM}
<EstimatedTotalBox
{collaborators}
{isDowngrade}
@@ -2,6 +2,7 @@ import type { PageLoad } from './$types';
import type { Organization } from '$lib/stores/organization';
import { BillingPlan, Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { isFreePlan } from '$lib/helpers/billing';
export const load: PageLoad = async ({ depends, parent }) => {
const { members, currentPlan, organizations } = await parent();
@@ -24,8 +25,8 @@ export const load: PageLoad = async ({ depends, parent }) => {
}
const selfService = currentPlan?.selfService ?? true;
const hasFreeOrgs = organizations.teams?.some(
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
const hasFreeOrgs = organizations.teams?.some((org) =>
isFreePlan((org as Organization)?.billingPlan)
);
return {
@@ -1,13 +1,17 @@
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import { Query } from '@appwrite.io/console';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const load = async ({ parent, depends }) => {
depends(Dependencies.DOMAINS);
const organizations = !isCloud
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization();
: await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
const { domain } = await parent();
return {
@@ -26,6 +26,7 @@
import { IconGithub, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Badge, Icon, Layout, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import { isFreePlan } from '$lib/helpers/billing';
let areMembersLimited: boolean = $state(false);
@@ -116,7 +117,7 @@
<Badge variant="secondary" content="Education">
<Icon icon={IconGithub} size="s" slot="start" />
</Badge>
{:else if isCloud && organization?.billingPlan === BillingPlan.FREE}
{:else if isCloud && isFreePlan(organization?.billingPlan)}
<Badge variant="secondary" content="Free"></Badge>
{/if}
@@ -154,7 +155,7 @@
</Button>
</div>
<div slot="tooltip">
{organization?.billingPlan === BillingPlan.FREE
{isFreePlan(organization?.billingPlan)
? 'Upgrade to add more members'
: `You've reached the members limit for the ${
tierToPlan(organization?.billingPlan)?.name
@@ -34,8 +34,8 @@
ActionMenu,
Tooltip
} from '@appwrite.io/pink-svelte';
import { BillingPlan } from '$lib/constants';
import { tierToPlan } from '$lib/stores/billing';
import { isFreePlan } from '$lib/helpers/billing';
export let data;
@@ -46,7 +46,7 @@
// Calculate if button should be disabled and tooltip should show
$: memberCount = data.organizationMembers?.total ?? 0;
$: isFreeWithMembers = $organization?.billingPlan === BillingPlan.FREE && memberCount >= 1;
$: isFreeWithMembers = isFreePlan($organization?.billingPlan) && memberCount >= 1;
$: isButtonDisabled = isCloud ? isFreeWithMembers : false;
const resend = async (member: Models.Membership) => {
@@ -89,7 +89,7 @@
</ConsoleButton>
</div>
<div slot="tooltip">
{$organization?.billingPlan === BillingPlan.FREE
{isFreePlan($organization?.billingPlan)
? 'Upgrade to add more members'
: `You've reached the members limit for the ${
tierToPlan($organization?.billingPlan)?.name
@@ -24,6 +24,7 @@
import { IconChartSquareBar, IconInfo } from '@appwrite.io/pink-icons-svelte';
import { onMount } from 'svelte';
import type { UsageProjectInfo } from '../../store';
import { isFreePlan } from '$lib/helpers/billing';
export let data;
@@ -61,7 +62,7 @@
<div class="u-flex u-cross-center u-main-space-between">
<Typography.Title>Usage</Typography.Title>
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Button
href={$upgradeURL}
on:click={() => {
@@ -104,7 +105,7 @@
</Link.Anchor>
{/if}
</p>
{:else if $organization.billingPlan === BillingPlan.FREE}
{:else if isFreePlan($organization.billingPlan)}
<p class="text">
If you exceed the limits of the Free plan, services for your organization's projects may
be disrupted.
@@ -10,6 +10,7 @@
import { IconInfo, IconPlus } from '@appwrite.io/pink-icons-svelte';
import { Icon, Layout, Table, Tooltip, Typography } from '@appwrite.io/pink-svelte';
import DualTimeView from '$lib/components/dualTimeView.svelte';
import { isFreePlan } from '$lib/helpers/billing';
export let members: Models.MembershipList;
@@ -21,7 +22,7 @@
<svelte:fragment slot="title">Members</svelte:fragment>
The number of members in your organization.
<svelte:fragment slot="aside">
{#if $organization.billingPlan !== BillingPlan.FREE}
{#if !isFreePlan($organization?.billingPlan)}
<div class="u-flex u-flex-vertical">
<Layout.Stack direction="row" justifyContent="space-between">
<Layout.Stack gap="s" direction="row" alignItems="center">
@@ -18,7 +18,6 @@
import { trackEvent, Click } from '$lib/actions/analytics';
import { type Models } from '@appwrite.io/console';
import { getServiceLimit, readOnly, upgradeURL } from '$lib/stores/billing';
import { BillingPlan } from '$lib/constants';
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { onMount, type ComponentType } from 'svelte';
import { canWriteProjects } from '$lib/stores/roles';
@@ -41,6 +40,7 @@
import ArchiveProject from '$lib/components/archiveProject.svelte';
import { resolvedProfile } from '$lib/profiles/index.svelte';
import type { PageData } from './$types';
import { isFreePlan } from '$lib/helpers/billing';
export let data: PageData;
@@ -201,7 +201,7 @@
</Alert.Inline>
{/if}
{#if isCloud && data.organization.billingPlan === BillingPlan.FREE && projectsToArchive.length === 0 && !freePlanAlertDismissed}
{#if isCloud && isFreePlan(data.organization.billingPlan) && projectsToArchive.length === 0 && !freePlanAlertDismissed}
<Alert.Inline dismissible on:dismiss={dismissFreePlanAlert}>
<Typography.Text
>Your Free plan includes up to 2 projects and limited resources. Upgrade to unlock
@@ -7,7 +7,6 @@
import { ID } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { isCloud } from '$lib/system';
import { BillingPlan } from '$lib/constants';
import { organization } from '$lib/stores/organization';
import { upgradeURL } from '$lib/stores/billing';
import CreatePolicy from './database-[database]/backups/createPolicy.svelte';
@@ -15,6 +14,7 @@
import { Alert, Icon, Tag } from '@appwrite.io/pink-svelte';
import { IconPencil } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
import { isFreePlan } from '$lib/helpers/billing';
export let showCreate = false;
let totalPolicies: UserBackupPolicy[] = [];
@@ -132,7 +132,7 @@
<CustomId bind:show={showCustomId} name="Database" bind:id autofocus={false} />
{#if isCloud}
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Alert.Inline title="This database won't be backed up" status="warning">
Upgrade your plan to ensure your data stays safe and backed up.
<svelte:fragment slot="actions">
@@ -107,7 +107,7 @@
try {
await sdk
.forProject(page.params.region, page.params.project)
.migrations.createCsvMigration({
.migrations.createCSVImport({
bucketId: file.bucketId,
fileId: file.$id,
resourceId: `${page.params.database}:${page.params.table}`,
@@ -1,8 +1,8 @@
<script lang="ts">
import { CustomId } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { InputSelect, InputText } from '$lib/elements/forms';
import Link from '$lib/elements/link.svelte';
import { isFreePlan } from '$lib/helpers/billing';
import { upgradeURL } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { isCloud } from '$lib/system';
@@ -78,7 +78,7 @@
disabled={specificationOptions.length < 1}
options={specificationOptions}
bind:value={specification} />
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Input.Helper state="default">
<Link href={$upgradeURL} variant="muted">Upgrade</Link> to Pro or Scale to adjust
your CPU and RAM beyond the default.
@@ -2,7 +2,7 @@
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -14,6 +14,7 @@
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { page } from '$app/state';
import { isFreePlan } from '$lib/helpers/billing';
export let func: Models.Function;
export let specs: Models.SpecificationList;
@@ -89,7 +90,7 @@
disabled={options.length < 1}
bind:value={specification}
{options} />
{#if isCloud && $organization.billingPlan === BillingPlan.FREE}
{#if isCloud && isFreePlan($organization?.billingPlan)}
<Alert.Inline title="Customizing specs available with Pro or Scale plans">
Upgrade to Pro or Scale to adjust your CPU and RAM beyond the default.
<svelte:fragment slot="actions">
@@ -6,7 +6,7 @@
import InputPassword from '$lib/elements/forms/inputPassword.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Click, Submit, trackError, trackEvent } from '$lib/actions/analytics';
import InputNumber from '$lib/elements/forms/inputNumber.svelte';
@@ -18,6 +18,7 @@
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
import { upgradeURL } from '$lib/stores/billing';
import { Link, Selector, Alert } from '@appwrite.io/pink-svelte';
import { isFreePlan } from '$lib/helpers/billing';
let enabled = false;
let senderName: string;
@@ -123,7 +124,7 @@
<Link.Anchor href={`${base}/project-${$project.region}-${$project.$id}/auth/templates`}
>here</Link.Anchor>
<svelte:fragment slot="aside">
{#if $organization.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Alert.Inline
status="info"
title="Custom SMTP is a Pro plan feature. Upgrade to enable custom SMTP sever.">
@@ -198,7 +199,7 @@
<svelte:fragment slot="actions">
<Button
submit
disabled={isButtonDisabled || $organization.billingPlan === BillingPlan.FREE}>
disabled={isButtonDisabled || isFreePlan($organization?.billingPlan)}>
Update
</Button>
</svelte:fragment>
@@ -15,6 +15,7 @@
import { Accordion, Icon, Layout, Link, Table, Typography } from '@appwrite.io/pink-svelte';
import { IconChartSquareBar } from '@appwrite.io/pink-icons-svelte';
import { page } from '$app/state';
import { isFreePlan } from '$lib/helpers/billing.js';
export let data;
@@ -52,7 +53,7 @@
<div class="u-flex u-cross-center u-main-space-between">
<Typography.Title>Usage</Typography.Title>
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<Button href={$upgradeURL}>
<span class="text">Upgrade</span>
</Button>
@@ -73,7 +74,7 @@
on:click={() => ($showUsageRatesModal = true)}
>Learn more about plan usage limits.</Link.Button>
</p>
{:else if $organization.billingPlan === BillingPlan.FREE}
{:else if isFreePlan($organization?.billingPlan)}
<p class="text">
If you exceed the limits of the {plan} plan, services for your projects may be disrupted.
<Link.Anchor href={$upgradeURL} class="link"
@@ -2,7 +2,7 @@
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -13,6 +13,7 @@
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { page } from '$app/state';
import { isFreePlan } from '$lib/helpers/billing';
export let site: Models.Site;
export let specs: Models.SpecificationList;
@@ -82,7 +83,7 @@
disabled={options.length < 1}
bind:value={specification}
{options} />
{#if isCloud && $organization.billingPlan === BillingPlan.FREE}
{#if isCloud && isFreePlan($organization?.billingPlan)}
<Alert.Inline title="Customizing specs available with Pro or Scale plans">
Upgrade to Pro or Scale to adjust your CPU and RAM beyond the default.
<svelte:fragment slot="actions">
@@ -2,7 +2,6 @@
import { Click, Submit, trackEvent } from '$lib/actions/analytics';
import { CardGrid } from '$lib/components';
import { Alert } from '@appwrite.io/pink-svelte';
import { BillingPlan } from '$lib/constants';
import { Button, Form, InputNumber, InputSelect } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
import { createByteUnitPair } from '$lib/helpers/unit';
@@ -12,6 +11,7 @@
import { updateBucket } from './+page.svelte';
import type { Plan } from '$lib/sdk/billing';
import type { Models } from '@appwrite.io/console';
import { isFreePlan } from '$lib/helpers/billing';
export let bucket: Models.Bucket;
export let currentPlan: Plan | null;
@@ -50,11 +50,11 @@
The {currentPlan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
Upgrade to allow files of a larger size.
{/if}
<svelte:fragment slot="actions">
{#if $organization?.billingPlan === BillingPlan.FREE}
{#if isFreePlan($organization?.billingPlan)}
<div class="alert-buttons u-flex">
<Button
text
+10 -5
View File
@@ -2,12 +2,12 @@ import { sdk } from '$lib/stores/sdk.js';
import { redirect } from '@sveltejs/kit';
import { base } from '$app/paths';
import { isCloud } from '$lib/system';
import { BillingPlan } from '$lib/constants';
import { ID, type Models } from '@appwrite.io/console';
import { ID, type Models, Query } from '@appwrite.io/console';
import type { OrganizationList } from '$lib/stores/organization';
import { redirectTo } from '$routes/store';
import type { PageLoad } from './$types';
import { getRepositoryInfo } from '$lib/helpers/github';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const load: PageLoad = async ({ parent, url }) => {
const { account } = await parent();
@@ -66,7 +66,9 @@ export const load: PageLoad = async ({ parent, url }) => {
// Get organizations
let organizations: Models.TeamList<Record<string, unknown>> | OrganizationList | undefined;
if (isCloud) {
organizations = await sdk.forConsole.billing.listOrganization();
organizations = await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
} else {
organizations = await sdk.forConsole.teams.list();
}
@@ -77,8 +79,9 @@ export const load: PageLoad = async ({ parent, url }) => {
await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal Projects',
BillingPlan.FREE,
resolvedProfile.freeTier,
null,
resolvedProfile.organizationPlatform,
null
);
} else {
@@ -89,7 +92,9 @@ export const load: PageLoad = async ({ parent, url }) => {
}
if (isCloud) {
organizations = await sdk.forConsole.billing.listOrganization();
organizations = await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
} else {
organizations = await sdk.forConsole.teams.list();
}
+11 -6
View File
@@ -2,12 +2,12 @@ import { sdk } from '$lib/stores/sdk.js';
import { redirect, error } from '@sveltejs/kit';
import { base } from '$app/paths';
import { isCloud } from '$lib/system';
import { BillingPlan } from '$lib/constants';
import { ID, type Models } from '@appwrite.io/console';
import { ID, type Models, Query } from '@appwrite.io/console';
import type { OrganizationList } from '$lib/stores/organization';
import { redirectTo } from '$routes/store';
import type { PageLoad } from './$types';
import { getRepositoryInfo } from '$lib/helpers/github';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const load: PageLoad = async ({ parent, url }) => {
const { account } = await parent();
@@ -83,7 +83,9 @@ export const load: PageLoad = async ({ parent, url }) => {
let organizations: Models.TeamList<Record<string, unknown>> | OrganizationList | undefined;
if (isCloud) {
organizations = await sdk.forConsole.billing.listOrganization();
organizations = await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
} else {
organizations = await sdk.forConsole.teams.list();
}
@@ -94,8 +96,9 @@ export const load: PageLoad = async ({ parent, url }) => {
await sdk.forConsole.billing.createOrganization(
ID.unique(),
'Personal Projects',
BillingPlan.FREE,
null
resolvedProfile.freeTier,
null,
resolvedProfile.organizationPlatform
);
} else {
await sdk.forConsole.teams.create({
@@ -106,7 +109,9 @@ export const load: PageLoad = async ({ parent, url }) => {
// Refetch organizations after creation
if (isCloud) {
organizations = await sdk.forConsole.billing.listOrganization();
organizations = await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
]);
} else {
organizations = await sdk.forConsole.teams.list();
}
@@ -1,11 +1,12 @@
import { BillingPlan } from '$lib/constants.js';
import { sdk } from '$lib/stores/sdk.js';
import { ID, type Models } from '@appwrite.io/console';
import { ID, type Models, Query } from '@appwrite.io/console';
import { isCloud } from '$lib/system.js';
import { error, redirect } from '@sveltejs/kit';
import type { OrganizationList } from '$lib/stores/organization.js';
import { redirectTo } from '$routes/store.js';
import { base } from '$app/paths';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const load = async ({ parent, url, params }) => {
const { account } = await parent();
@@ -39,7 +40,11 @@ export const load = async ({ parent, url, params }) => {
let organizations: Models.TeamList<Record<string, unknown>> | OrganizationList | undefined;
if (isCloud) {
organizations = account?.$id ? await sdk.forConsole.billing.listOrganization() : undefined;
organizations = account?.$id
? await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
])
: undefined;
} else {
organizations = account?.$id ? await sdk.forConsole.teams.list() : undefined;
}
+5 -2
View File
@@ -8,9 +8,10 @@ import type { LayoutLoad } from './$types';
import { redirectTo } from './store';
import { base, resolve } from '$app/paths';
import type { Account } from '$lib/stores/user';
import type { AppwriteException } from '@appwrite.io/console';
import { type AppwriteException, Query } from '@appwrite.io/console';
import { isCloud, VARS } from '$lib/system';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { resolvedProfile } from '$lib/profiles/index.svelte';
export const ssr = false;
@@ -42,7 +43,9 @@ export const load: LayoutLoad = async ({ depends, url, route }) => {
account: account,
organizations: !isCloud
? await sdk.forConsole.teams.list()
: await sdk.forConsole.billing.listOrganization()
: await sdk.forConsole.billing.listOrganization([
Query.equal('platform', resolvedProfile.organizationPlatform)
])
};
}