mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1196 from appwrite/feat-scale-plan
feat: scale plan
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
export let isDowngrade = false;
|
||||
|
||||
const today = new Date();
|
||||
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
@@ -41,8 +42,8 @@
|
||||
<p class="text">{formatCurrency(currentPlan.price)}</p>
|
||||
</span>
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">Additional seats ({collaborators?.length})</p>
|
||||
<p class="text">
|
||||
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
|
||||
<p class="text" class:u-bold={isDowngrade}>
|
||||
{formatCurrency(extraSeatsCost)}
|
||||
</p>
|
||||
</span>
|
||||
|
||||
@@ -6,3 +6,4 @@ export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte';
|
||||
export { default as PlanComparisonBox } from './planComparisonBox.svelte';
|
||||
export { default as EmptyCardCloud } from './emptyCardCloud.svelte';
|
||||
export { default as CreditsApplied } from './creditsApplied.svelte';
|
||||
export { default as PlanSelection } from './planSelection.svelte';
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
|
||||
let selectedTab: 'tier-0' | 'tier-1' = 'tier-0';
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
export let downgrade = false;
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-0'}
|
||||
on:click={() => (selectedTab = 'tier-0')}>
|
||||
Free
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === 'tier-1'}
|
||||
on:click={() => (selectedTab = 'tier-1')}>
|
||||
Pro
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
<Card style="--card-padding: 1.5rem">
|
||||
<div class="comparison-box">
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.FREE}
|
||||
on:click={() => (selectedTab = BillingPlan.FREE)}>
|
||||
{tierFree.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.PRO}
|
||||
on:click={() => (selectedTab = BillingPlan.PRO)}>
|
||||
{tierPro.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.SCALE}
|
||||
on:click={() => (selectedTab = BillingPlan.SCALE)}>
|
||||
{tierScale.name}
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
{#if selectedTab === 'tier-0'}
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
@@ -86,16 +94,50 @@
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === 'tier-1'}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<ul class="un-order-list u-margin-block-start-8">
|
||||
<li>Everything in the Free plan, plus:</li>
|
||||
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited databases, buckets, functions</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}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles <span class="inline-tag">Coming soon</span></li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
<li>SSO <span class="inline-tag">Coming soon</span></li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.comparison-box {
|
||||
border-radius: var(--border-radius-small);
|
||||
background: hsl(var(--color-neutral-5));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box {
|
||||
background: hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-0));
|
||||
border: 1px solid hsl(var(--color-neutral-10));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-80));
|
||||
border: 1px solid hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
|
||||
export let tier: Tier;
|
||||
export let members: number;
|
||||
|
||||
const plan = $plansInfo?.get(tier);
|
||||
let excess: {
|
||||
@@ -41,7 +42,7 @@
|
||||
$organization.billingCurrentInvoiceDate,
|
||||
new Date().toISOString()
|
||||
);
|
||||
excess = calculateExcess(usage, plan, $organization);
|
||||
excess = calculateExcess(usage, plan, members);
|
||||
showExcess = Object.values(excess).some((value) => value > 0);
|
||||
});
|
||||
</script>
|
||||
@@ -98,7 +99,8 @@
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
{humanFileSize(excess?.storage)}
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
|
||||
</script>
|
||||
|
||||
{#if billingPlan}
|
||||
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree}
|
||||
value={BillingPlan.FREE}
|
||||
tooltipShow={anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account."
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierFree.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.PRO} padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
<li>
|
||||
<LabelCard name="plan" bind:group={billingPlan} value={BillingPlan.SCALE} padding={1.5}>
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierScale.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierScale.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
@@ -150,7 +150,7 @@ export const tierPro: TierData = {
|
||||
};
|
||||
export const tierScale: TierData = {
|
||||
name: 'Scale',
|
||||
description: 'For scaling teams that need dedicated support.'
|
||||
description: 'For scaling teams and agencies that need dedicated support.'
|
||||
};
|
||||
|
||||
export const showUsageRatesModal = writable<boolean>(false);
|
||||
@@ -422,14 +422,14 @@ export const upgradeURL = derived(
|
||||
|
||||
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
|
||||
|
||||
export function calculateExcess(usage: OrganizationUsage, plan: Plan, org: Organization) {
|
||||
export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: number) {
|
||||
const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0;
|
||||
return {
|
||||
bandwidth: calculateResourceSurplus(totBandwidth, plan.bandwidth),
|
||||
storage: calculateResourceSurplus(usage?.storageTotal, plan.storage, 'GB'),
|
||||
users: calculateResourceSurplus(usage?.usersTotal, plan.users),
|
||||
executions: calculateResourceSurplus(usage?.executionsTotal, plan.executions, 'GB'),
|
||||
members: calculateResourceSurplus(org.total, plan.members)
|
||||
members: calculateResourceSurplus(members, plan.members)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//campaign welcome and startup
|
||||
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
|
||||
export type CampaignData = {
|
||||
title: string;
|
||||
description: string;
|
||||
template: 'card' | 'review';
|
||||
data?: Record<string, unknown>;
|
||||
onlyNewOrgs?: boolean;
|
||||
plan?: BillingPlan;
|
||||
footer?: boolean;
|
||||
};
|
||||
|
||||
@@ -22,7 +25,7 @@ campaigns
|
||||
template: 'card',
|
||||
title: 'Welcome to the Startups program!',
|
||||
description:
|
||||
"We're excited to have you on board. Add the coupon code to your Appwrite Pro account to join."
|
||||
"We're excited to have you on board. Add the coupon code to your Appwrite Scale account to join."
|
||||
})
|
||||
.set('RenderATL2024', {
|
||||
template: 'card',
|
||||
@@ -52,10 +55,11 @@ campaigns
|
||||
template: 'review',
|
||||
title: 'Welcome to the Startups program',
|
||||
description:
|
||||
"We're excited to have you on board. Add your credit code to your Appwrite Pro account to join.",
|
||||
"We're excited to have you on board. Add your credit code to your Appwrite Scale account to join.",
|
||||
onlyNewOrgs: true,
|
||||
plan: BillingPlan.SCALE,
|
||||
data: {
|
||||
cta: 'Get everything out of Cloud with Pro',
|
||||
cta: 'Get everything out of Cloud with Scale',
|
||||
claimed: 'Your credits will be valid for 12 months.',
|
||||
unclaimed: 'Apply your code to join the Startups program.',
|
||||
reviews: [
|
||||
|
||||
@@ -19,7 +19,7 @@ export type Organization = Models.Team<Record<string, unknown>> & {
|
||||
billingAddressId?: string;
|
||||
amount: number;
|
||||
billingTaxId?: string;
|
||||
billingPlanDowngrade?: string;
|
||||
billingPlanDowngrade?: Tier;
|
||||
};
|
||||
|
||||
export type OrganizationList = {
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
let coupon: string;
|
||||
let couponData = data?.couponData;
|
||||
let campaign = campaigns.get(data?.couponData?.campaign ?? data?.campaign);
|
||||
let billingPlan = BillingPlan.PRO;
|
||||
|
||||
onMount(async () => {
|
||||
await loadPaymentMethods();
|
||||
@@ -80,6 +81,9 @@
|
||||
selectedOrgId = $page.url.searchParams.get('org');
|
||||
canSelectOrg = false;
|
||||
}
|
||||
if (campaign?.plan) {
|
||||
billingPlan = campaign.plan;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
@@ -101,15 +105,15 @@
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
name,
|
||||
BillingPlan.PRO,
|
||||
billingPlan,
|
||||
paymentMethodId
|
||||
);
|
||||
}
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== BillingPlan.PRO) {
|
||||
else if (selectedOrg?.billingPlan === BillingPlan.FREE) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
BillingPlan.PRO,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null
|
||||
);
|
||||
@@ -270,7 +274,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedOrg?.billingPlan === BillingPlan.PRO}
|
||||
{#if selectedOrg?.billingPlan !== BillingPlan.FREE}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { LabelCard } from '$lib/components';
|
||||
import {
|
||||
EstimatedTotalBox,
|
||||
PlanComparisonBox,
|
||||
PlanSelection,
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputTags, InputText, Label } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
@@ -20,7 +19,7 @@
|
||||
WizardSecondaryHeader
|
||||
} from '$lib/layout';
|
||||
import type { Coupon, PaymentList } from '$lib/sdk/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierToPlan } from '$lib/stores/billing';
|
||||
import { tierToPlan } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -28,7 +27,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
$: anyOrgFree = $organizationList.teams?.find(
|
||||
$: anyOrgFree = $organizationList.teams?.some(
|
||||
(org) => (org as Organization)?.billingPlan === BillingPlan.FREE
|
||||
);
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
@@ -175,9 +174,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: if (billingPlan === BillingPlan.PRO) {
|
||||
$: if (billingPlan !== BillingPlan.FREE) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
</script>
|
||||
@@ -205,53 +202,9 @@
|
||||
For more details on our plans, visit our
|
||||
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
|
||||
</p>
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value="tier-0"
|
||||
disabled={!!anyOrgFree}
|
||||
tooltipShow={!!anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">{tierFree.description}</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{#if billingPlan === BillingPlan.PRO}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<PlanSelection bind:billingPlan {anyOrgFree} isNewOrg />
|
||||
{#if billingPlan !== BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
{
|
||||
value: BillingPlan.PRO,
|
||||
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons`
|
||||
},
|
||||
{
|
||||
value: BillingPlan.SCALE,
|
||||
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
{/if}
|
||||
{#if $organization?.billingPlanDowngrade}
|
||||
<Alert type="info" class="common-section">
|
||||
Your organization will change to a {tierToPlan(BillingPlan.FREE).name} plan once your current
|
||||
billing cycle ends and your invoice is paid on {toLocaleDate(
|
||||
Your organization will change to a {tierToPlan($organization?.billingPlanDowngrade)
|
||||
.name} plan once your current billing cycle ends and your invoice is paid on {toLocaleDate(
|
||||
$organization.billingNextInvoiceDate
|
||||
)}.
|
||||
</Alert>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{isTrial ? formatCurrency(0) : formatCurrency(currentPlan?.price)}
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage}
|
||||
{#if $organization?.billingPlan !== BillingPlan.FREE && extraUsage > 0}
|
||||
<CollapsibleItem isInfo gap={8}>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<span class="body-text-2"><b>Add-ons</b></span><span class="inline-tag"
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Alert, LabelCard } from '$lib/components';
|
||||
import { Alert } from '$lib/components';
|
||||
import {
|
||||
EstimatedTotalBox,
|
||||
PlanComparisonBox,
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import PlanExcess from '$lib/components/billing/planExcess.svelte';
|
||||
import PlanSelection from '$lib/components/billing/planSelection.svelte';
|
||||
import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte';
|
||||
import { BillingPlan, Dependencies, feedbackDowngradeOptions } from '$lib/constants';
|
||||
import {
|
||||
@@ -21,7 +22,7 @@
|
||||
InputTextarea,
|
||||
Label
|
||||
} from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { formatCurrency } from '$lib/helpers/numbers.js';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
@@ -29,7 +30,7 @@
|
||||
WizardSecondaryHeader
|
||||
} from '$lib/layout';
|
||||
import { type Coupon, type PaymentList } from '$lib/sdk/billing';
|
||||
import { plansInfo, tierFree, tierPro, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organization, organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -93,7 +94,11 @@
|
||||
billingPlan = plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
billingPlan = BillingPlan.PRO;
|
||||
if ($organization?.billingPlan === BillingPlan.SCALE) {
|
||||
billingPlan = BillingPlan.SCALE;
|
||||
} else {
|
||||
billingPlan = BillingPlan.PRO;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
@@ -106,9 +111,9 @@
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (billingPlan === BillingPlan.FREE) {
|
||||
if (isDowngrade) {
|
||||
await downgrade();
|
||||
} else {
|
||||
} else if (isUpgrade) {
|
||||
await upgrade();
|
||||
}
|
||||
}
|
||||
@@ -238,9 +243,7 @@
|
||||
|
||||
$: isUpgrade = billingPlan > $organization.billingPlan;
|
||||
$: isDowngrade = billingPlan < $organization.billingPlan;
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: if (billingPlan === BillingPlan.PRO) {
|
||||
$: if (billingPlan !== BillingPlan.FREE) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
$: isButtonDisabled = $organization.billingPlan === billingPlan;
|
||||
@@ -261,72 +264,37 @@
|
||||
For more details on our plans, visit our
|
||||
<Button href="https://appwrite.io/pricing" external link>pricing page</Button>.
|
||||
</p>
|
||||
{#if anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
<Alert type="warning" class="u-margin-block-16">
|
||||
You are limited to one {tierToPlan(BillingPlan.FREE).name} organization per account.
|
||||
Consider upgrading or deleting <Button
|
||||
link
|
||||
href={`${base}/console/organization-${anyOrgFree.$id}`}
|
||||
>{anyOrgFree.name}</Button
|
||||
>.
|
||||
</Alert>
|
||||
{/if}
|
||||
<ul
|
||||
class="u-flex u-gap-16 u-margin-block-start-8"
|
||||
class:u-margin-block-start-16={anyOrgFree && billingPlan === BillingPlan.PRO}
|
||||
style="--p-grid-item-size:16em; --p-grid-item-size-small-screens:16rem; --grid-gap: 1rem;">
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={!!anyOrgFree}
|
||||
value="tier-0"
|
||||
tooltipShow={!!anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization.billingPlan === BillingPlan.FREE}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">{tierFree.description}</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
<PlanSelection
|
||||
bind:billingPlan
|
||||
anyOrgFree={!!anyOrgFree}
|
||||
class={anyOrgFree && billingPlan !== BillingPlan.FREE
|
||||
? 'u-margin-block-start-16'
|
||||
: ''} />
|
||||
|
||||
<li class="u-flex-basis-50-percent">
|
||||
<LabelCard name="plan" bind:group={billingPlan} value="tier-1">
|
||||
<svelte:fragment slot="custom">
|
||||
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization.billingPlan === BillingPlan.PRO}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-gray u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{#if isDowngrade}
|
||||
<PlanExcess tier={BillingPlan.FREE} class="u-margin-block-start-24" />
|
||||
{#if billingPlan === BillingPlan.FREE}
|
||||
<PlanExcess
|
||||
tier={BillingPlan.FREE}
|
||||
class="u-margin-block-start-24"
|
||||
members={data?.members?.total ?? 0} />
|
||||
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE}
|
||||
{@const extraMembers = collaborators?.length ?? 0}
|
||||
<Alert type="error" class="u-margin-block-start-24">
|
||||
<svelte:fragment slot="title">
|
||||
Your monthly payments will be adjusted for the Pro plan
|
||||
</svelte:fragment>
|
||||
After switching plans,
|
||||
<b
|
||||
>you will be charged {formatCurrency(
|
||||
extraMembers *
|
||||
($plansInfo?.get(billingPlan)?.addons?.member?.price ?? 0)
|
||||
)} monthly for {extraMembers} team members.</b> This will be reflected in
|
||||
your next invoice.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if billingPlan === BillingPlan.PRO && $organization.billingPlan !== BillingPlan.PRO}
|
||||
<!-- Show email input if upgrading from free plan -->
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan === BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
@@ -348,7 +316,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isUpgrade && billingPlan === BillingPlan.FREE && $organization.billingPlan !== BillingPlan.FREE}
|
||||
{#if isDowngrade}
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputSelect
|
||||
id="reason"
|
||||
@@ -366,12 +334,13 @@
|
||||
{/if}
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== BillingPlan.PRO}
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan}
|
||||
<EstimatedTotalBox
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget />
|
||||
bind:billingBudget
|
||||
{isDowngrade} />
|
||||
{:else}
|
||||
<PlanComparisonBox downgrade={isDowngrade} />
|
||||
{/if}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Dependencies } from '$lib/constants';
|
||||
|
||||
export const load = async ({ depends, parent }) => {
|
||||
const { members } = await parent();
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
|
||||
depends(Dependencies.ORGANIZATION);
|
||||
return {
|
||||
members
|
||||
};
|
||||
|
||||
@@ -59,14 +59,12 @@
|
||||
|
||||
<Modal title="Invite member" {error} size="big" bind:show={showCreate} onSubmit={create}>
|
||||
{#if isCloud}
|
||||
<Alert type="info">
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE}
|
||||
You can add unlimited organization members on the {plan.name} plan at no cost.
|
||||
{:else if $organization?.billingPlan === BillingPlan.PRO}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO}
|
||||
<Alert type="info">
|
||||
You can add unlimited organization members on the {plan.name} plan for
|
||||
<b>{formatCurrency(plan.addons.member.price)} each per billing period</b>.
|
||||
{/if}
|
||||
</Alert>
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
<FormList>
|
||||
<InputEmail
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
<span
|
||||
class="icon-info"
|
||||
use:tooltip={{
|
||||
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan for ${formatCurrency(plan.addons.member.price)} each per billing period.`
|
||||
content: `You can add unlimited organization members on the ${tierToPlan($organization.billingPlan).name} plan ${$organization.billingPlan === BillingPlan.PRO ? `for ${formatCurrency(plan.addons.member.price)} each per billing period.` : '.'}`
|
||||
}}></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user