mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'feat-profiles' into fix-early-init-crash
This commit is contained in:
+4
-4
@@ -12,7 +12,7 @@
|
||||
"clean": "rm -rf node_modules && rm -rf .svelte_kit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write --cache .",
|
||||
"format": "prettier --cache --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
@@ -22,11 +22,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@406d8be",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@4838219",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@5b26bb8",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@865e2fc",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-svelte@33845eb",
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^10.25.0",
|
||||
|
||||
Generated
+274
-281
File diff suppressed because it is too large
Load Diff
@@ -1,103 +1,220 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { isFreePlan, isPaidPlan } from '$lib/helpers/billing';
|
||||
import { plansInfo, type Tier } from '$lib/stores/billing';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let downgrade = false;
|
||||
let {
|
||||
downgrade = false
|
||||
}: {
|
||||
downgrade?: boolean;
|
||||
} = $props();
|
||||
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
let selectedTab: Tier = $state(resolvedProfile.freeTier as Tier);
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
const currentPlan: Plan = $derived($plansInfo.get(selectedTab));
|
||||
const visiblePlans: Plan[] = $derived.by(() => {
|
||||
return page.data.plans.plans.filter((plan: Plan) => plan.$id !== BillingPlan.SCALE);
|
||||
});
|
||||
|
||||
const allTiers: Tier[] = [BillingPlan.FREE, BillingPlan.PRO, BillingPlan.SCALE];
|
||||
$: visibleTiers = allTiers.filter((tier) => tier !== BillingPlan.SCALE);
|
||||
const uniquePlans: Plan[] = $derived.by(() => {
|
||||
const map = new Map(visiblePlans.map((p) => [p.group ?? p.$id, p]));
|
||||
|
||||
return [...map.values()];
|
||||
});
|
||||
|
||||
const groupPlans = $derived.by(() => {
|
||||
const selected = visiblePlans.find((p: Plan) => p.$id === selectedTab);
|
||||
if (!selected) return [];
|
||||
|
||||
const groupKey = selected.group ?? selected.$id;
|
||||
return visiblePlans.filter((p: Plan) => (p.group ?? p.$id) === groupKey);
|
||||
});
|
||||
|
||||
// get the smallest in the group!
|
||||
const basePaidPlan = $derived.by(() => {
|
||||
return groupPlans.length > 1
|
||||
? [...groupPlans].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[0]
|
||||
: currentPlan;
|
||||
});
|
||||
|
||||
function getCleanPlanName(plan: Plan) {
|
||||
return plan?.name?.replace(resolvedProfile.platform, '');
|
||||
}
|
||||
|
||||
function pluralize(count: number, singular: string, plural?: string) {
|
||||
if (count === 1) return singular;
|
||||
return plural ?? `${singular}s`;
|
||||
}
|
||||
|
||||
function selectPlan(plan: string) {
|
||||
selectedTab = plan as Tier;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Base>
|
||||
<Layout.Stack>
|
||||
<Tabs.Root stretch let:root>
|
||||
{#each visibleTiers as tier}
|
||||
{#each uniquePlans as plan}
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
active={selectedTab === tier}
|
||||
on:click={() => (selectedTab = tier)}>
|
||||
{tierToPlan(tier).name}
|
||||
active={selectedTab === plan.$id}
|
||||
on:click={() => selectPlan(plan.$id)}>
|
||||
{getCleanPlanName(plan)}
|
||||
</Tabs.Item.Button>
|
||||
{/each}
|
||||
</Tabs.Root>
|
||||
|
||||
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{plan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{formatNum(plan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>
|
||||
Limited to {plan.bandwidth}GB bandwidth
|
||||
</li>
|
||||
<li>
|
||||
Limited to {plan.storage}GB storage
|
||||
</li>
|
||||
<li>
|
||||
Limited to {formatNum(plan.executions)} executions
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>Unlimited seats</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
<li>{formatNum(plan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles</li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
<Typography.Text variant="m-600">{getCleanPlanName(currentPlan)} plan</Typography.Text>
|
||||
|
||||
{#if resolvedProfile.id === ProfileMode.CONSOLE}
|
||||
{@render appwritePlanView()}
|
||||
{:else}
|
||||
{@render imaginePlanView()}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
{#snippet appwritePlanView()}
|
||||
{#if isFreePlan(selectedTab)}
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
Limited to {currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets}
|
||||
{pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions}
|
||||
{pluralize(currentPlan.functions, 'Function')} per project
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{currentPlan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{currentPlan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{formatNum(currentPlan.executions)} executions
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
Limited to {currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'Database')}, {currentPlan.buckets}
|
||||
{pluralize(currentPlan.buckets, 'Bucket')}, {currentPlan.functions}
|
||||
{pluralize(currentPlan.functions, 'Function')} per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>
|
||||
Limited to {currentPlan.bandwidth}GB bandwidth
|
||||
</li>
|
||||
<li>
|
||||
Limited to {currentPlan.storage}GB storage
|
||||
</li>
|
||||
<li>
|
||||
Limited to {formatNum(currentPlan.executions)} executions
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if isPaidPlan(selectedTab)}
|
||||
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>Unlimited seats</li>
|
||||
<li>{currentPlan.bandwidth}GB bandwidth</li>
|
||||
<li>{currentPlan.storage}GB storage</li>
|
||||
<li>{formatNum(currentPlan.executions)} executions</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles</li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet imaginePlanView()}
|
||||
{#if isFreePlan(selectedTab)}
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">
|
||||
{currentPlan?.limits?.dailyCredits ?? currentPlan?.limits?.credits} credits /
|
||||
day
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">{currentPlan.bandwidth}GB bandwidth</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">{currentPlan.storage}GB storage</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text"
|
||||
>{currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'database')} per project</span>
|
||||
</li>
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
<span class="icon-arrow-down u-color-text-danger" aria-hidden="true"></span>
|
||||
<span class="text">Community support</span>
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
{currentPlan?.limits?.dailyCredits ?? currentPlan?.limits?.credits} credits / day
|
||||
</li>
|
||||
<li>{currentPlan.bandwidth}GB bandwidth</li>
|
||||
<li>{currentPlan.storage}GB storage</li>
|
||||
<li>
|
||||
{currentPlan.databases}
|
||||
{pluralize(currentPlan.databases, 'database')} per project
|
||||
</li>
|
||||
<li>Community support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if isPaidPlan(selectedTab)}
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
{basePaidPlan?.limits?.credits?.toLocaleString()} credits / month
|
||||
{#if basePaidPlan?.usage?.credits}
|
||||
(${basePaidPlan.usage.credits.price} per {basePaidPlan.usage.credits.value} additional
|
||||
credits)
|
||||
{/if}
|
||||
</li>
|
||||
<li>{currentPlan.bandwidth / 1000}TB bandwidth</li>
|
||||
<li>{currentPlan.storage}GB storage</li>
|
||||
<li>Unlimited databases</li>
|
||||
<li>Removable {resolvedProfile.platform} branding</li>
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -6,43 +6,161 @@
|
||||
import { LabelCard } from '..';
|
||||
import type { Plan } from '$lib/sdk/billing';
|
||||
import { page } from '$app/state';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { InputSelect } from '$lib/elements/forms';
|
||||
|
||||
export let billingPlan: BillingPlan;
|
||||
export let disabled = false;
|
||||
export let isNewOrg = false;
|
||||
export let selfService = true;
|
||||
export let anyOrgFree = false;
|
||||
export let billingPlan: BillingPlan;
|
||||
|
||||
$: plans = Object.values(page.data.plans.plans) as Plan[];
|
||||
$: currentPlanInList = plans.some((plan) => plan.$id === $currentPlan?.$id);
|
||||
let selectedTiers: Record<string, string> = {};
|
||||
|
||||
// experiment to remove scale plan temporarily
|
||||
$: plansWithoutScale = plans.filter((plan) => plan.$id != BillingPlan.SCALE);
|
||||
type PlanGroup = {
|
||||
key: string;
|
||||
plans: Plan[];
|
||||
isGrouped: boolean;
|
||||
tierOptions?: { value: string; label: string; badge?: string }[];
|
||||
};
|
||||
|
||||
$: groupedPlans = (() => {
|
||||
const plans = Object.values(page.data.plans.plans) as Plan[];
|
||||
const groups = filterAndGroupPlans(plans);
|
||||
return buildPlanGroups(groups);
|
||||
})();
|
||||
|
||||
$: currentPlanInList = Object.values(page.data.plans.plans).some(
|
||||
(plan: Plan) => plan.$id === $currentPlan?.$id
|
||||
);
|
||||
|
||||
$: {
|
||||
for (const group of groupedPlans) {
|
||||
if (group.isGrouped) {
|
||||
selectedTiers[group.key] = getDefaultTierForGroup(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterAndGroupPlans(plans: Plan[]): Map<string, Plan[]> {
|
||||
const groups = new Map<string, Plan[]>();
|
||||
|
||||
for (const plan of plans) {
|
||||
if (plan.$id === BillingPlan.SCALE) continue;
|
||||
|
||||
const key = plan.group ?? plan.$id;
|
||||
const existing = groups.get(key);
|
||||
if (existing) {
|
||||
existing.push(plan);
|
||||
} else {
|
||||
groups.set(key, [plan]);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function buildPlanGroups(groups: Map<string, Plan[]>): PlanGroup[] {
|
||||
return Array.from(groups.entries())
|
||||
.map(([key, groupPlans]) => {
|
||||
const isGrouped = groupPlans.length > 1;
|
||||
const group: PlanGroup = { key, plans: groupPlans, isGrouped };
|
||||
|
||||
if (isGrouped) {
|
||||
group.tierOptions = buildTierOptions(groupPlans);
|
||||
}
|
||||
|
||||
return group;
|
||||
})
|
||||
.sort((a, b) => (a.plans[0]?.order ?? 0) - (b.plans[0]?.order ?? 0));
|
||||
}
|
||||
|
||||
function buildTierOptions(plans: Plan[]): { value: string; label: string; badge?: string }[] {
|
||||
return plans.map((plan) => ({
|
||||
value: plan.$id,
|
||||
label: getPlanLabel(plan),
|
||||
badge: $organization?.billingPlan === plan.$id ? 'Current' : undefined
|
||||
}));
|
||||
}
|
||||
|
||||
function getDefaultTierForGroup(group: PlanGroup): string {
|
||||
// billingPlan if in group, else org's current plan, else first plan
|
||||
const selectedGroupPlan = group.plans.find((p) => p.$id === billingPlan);
|
||||
if (selectedGroupPlan) {
|
||||
return selectedGroupPlan.$id;
|
||||
}
|
||||
|
||||
const orgCurrentPlan = group.plans.find((p) => p.$id === $organization?.billingPlan);
|
||||
if (orgCurrentPlan) {
|
||||
return orgCurrentPlan.$id;
|
||||
}
|
||||
|
||||
return selectedTiers[group.key] || group.plans[0].$id;
|
||||
}
|
||||
|
||||
function getPlanLabel(plan: Plan): string {
|
||||
const price = formatCurrency(plan?.price ?? 0);
|
||||
if (resolvedProfile.id === ProfileMode.STUDIO) {
|
||||
if (plan.limits?.dailyCredits) {
|
||||
return `${plan.limits.dailyCredits.toLocaleString()} daily credits for ${price} per month`;
|
||||
} else if (plan.limits?.credits) {
|
||||
return `${plan.limits.credits.toLocaleString()} credits per month for ${price}`;
|
||||
}
|
||||
}
|
||||
return `${price} per month`;
|
||||
}
|
||||
|
||||
function handleTierChange(group: PlanGroup) {
|
||||
billingPlan = selectedTiers[group.key] as BillingPlan;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout.Stack>
|
||||
{#each plansWithoutScale as plan}
|
||||
{#each groupedPlans as group (group.key)}
|
||||
{@const basePlan = group.plans[0]}
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={!selfService || (plan.$id === BillingPlan.FREE && anyOrgFree)}
|
||||
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
value={plan.$id}
|
||||
title={plan.name}>
|
||||
disabled={!selfService || (basePlan.$id === BillingPlan.FREE && anyOrgFree) || disabled}
|
||||
tooltipShow={basePlan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
value={group.isGrouped ? selectedTiers[group.key] || basePlan.$id : basePlan.$id}
|
||||
title={basePlan.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === plan.$id && !isNewOrg}
|
||||
{#if group.plans.some((plan) => $organization?.billingPlan === plan.$id) && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Typography.Caption variant="400">
|
||||
{plan.desc}
|
||||
</Typography.Caption>
|
||||
<Typography.Text>
|
||||
{@const isZeroPrice = (plan.price ?? 0) <= 0}
|
||||
{@const price = formatCurrency(plan.price ?? 0)}
|
||||
{isZeroPrice ? price : `${price} per month + usage`}
|
||||
</Typography.Text>
|
||||
|
||||
<Layout.Stack direction="column" gap="m">
|
||||
<Layout.Stack direction="column" gap="xxs">
|
||||
<Typography.Caption variant="400">
|
||||
{basePlan.desc}
|
||||
</Typography.Caption>
|
||||
|
||||
{#if !group.isGrouped}
|
||||
<Typography.Text>
|
||||
{@const isZeroPrice = (basePlan.price ?? 0) <= 0}
|
||||
{@const price = formatCurrency(basePlan.price ?? 0)}
|
||||
{#if resolvedProfile.id === ProfileMode.STUDIO}
|
||||
{getPlanLabel(basePlan)}
|
||||
{:else}
|
||||
{isZeroPrice ? price : getPlanLabel(basePlan)}
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if group.isGrouped}
|
||||
<InputSelect
|
||||
id="tier-{group.key}"
|
||||
bind:value={selectedTiers[group.key]}
|
||||
on:change={() => handleTierChange(group)}
|
||||
options={group.tierOptions} />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</LabelCard>
|
||||
{/each}
|
||||
|
||||
{#if $currentPlan && !currentPlanInList}
|
||||
<LabelCard
|
||||
name="plan"
|
||||
@@ -54,14 +172,20 @@
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Typography.Caption variant="400">
|
||||
{$currentPlan.desc}
|
||||
</Typography.Caption>
|
||||
|
||||
<Typography.Text>
|
||||
{@const isZeroPrice = ($currentPlan?.price ?? 0) <= 0}
|
||||
{@const price = formatCurrency($currentPlan?.price ?? 0)}
|
||||
{isZeroPrice ? price : `${price} per month + usage`}
|
||||
{isZeroPrice ? price : getPlanLabel($currentPlan)}
|
||||
</Typography.Text>
|
||||
</LabelCard>
|
||||
{/if}
|
||||
|
||||
<Typography.Text style="margin-left: 6px;" variant="m-400">
|
||||
Additional charges apply when you <b>exceed</b> plan limits.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text external href="https://appwrite.io/privacy">Privacy Policy</Button>
|
||||
<Button text external href="{resolvedProfile.website}/privacy">Privacy Policy</Button>
|
||||
<Button on:click={() => confirmChoices(selected)}>Save preferences</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { getServiceLimit, plansInfo } from '$lib/stores/billing';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { Badge, Icon, Layout, Table, Typography, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconArrowUp, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -13,6 +12,7 @@
|
||||
import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date';
|
||||
import { organization, type Organization } from '$lib/stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
// Props
|
||||
type Props = {
|
||||
@@ -30,37 +30,37 @@
|
||||
let showSelectionReminder = $state(false);
|
||||
|
||||
// Derived state using runes
|
||||
let freePlanLimits = $derived({
|
||||
projects: $plansInfo?.get(BillingPlan.FREE)?.projects,
|
||||
members: getServiceLimit('members', BillingPlan.FREE),
|
||||
storage: getServiceLimit('storage', BillingPlan.FREE)
|
||||
const freePlanLimits = $derived({
|
||||
projects: $plansInfo?.get(resolvedProfile.freeTier)?.projects,
|
||||
members: getServiceLimit('members', resolvedProfile.freeTier),
|
||||
storage: getServiceLimit('storage', resolvedProfile.freeTier)
|
||||
});
|
||||
|
||||
// When preparing to downgrade to Free, enforce Free plan limit locally (2)
|
||||
let allowedProjectsToKeep = $derived(freePlanLimits.projects);
|
||||
const allowedProjectsToKeep = $derived(freePlanLimits.projects);
|
||||
|
||||
let currentUsage = $derived({
|
||||
const currentUsage = $derived({
|
||||
projects: projects?.length || 0,
|
||||
members: members?.length || 0,
|
||||
storage: storageUsage || 0
|
||||
});
|
||||
|
||||
let storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024));
|
||||
const storageUsageGB = $derived(storageUsage / (1024 * 1024 * 1024));
|
||||
|
||||
let isLimitExceeded = $derived({
|
||||
const isLimitExceeded = $derived({
|
||||
projects: currentUsage.projects > freePlanLimits.projects,
|
||||
members: currentUsage.members > freePlanLimits.members,
|
||||
storage: storageUsageGB > freePlanLimits.storage
|
||||
});
|
||||
|
||||
let excessUsage = $derived({
|
||||
const excessUsage = $derived({
|
||||
projects: Math.max(0, currentUsage.projects),
|
||||
members: Math.max(0, currentUsage.members - freePlanLimits.members),
|
||||
storage: Math.max(0, storageUsageGB - freePlanLimits.storage)
|
||||
});
|
||||
|
||||
// projects that would be archived with the current selection
|
||||
let projectsToArchive = $derived(
|
||||
const projectsToArchive = $derived(
|
||||
projects.filter((project) => !selectedProjects.includes(project.$id))
|
||||
);
|
||||
|
||||
@@ -113,7 +113,9 @@
|
||||
error = `You must select exactly ${allowedProjectsToKeep} projects to keep.`;
|
||||
return;
|
||||
}
|
||||
// Keep selection locally; parent flow will apply after plan change
|
||||
|
||||
// Keep selection locally;
|
||||
// parent flow will apply after plan change
|
||||
showSelectProject = false;
|
||||
showSelectionReminder = false;
|
||||
addNotification({ type: 'success', message: `Projects selected for archiving` });
|
||||
@@ -319,7 +321,7 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
/*z-index: 2;*/
|
||||
}
|
||||
|
||||
/* Small viewport optimizations */
|
||||
@@ -327,7 +329,7 @@
|
||||
.responsive-table {
|
||||
margin-inline: -1rem;
|
||||
padding-inline: 1rem;
|
||||
z-index: 5;
|
||||
/*z-index: 5;*/
|
||||
}
|
||||
|
||||
.responsive-table :global(td),
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
cta: 'Discord',
|
||||
showSupport: false,
|
||||
label: 'Community support',
|
||||
link: 'https://appwrite.io/discord',
|
||||
link: `${resolvedProfile.discord}`,
|
||||
description: 'Get support from our community through Discord'
|
||||
},
|
||||
...(!resolvedProfile.showGithubIssueSupport
|
||||
|
||||
@@ -9,3 +9,7 @@ export function isFreePlan(plan: BillingPlan | string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPaidPlan(plan: BillingPlan | string): boolean {
|
||||
return !isFreePlan(plan);
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://appwrite.io/docs"
|
||||
href="{resolvedProfile.website}/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Docs
|
||||
@@ -110,7 +110,7 @@
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://appwrite.io/terms"
|
||||
href="{resolvedProfile.website}/terms"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Terms
|
||||
@@ -121,7 +121,7 @@
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://appwrite.io/privacy"
|
||||
href="{resolvedProfile.website}/privacy"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Privacy
|
||||
@@ -133,7 +133,7 @@
|
||||
<Link.Anchor
|
||||
size="s"
|
||||
variant="quiet"
|
||||
href="https://appwrite.io/cookies"
|
||||
href="{resolvedProfile.website}/cookies"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
Cookies
|
||||
|
||||
@@ -54,6 +54,8 @@ export type Profile = {
|
||||
sites: boolean;
|
||||
settings: boolean;
|
||||
};
|
||||
discord: string;
|
||||
website: string;
|
||||
showOrgInBreadcrumbs: boolean;
|
||||
minimalOrgHeader: boolean;
|
||||
getProjectRoute: (params: { region: string; project: string }) => ResolvedPathname;
|
||||
@@ -101,6 +103,8 @@ export const base: Profile = {
|
||||
sites: true,
|
||||
settings: true
|
||||
},
|
||||
discord: 'https://appwrite.io/discord',
|
||||
website: 'https://appwrite.io',
|
||||
showOrgInBreadcrumbs: true,
|
||||
minimalOrgHeader: false,
|
||||
getProjectRoute({ region, project }) {
|
||||
@@ -127,7 +131,7 @@ export const studio: Profile = {
|
||||
logins: [/** temporary */ Logins.EMAIL, Logins.GITHUB, Logins.GOOGLE].filter(Boolean),
|
||||
oauthProviders: {
|
||||
github: OAuthProvider.GithubImagine,
|
||||
google: OAuthProvider.Google
|
||||
google: OAuthProvider.GoogleImagine
|
||||
},
|
||||
css: StudioCss,
|
||||
component: {
|
||||
@@ -151,6 +155,8 @@ export const studio: Profile = {
|
||||
sites: false,
|
||||
settings: true
|
||||
},
|
||||
website: 'https://imagine.dev',
|
||||
discord: 'https://imagine.dev/discord',
|
||||
showOrgInBreadcrumbs: false,
|
||||
minimalOrgHeader: true,
|
||||
getProjectRoute({ region, project }) {
|
||||
|
||||
+12
-16
@@ -1,7 +1,8 @@
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
import type { Campaign } from '$lib/stores/campaigns';
|
||||
import type { Client, Models, Platform } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, type Client, type Models, type Platform } from '@appwrite.io/console';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import type { Organization, OrganizationError, OrganizationList } from '../stores/organization';
|
||||
|
||||
export type PaymentMethodData = {
|
||||
@@ -411,6 +412,9 @@ export type Plan = {
|
||||
databasesWrites: AdditionalResource;
|
||||
GBHours: AdditionalResource;
|
||||
imageTransformations: AdditionalResource;
|
||||
|
||||
// imagine specific
|
||||
credits: AdditionalResource;
|
||||
};
|
||||
addons: {
|
||||
seats: PlanAddon;
|
||||
@@ -431,6 +435,11 @@ export type Plan = {
|
||||
buildSize: number; // in MB
|
||||
deploymentSize: number; // in MB
|
||||
usagePerProject: boolean;
|
||||
limits: {
|
||||
credits?: number;
|
||||
dailyCredits?: number;
|
||||
};
|
||||
group: null | BillingPlanGroup;
|
||||
};
|
||||
|
||||
export type PlanList = {
|
||||
@@ -581,7 +590,8 @@ export class Billing {
|
||||
const path = `/console/plans`;
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
const params = {
|
||||
queries
|
||||
queries,
|
||||
platform: resolvedProfile.platform.toLowerCase()
|
||||
};
|
||||
return await this.client.call(
|
||||
'get',
|
||||
@@ -1441,18 +1451,4 @@ export class Billing {
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async getPlansInfo(): Promise<PlanList> {
|
||||
const path = `/console/plans`;
|
||||
const params = {};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'GET',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,9 @@ import { user } from './user';
|
||||
import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte';
|
||||
import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte';
|
||||
import ProjectsLimit from '$lib/components/billing/alerts/projectsLimit.svelte';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
import { isFreePlan, isPaidPlan } from '$lib/helpers/billing';
|
||||
import { BillingPlan as CloudSdkBillingPlan } from '@appwrite.io/console';
|
||||
|
||||
export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1';
|
||||
|
||||
@@ -153,7 +154,11 @@ export type PlanServices =
|
||||
| 'authPhone'
|
||||
| 'imageTransformations';
|
||||
|
||||
export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan?: Plan): number {
|
||||
export function getServiceLimit(
|
||||
serviceId: PlanServices,
|
||||
tier: Tier | CloudSdkBillingPlan = null,
|
||||
plan?: Plan
|
||||
): number {
|
||||
if (!isCloud) return 0;
|
||||
if (!serviceId) return 0;
|
||||
|
||||
@@ -266,7 +271,7 @@ 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) {
|
||||
if (isPaidPlan(currentTier)) {
|
||||
return false; // No project limitation for members on Pro/Scale plans
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
import type { LayoutData } from './$types';
|
||||
import Studio from '$lib/studio/studio.svelte';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
@@ -110,7 +111,7 @@
|
||||
{
|
||||
label: 'Open documentation',
|
||||
callback: () => {
|
||||
window.open('https://appwrite.io/docs', '_blank');
|
||||
window.open(`${resolvedProfile.website}/docs`, '_blank');
|
||||
},
|
||||
group: 'help',
|
||||
icon: IconBookOpen
|
||||
@@ -118,7 +119,7 @@
|
||||
{
|
||||
label: 'Contact support',
|
||||
callback: () => {
|
||||
window.open('https://appwrite.io/discord', '_blank');
|
||||
window.open(`${resolvedProfile.website}/discord`, '_blank');
|
||||
},
|
||||
group: 'help',
|
||||
icon: IconQuestionMarkCircle
|
||||
@@ -134,7 +135,7 @@
|
||||
{
|
||||
label: 'Join Discord community',
|
||||
callback: () => {
|
||||
window.open('https://appwrite.io/discord', '_blank');
|
||||
window.open('${resolvedProfile.website}/discord', '_blank');
|
||||
},
|
||||
group: 'help',
|
||||
icon: IconDiscord
|
||||
|
||||
@@ -17,7 +17,7 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
|
||||
const { endpoint, project } = sdk.forConsole.client.config;
|
||||
const [preferences, plansArray, versionData, consoleVariables] = await Promise.all([
|
||||
sdk.forConsole.account.getPrefs(),
|
||||
isCloud ? sdk.forConsole.billing.getPlansInfo() : null,
|
||||
isCloud ? sdk.forConsole.billing.listPlans() : null,
|
||||
fetch(`${endpoint}/health/version`, {
|
||||
headers: { 'X-Appwrite-Project': project }
|
||||
}).then((response) => response.json() as { version?: string }),
|
||||
|
||||
@@ -198,13 +198,16 @@
|
||||
<Typography.Text>
|
||||
For more details on our plans, visit our
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/pricing"
|
||||
href="{resolvedProfile.website}/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">pricing page</Link.Anchor
|
||||
>.
|
||||
</Typography.Text>
|
||||
|
||||
<PlanSelection bind:billingPlan={selectedPlan} isNewOrg />
|
||||
<PlanSelection
|
||||
isNewOrg
|
||||
disabled={$isSubmitting}
|
||||
bind:billingPlan={selectedPlan} />
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
{#if !isFreePlan(selectedPlan)}
|
||||
@@ -258,6 +261,8 @@
|
||||
<Button fullWidthMobile secondary on:click={() => (showExitModal = true)}>Cancel</Button>
|
||||
<Button
|
||||
fullWidthMobile
|
||||
forceShowLoader
|
||||
submissionLoader={$isSubmitting}
|
||||
on:click={() => formComponent.triggerSubmit()}
|
||||
disabled={$isSubmitting}>
|
||||
Create organization
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
will need to either upgrade to the Pro plan, transfer your projects to a Pro
|
||||
organization, or migrate to self-hosting.
|
||||
<svelte:fragment slot="actions">
|
||||
<Button href="https://appwrite.io/pricing" external text>Learn more</Button>
|
||||
<Button href="{resolvedProfile.website}/pricing" external text>Learn more</Button>
|
||||
</svelte:fragment>
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
@@ -37,13 +37,14 @@
|
||||
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';
|
||||
import { isFreePlan, isPaidPlan } from '$lib/helpers/billing.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let selectedCoupon: Partial<Coupon> = null;
|
||||
|
||||
let selectedPlan: BillingPlan = data.plan as BillingPlan;
|
||||
|
||||
let previousPage: string = base;
|
||||
let showExitModal = false;
|
||||
let formComponent: Form;
|
||||
@@ -98,8 +99,7 @@
|
||||
await validate(organizationId, invites);
|
||||
}
|
||||
|
||||
selectedPlan =
|
||||
$currentPlan?.$id === BillingPlan.SCALE ? BillingPlan.SCALE : BillingPlan.PRO;
|
||||
selectedPlan = $currentPlan?.$id as BillingPlan;
|
||||
|
||||
try {
|
||||
orgUsage = await sdk.forConsole.billing.listUsage(data.organization.$id);
|
||||
@@ -108,11 +108,10 @@
|
||||
}
|
||||
|
||||
try {
|
||||
allProjects = await sdk.forConsole.projects.list([
|
||||
Query.equal('teamId', data.organization.$id),
|
||||
Query.limit(1000)
|
||||
]);
|
||||
} catch {
|
||||
allProjects = await sdk.forConsole.projects.list({
|
||||
queries: [Query.limit(1000), Query.equal('teamId', data.organization.$id)]
|
||||
});
|
||||
} catch (error) {
|
||||
allProjects = { projects: [] };
|
||||
}
|
||||
});
|
||||
@@ -234,6 +233,7 @@
|
||||
!data?.members?.memberships?.find((m) => m.userEmail === collaborator)
|
||||
);
|
||||
}
|
||||
|
||||
const org = await sdk.forConsole.billing.updatePlan(
|
||||
data.organization.$id,
|
||||
selectedPlan,
|
||||
@@ -294,11 +294,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: isUpgrade = $plansInfo.get(selectedPlan).order > $currentPlan?.order;
|
||||
$: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan?.order;
|
||||
$: isUpgrade = $plansInfo.get(selectedPlan)?.order > $currentPlan?.order;
|
||||
$: isDowngrade = $plansInfo.get(selectedPlan)?.order < $currentPlan?.order;
|
||||
$: isButtonDisabled =
|
||||
$organization?.billingPlan === selectedPlan ||
|
||||
(isDowngrade && isFreePlan(selectedPlan) && data.hasFreeOrgs);
|
||||
|
||||
// reactive vars for same group downgrades
|
||||
$: selectedPlanGroup = $plansInfo?.get(selectedPlan)?.group;
|
||||
$: currentPlanGroup = $plansInfo?.get($organization?.billingPlan)?.group;
|
||||
$: isSameGroupDowngrade =
|
||||
currentPlanGroup && selectedPlanGroup && currentPlanGroup === selectedPlanGroup;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -313,7 +319,7 @@
|
||||
<Typography.Text>
|
||||
For more details on our plans, visit our
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/pricing"
|
||||
href="{resolvedProfile.website}/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">pricing page</Link.Anchor
|
||||
>.
|
||||
@@ -326,7 +332,10 @@
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
<PlanSelection bind:billingPlan={selectedPlan} selfService={data.selfService} />
|
||||
<PlanSelection
|
||||
disabled={$isSubmitting}
|
||||
selfService={data.selfService}
|
||||
bind:billingPlan={selectedPlan} />
|
||||
|
||||
{#if isDowngrade && isFreePlan(selectedPlan) && data.hasFreeOrgs}
|
||||
<Alert.Inline
|
||||
@@ -335,10 +344,11 @@
|
||||
To downgrade this organization, first migrate or delete your existing
|
||||
free organization.
|
||||
<Layout.Stack gap="xs" direction="row" justifyContent="flex-start">
|
||||
<!-- check the link on imagine later -->
|
||||
<Button
|
||||
compact
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/migrations/cloud"
|
||||
href="{resolvedProfile.website}/docs/advanced/migrations/cloud"
|
||||
>Migration guide</Button>
|
||||
</Layout.Stack>
|
||||
</Alert.Inline>
|
||||
@@ -350,15 +360,22 @@
|
||||
extraMembers *
|
||||
($plansInfo?.get(selectedPlan)?.addons?.seats?.price ?? 0)
|
||||
)}
|
||||
{#if selectedPlan === BillingPlan.PRO}
|
||||
{#if isPaidPlan(selectedPlan)}
|
||||
<Alert.Inline status="error">
|
||||
<svelte:fragment slot="title">
|
||||
Your monthly payments will be adjusted for the Pro plan
|
||||
</svelte:fragment>
|
||||
After switching plans,
|
||||
<b
|
||||
>you will be charged {price} monthly for {extraMembers} team members.</b>
|
||||
This will be reflected in your next invoice.
|
||||
{#if isSameGroupDowngrade}
|
||||
After switching plans,
|
||||
<b>you will be charged {price}.</b>
|
||||
This will be reflected in your next invoice.
|
||||
{:else}
|
||||
After switching plans,
|
||||
<b
|
||||
>you will be charged {price} monthly for {extraMembers} team
|
||||
members.</b>
|
||||
This will be reflected in your next invoice.
|
||||
{/if}
|
||||
</Alert.Inline>
|
||||
{:else if isFreePlan(selectedPlan)}
|
||||
<Alert.Inline
|
||||
@@ -374,12 +391,14 @@
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
<OrganizationUsageLimits
|
||||
bind:this={usageLimitsComponent}
|
||||
organization={data.organization}
|
||||
projects={allProjects?.projects || []}
|
||||
members={data.members?.memberships || []}
|
||||
storageUsage={orgUsage?.storageTotal ?? 0} />
|
||||
{#if !isSameGroupDowngrade}
|
||||
<OrganizationUsageLimits
|
||||
bind:this={usageLimitsComponent}
|
||||
organization={data.organization}
|
||||
projects={allProjects?.projects || []}
|
||||
members={data.members?.memberships || []}
|
||||
storageUsage={orgUsage?.storageTotal ?? 0} />
|
||||
{/if}
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Fieldset>
|
||||
|
||||
@@ -3,17 +3,18 @@ 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';
|
||||
import type { PlanList } from '$lib/sdk/billing';
|
||||
|
||||
export const load: PageLoad = async ({ depends, parent }) => {
|
||||
const { members, currentPlan, organizations } = await parent();
|
||||
depends(Dependencies.UPGRADE_PLAN);
|
||||
|
||||
let plans;
|
||||
let plans: PlanList;
|
||||
try {
|
||||
plans = await sdk.forConsole.billing.listPlans();
|
||||
} catch (error) {
|
||||
console.error('Failed to load billing plans:', error);
|
||||
plans = { plans: {} };
|
||||
plans = { total: 0, plans: [] };
|
||||
}
|
||||
|
||||
let plan: BillingPlan;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { UsageProjectInfo } from '../../store';
|
||||
import { isFreePlan } from '$lib/helpers/billing';
|
||||
import { resolvedProfile } from '$lib/profiles/index.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -83,7 +84,7 @@
|
||||
<Link.Button on:click={() => ($showUsageRatesModal = true)}>Learn more</Link.Button>
|
||||
{:else}
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/pricing"
|
||||
href="{resolvedProfile.website}/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Learn more
|
||||
@@ -98,7 +99,7 @@
|
||||
<Link.Button on:click={() => ($showUsageRatesModal = true)}>Learn more</Link.Button>
|
||||
{:else}
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/pricing"
|
||||
href="{resolvedProfile.website}/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Learn more
|
||||
|
||||
+5
-6
@@ -3,7 +3,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Card } from '$lib/components';
|
||||
import { Card, CustomId } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -11,8 +11,7 @@
|
||||
import { Fieldset, Layout, Icon, Input, Tag } from '@appwrite.io/pink-svelte';
|
||||
import { IconGithub, IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { ID, Runtime, Type } from '@appwrite.io/console';
|
||||
import { CustomId } from '$lib/components';
|
||||
import { ID, Runtime, TemplateReferenceType } from '@appwrite.io/console';
|
||||
import { getIconFromRuntime } from '$lib/stores/runtimes';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { iconPath } from '$lib/stores/app';
|
||||
@@ -32,8 +31,8 @@
|
||||
let showCustomId = $state(false);
|
||||
let isSubmitting = $state(writable(false));
|
||||
|
||||
let id = $state(ID.unique());
|
||||
let name = $state(data.repository.name);
|
||||
let id: string = $state(ID.unique());
|
||||
let name: string = $state(data.repository.name);
|
||||
|
||||
let execute = $state(true);
|
||||
let entrypoint = $state('');
|
||||
@@ -135,7 +134,7 @@
|
||||
repository: data.repository.name,
|
||||
owner: data.repository.owner,
|
||||
rootDirectory: rootDir || '.',
|
||||
type: Type.Tag,
|
||||
type: TemplateReferenceType.Tag,
|
||||
reference: latestTag ?? '1.0.0',
|
||||
activate: true
|
||||
});
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
import { installation, repository } from '$lib/stores/vcs';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { ID, Runtime, VCSDeploymentType, VCSDetectionType } from '@appwrite.io/console';
|
||||
import { ID, Runtime, VCSDetectionType, VCSReferenceType } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import Details from '../(components)/details.svelte';
|
||||
@@ -133,7 +133,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.functions.createVcsDeployment({
|
||||
functionId: func.$id,
|
||||
type: VCSDeploymentType.Branch,
|
||||
type: VCSReferenceType.Branch,
|
||||
reference: branch,
|
||||
activate: true
|
||||
});
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import ProductionBranch from '$lib/components/git/productionBranchFieldset.svelte';
|
||||
import Configuration from './configuration.svelte';
|
||||
import { ID, Runtime, Type, type Models } from '@appwrite.io/console';
|
||||
import { ID, Runtime, type Models, TemplateReferenceType } from '@appwrite.io/console';
|
||||
import {
|
||||
ConnectBehaviour,
|
||||
NewRepository,
|
||||
@@ -180,7 +180,7 @@
|
||||
repository: data.template.providerRepositoryId || undefined,
|
||||
owner: data.template.providerOwner || undefined,
|
||||
rootDirectory: rt?.providerRootDirectory || undefined,
|
||||
type: Type.Tag,
|
||||
type: TemplateReferenceType.Tag,
|
||||
reference: data.template.providerVersion || undefined,
|
||||
activate: true
|
||||
});
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { installation, repository, sortBranches } from '$lib/stores/vcs';
|
||||
import { Runtime, VCSDeploymentType, type Models } from '@appwrite.io/console';
|
||||
import { Runtime, VCSReferenceType, type Models } from '@appwrite.io/console';
|
||||
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon, Input, Layout, Skeleton, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { func } from '../store';
|
||||
@@ -98,7 +98,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.functions.createVcsDeployment({
|
||||
functionId: $func.$id,
|
||||
type: VCSDeploymentType.Commit,
|
||||
type: VCSReferenceType.Commit,
|
||||
reference: commit,
|
||||
activate
|
||||
});
|
||||
@@ -107,7 +107,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.functions.createVcsDeployment({
|
||||
functionId: $func.$id,
|
||||
type: VCSDeploymentType.Branch,
|
||||
type: VCSReferenceType.Branch,
|
||||
reference: branch,
|
||||
activate
|
||||
});
|
||||
|
||||
@@ -565,7 +565,7 @@
|
||||
color="--fgcolor-neutral-secondary">
|
||||
Deploy the {resolvedProfile.platform} MCP server
|
||||
with a single click, or view the <Link.Anchor
|
||||
href="https://appwrite.io/docs"
|
||||
href="{resolvedProfile.website}/docs"
|
||||
target="_blank">docs</Link.Anchor> for instructions.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
@@ -598,7 +598,7 @@
|
||||
</Card.Base>
|
||||
|
||||
<Card.Link
|
||||
href="https://appwrite.io/discord"
|
||||
href="{resolvedProfile.website}/discord"
|
||||
padding="s"
|
||||
on:click={() => {
|
||||
trackEvent(Click.OnboardingDiscordClick);
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
<svelte:fragment slot="description">
|
||||
Deploy the {resolvedProfile.platform} MCP server with a single click, or view the
|
||||
<PinkLink.Anchor
|
||||
href="https://appwrite.io/docs"
|
||||
href="{resolvedProfile.website}/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</PinkLink.Anchor> for instructions.
|
||||
</svelte:fragment>
|
||||
|
||||
+9
-4
@@ -3,7 +3,7 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Card } from '$lib/components';
|
||||
import { Card, CustomId } from '$lib/components';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -12,8 +12,13 @@
|
||||
import { IconGithub, IconPencil } from '@appwrite.io/pink-icons-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Domain from '../domain.svelte';
|
||||
import { Adapter, BuildRuntime, Framework, ID, Type } from '@appwrite.io/console';
|
||||
import { CustomId } from '$lib/components';
|
||||
import {
|
||||
Adapter,
|
||||
BuildRuntime,
|
||||
Framework,
|
||||
ID,
|
||||
TemplateReferenceType
|
||||
} from '@appwrite.io/console';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites';
|
||||
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { iconPath } from '$lib/stores/app';
|
||||
@@ -173,7 +178,7 @@
|
||||
repository: data.repository.name,
|
||||
owner: data.repository.owner,
|
||||
rootDirectory: rootDir || '.',
|
||||
type: Type.Tag,
|
||||
type: TemplateReferenceType.Tag,
|
||||
reference: latestTag ?? '1.0.0',
|
||||
activate: true
|
||||
});
|
||||
|
||||
+4
-4
@@ -19,7 +19,7 @@
|
||||
BuildRuntime,
|
||||
Framework,
|
||||
ID,
|
||||
VCSDeploymentType,
|
||||
VCSReferenceType,
|
||||
VCSDetectionType
|
||||
} from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
@@ -98,8 +98,8 @@
|
||||
let site = await sdk.forProject(page.params.region, page.params.project).sites.create({
|
||||
siteId: id || ID.unique(),
|
||||
name,
|
||||
framework: fr,
|
||||
buildRuntime,
|
||||
framework: fr as Framework,
|
||||
buildRuntime: buildRuntime as BuildRuntime,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
outputDirectory,
|
||||
@@ -132,7 +132,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.sites.createVcsDeployment({
|
||||
siteId: site.$id,
|
||||
type: VCSDeploymentType.Branch,
|
||||
type: VCSReferenceType.Branch,
|
||||
reference: branch,
|
||||
activate: true
|
||||
});
|
||||
|
||||
+11
-4
@@ -24,7 +24,14 @@
|
||||
import Details from '../../details.svelte';
|
||||
import Configuration from './configuration.svelte';
|
||||
import Aside from '../../aside.svelte';
|
||||
import { Adapter, BuildRuntime, Framework, ID, Type, type Models } from '@appwrite.io/console';
|
||||
import {
|
||||
Adapter,
|
||||
BuildRuntime,
|
||||
Framework,
|
||||
ID,
|
||||
type Models,
|
||||
TemplateReferenceType
|
||||
} from '@appwrite.io/console';
|
||||
import {
|
||||
ConnectBehaviour,
|
||||
NewRepository,
|
||||
@@ -120,8 +127,8 @@
|
||||
.sites.create({
|
||||
siteId: id || ID.unique(),
|
||||
name,
|
||||
framework: fr,
|
||||
buildRuntime,
|
||||
framework: fr as Framework,
|
||||
buildRuntime: buildRuntime as BuildRuntime,
|
||||
installCommand: framework.installCommand,
|
||||
buildCommand: framework.buildCommand,
|
||||
outputDirectory: framework.outputDirectory,
|
||||
@@ -160,7 +167,7 @@
|
||||
repository: data.template.providerRepositoryId,
|
||||
owner: data.template.providerOwner,
|
||||
rootDirectory: framework.providerRootDirectory,
|
||||
type: Type.Tag,
|
||||
type: TemplateReferenceType.Tag,
|
||||
reference: data.template.providerVersion,
|
||||
activate: true
|
||||
});
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
Adapter,
|
||||
BuildRuntime,
|
||||
Framework,
|
||||
VCSDeploymentType,
|
||||
VCSReferenceType,
|
||||
type Models
|
||||
} from '@appwrite.io/console';
|
||||
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
|
||||
@@ -106,7 +106,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.sites.createVcsDeployment({
|
||||
siteId: site.$id,
|
||||
type: VCSDeploymentType.Commit,
|
||||
type: VCSReferenceType.Commit,
|
||||
reference: commit,
|
||||
activate
|
||||
});
|
||||
@@ -115,7 +115,7 @@
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.sites.createVcsDeployment({
|
||||
siteId: site.$id,
|
||||
type: VCSDeploymentType.Branch,
|
||||
type: VCSReferenceType.Branch,
|
||||
reference: branch,
|
||||
activate
|
||||
});
|
||||
|
||||
@@ -149,13 +149,13 @@
|
||||
bind:value={pass} />
|
||||
<InputChoice required bind:value={terms} id="terms" label="terms" showLabel={false}>
|
||||
By registering, you agree that you have read, understand, and acknowledge our <Link.Anchor
|
||||
href="https://appwrite.io/privacy"
|
||||
href="{resolvedProfile.website}/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Privacy Policy</Link.Anchor>
|
||||
and accept our
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/terms"
|
||||
href="{resolvedProfile.website}/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">General Terms of Use</Link.Anchor
|
||||
>.</InputChoice>
|
||||
|
||||
@@ -134,14 +134,14 @@
|
||||
<InputChoice required value={terms} id="terms" label="terms" showLabel={false}>
|
||||
By registering, you agree that you have read, understand, and acknowledge our <a
|
||||
class="link"
|
||||
href="https://appwrite.io/privacy"
|
||||
href="{resolvedProfile.website}/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Privacy Policy</a>
|
||||
and accept our
|
||||
<a
|
||||
class="link"
|
||||
href="https://appwrite.io/terms"
|
||||
href="{resolvedProfile.website}/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">General Terms of Use</a
|
||||
>.</InputChoice>
|
||||
|
||||
@@ -81,12 +81,12 @@
|
||||
label="terms"
|
||||
showLabel={false}>
|
||||
By accepting the invitation, you agree to the <Link.Anchor
|
||||
href="https://appwrite.io/terms"
|
||||
href="{resolvedProfile.website}/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Terms and Conditions</Link.Anchor>
|
||||
and
|
||||
<Link.Anchor
|
||||
href="https://appwrite.io/privacy"
|
||||
href="{resolvedProfile.website}/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Privacy Policy</Link.Anchor
|
||||
|
||||
Reference in New Issue
Block a user